Sõnastik

Valige vasakul üks märksõnadest ...

Programming in JuliaFunctions

Lugemise aeg: ~25 min

Functions can be used to organize code and achieve separation of concerns: once a function is written, it may be relied upon to perform its designated task without the programmer having to think about how it accomplishes that task. This conceptual aid is crucial for writing maintainable code to solve large, complex problems.

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 , while "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 syntax looks like the corresponding math syntax: the function (x,y)\mapsto x^2 + y^2 can be written as (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 x\mapsto -x.

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? Add test cases for this, and fix the function so that they pass.

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
Bruno
Bruno Bruno