Programming in JuliaFunctions
A good rule of thumb is that a function should be sufficiently general to be re-usable without duplicating internal logic, but specific enough that you can actually implement it.
Exercise
How could the design of the following code be improved?
function remove_one_leading_space(S)
if S[1] == " "
S[1:end]
else
S
end
end
function remove_two_leading_spaces(S)
if S[1:2] == " "
S[2:]
else
S
end
end
function remove_three_leading_spaces(S)
if S[1:3] == " "
S[3:end]
else
S
end
end
Solution. We should have a single function to remove whatever number of leading spaces the string happens to have. The design above has the problem that we have to figure out how many leading spaces there are before we can call the appropriate function, which means that most of the work that should be performed by the function will have to be performed when the function is called. Thus separation of concerns is not achieved.
Arguments
The objects supplied to a function when it's called are referred to as the function's arguments. The variables which represent the arguments in the function definition are called parameters. The block of code that runs when the function is called is the body of the function.
Exercise
In the following block of code, s
is "hello"
is
function duplicate(s)
s * s
end
duplicate("hello")
We can give parameters default values and supply arguments for those parameters optionally when calling the function:
function line(m, x; b=0) m * x + b end line(2,3) # returns 6 line(5,4,b=2) # returns 22
The arguments 1, 2, and 3 in this example are called positional arguments, and 5
is a keyword argument.
If a string literal appears immediately before a function's definition, that string will be interpreted as documentation for the function. This docstring helps you and other users of your functions quickly ascertain how they are meant to be used. A function's docstring can accessed in a Julia REPL or notebook by prepending the funtion name with a question mark. For example, ?print
pulls up the docstring for the built-in print
function.
Anonymous functions
A function may be defined without assigning a name to it. Such a function is said to be anonymous. Julia's anonymous function (x,y) -> x^2 + y^2
in Julia. A common situation where anonymous functions can be useful is when supplying one function to another as an argument. For example:
apply_three_times(f, x) = f(f(f(x))) apply_three_times(x->x^2, 2)
Exercise
Write a function that takes two arguments a
and b
and a function f
and returns a
if f(a) < f(b)
and b
otherwise. Then use anonymous function syntax to call your function with two numbers and the negation function .
Solution. Here's an example solution:
function which_bigger(a, b, f) if f(a) < f(b) a else b end end which_bigger(4, 6, x->-x)
Scope
The scope of a variable is the region in the program where it is accessible. For example, if you define x
to be 47
on line 413 of your file and get an error because you tried to use x
on line 35, the problem is that the variable wasn't in scope yet.
A variable defined in the main body of a file has global scope, meaning that it is visible throughout the program from its point of definition.
A variable defined in the body of a function is in that function's local scope. For example:
function f(x) y = 2 x + y end y
Exercise
Try nesting one function definition inside another. Are variables in the enclosing function body available in the inner function. What about vice versa?
function f() function g() j = 2 i end print(j) i = 1 g() end f()
Solution. The variable defined in the inner function is not in scope in the body of the outer function, but the variable defined in the body of the outer function is in scope in the body of the inner function.
Testing
It's highly recommended to write tests to accompany your functions, so you can confirm that each function behaves as expected. This is especially important as your codebase grows, because changes in one function can lead to problems in other functions that use it. Having a way to test functions throughout your codebase helps you discover these breakages quickly, before they cause harm.
The standard way to do this in Julia (which you have already seen several times in this course) is write @test
statements. An @test
statement throws an error if the following expression evaluates to false
. In a full-fledged Julia project, these tests typically go in a directory called test
so that tests can be run for the whole project.
""" Concatenate strings s and t, ensuring a space between them if s ends with a non-space character and t begins with a non-space character """ function space_concat(s,t) if s[end] == ' ' || t[1] == ' ' s * t else return s * " " * t end end using Test @test space_concat("foo", "bar") == "foo bar" @test space_concat("foo ", "bar") == "foo bar" test_space_concat() space_concat("foo", "bar")
Exercise
The test cases above don't cover the degenerate situation where one of the strings is empty. Does the function return correct values for these degenerate cases?
Solution. We check the empty string conditions prior to checking the last/first characters. This solves the problem because ||
is short-circuiting: if the first bool is true
in an ||
operation, the second is never evaluated.
function space_concat(s,t) if s == "" || t == "" || s[end] == ' ' || t[1] == ' ' s * t else s * " " * t end end using Test @test space_concat("foo", "bar") == "foo bar" @test space_concat("foo ", "bar") == "foo bar" @test space_concat("foo", "") == "foo" @test space_concat("", "bar") == "bar"
Exercises
Exercise
Write a function which accepts two strings as input and returns the concatenation of those two strings in alphabetical order.
Hint: Make a guess about which operator can be used to compare strings alphabetically.
function alphabetical_concat(s,t) # add code here end using Test @test alphabetical_concat("alphabet", "soup") == "alphabetsoup" @test alphabetical_concat("socks", "red") == "redsocks"
Solution.
function alphabetical_concat(s,t) if s < t s * t else t * s end end