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 struct
s 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 Int64
s 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.