Functions

In this section, we cover how to define and use functions in Julia. We also cover named arguments, default values, and functions with different argument types.

Defining Functions

Functions have a very similar definition syntax. They start with the keyword function followed by the name and arguments, then the body, and finally the end keyword:

function name(args)
    # this is the function body
    # stuff gets done here
end

For example, if we wanted to define a function double which doubles its argument, we would do something like this:

function double(x)
    2*x
end

double(2)
4

Implicit and Explicit Returns

You may notice that in our definition of double, we didn’t need to include a keyword such as return to tell Julia what the output is. Julia supports lazy typing, aspects of which you’ve seen already (e.g. defaulting to 64-bit numeric types), and as such it assumes that the last line executed is what you want returned if not keyword is included. If you so desire, you can include the return keyword and nothing will change:

function return_double(x)
    return 2*x
end

return_double(2)
4

For the sake of consistency and clarity of code, I will be using explicit returns in this textbook.

Named Arguments and Default Values

In Julia, it is possible to have named arguments with default values. Keyword parameters are separated from non-keyword paremeters using a semicolon in the function definition, and their default values are set with the assignment operator =, as in many other languages.

function pow(base; exp=2)
    return base ^ exp
end

pow(2), pow(2, exp=3)
(4, 8)

When an argument is keyworded, Julia will not allow you to pass it positionally. This means that the call pow(2, 3) in the example above would result in an error.

pow(2, 3)

    MethodError: no method matching pow(::Int64, ::Int64)
    Closest candidates are:
      pow(::Any; exp) at In[23]:2

    

    Stacktrace:

     [1] top-level scope at In[24]:1


It is possible, although not recommended, to include keyworded arguments before or without a semicolon, which will allow them to retain their default value but have their arguments passed positionally also.

# NOT recommended
function pow(base, exp=2)
    return base ^ exp
end

pow(2, 3)
8

Input Types

While Julia does not require it, it is possible to specify the input type of arguments when defining a function. This is useful for overriding functions for custom classes (called structs in Julia) and for altering the behavior of a function depending upon its input.

Let’s consider the case when we want to return an integer doubled even when a different numeric type (e.g. a float) is passed as the argument. We would want to construct our function to accept both integers and floats, but behave differently when the input is an integer as opposed to a float. The type of an argument is specified in the function signature using the :: syntax:

function my_func(arg_1::Type, arg_2::Type)

Continuing our double example from earlier, consider the following definitions of int_double:

function int_double(x::Int64)
    return 2*x
end

int_double(2)
4
function int_double(x::Float64)
    x = Int64(floor(x))
    return 2 * x
end

int_double(2.2)
4

In the second implementation of int_double, we convert our argument x to an integer. (Note that we call floor on x first; if we had not done this, we would have gotten an error from Julia because you can’t convert floats to integers if the float has a nonzero fractional part.)

One thing to consider when you construct a function that specifies input types is that we get errors when calling the method on types that are not covered in the definitions.

int_double(Float32(2.2))

    MethodError: no method matching int_double(::Float32)
    Closest candidates are:
      int_double(!Matched::Float64) at In[7]:2
      int_double(!Matched::Int64) at In[1]:2

    

    Stacktrace:

     [1] top-level scope at In[9]:1


This happens because we have not defined an int_double function with the signature int_double(::Float32). One way of alleviating a subset of these issues is to use unions of types, which are defined using Union{} syntax. Any type listed in the union will be accepted as a valid argument for that function.

function int_double(x::Union{Int64,Int32})
    return 2*x
end

function int_double(x::Union{Float64,Float32})
    x = Int(floor(x))
    return 2*x
end

int_double(Int32(2)), int_double(Float32(2.2))
(4, 4)

Another way to ensure that you don’t run into errors is to provide a final method with no type specification that does something else with the input, or prints an error message.

function int_double(x)
    println("Argument is not a valid type.")
end

int_double("2"), int_double(Int8(2));
Argument is not a valid type.
Argument is not a valid type.

Julia checks the functions with specified types before going to the generic, so our int_double(2.2) will still work.

int_double(2.2)
4

Anonymous Functions

Finally, Julia provides a simple syntax for defining anonymous (lambda) functions:

(args) -> action

As an example, consider the lambda_pow implementation below, which is similar to the pow function defined above.

lambda_pow = (base, exp) -> base ^ exp

lambda_pow(2, 3)
8

Exercises

Exercise 2.1.1: Define a function with the signature below that returns $ax^2 + bx + c$.

function quad(a, b, c, x)

Exercise 2.1.2: Define a higher order function make_quad(a, b, c) with the signature below that accepts Int64s as arguments and returns a function with the signature f(x) that returns $ax^2 + bx + c$.

Exercise 2.1.3: Create another function make_quad(a, b, c) as before that accepts 64-bit numeric types for all of its arguments (i.e. integers and floats). Implicily return an anonymous function in this implementation.

Exercise 2.1.4: Finally, create a function make_quad(a, b, c) that prints the error "Argument(s) not acceptable type." if the arguments are not 64-bit numeric types.