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.
How could the design of the following code be improved?
function remove_one_leading_space(S) if S == " " 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.
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.
In the following block of code,
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,
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)
Write a function that takes two arguments
b and a function
f and returns
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)
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
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.
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 == ' ' 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")
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 == ' ' 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"
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"
function alphabetical_concat(s,t) if s < t s * t else t * s end end