Function, the building blocks of Julia, is a collected group of instructions that maps a tuple of argument values to a return value. It acts as the subroutines, procedures, blocks, and other similar structures concepts found in other programming languages.
Defining Functions
There are following three ways in which we can define functions −
When there is a single expression in a function, you can define it by writing the name of the function and any arguments in parentheses on the left side and write an expression on the right side of an equal sign.
Example
julia> f(a) = a * af (generic function with 1 method)julia> f(5)25julia> func(x, y) = sqrt(x^2 + y^2)func (generic function with 1 method)julia> func(5, 4)6.4031242374328485
If there are multiple expressions in a function, you can define it as shown below −
function functionname(args) expression expression expression ... expressionend
Example
julia> function bills(money) if money < 0 return false else return true end endbills (generic function with 1 method)julia> bills(50)truejulia> bills(-50)false
If a function returns more than one value, we need to use tuples.
Example
julia> function mul(x,y) x+y, x*y endmul (generic function with 1 method)julia> mul(5, 10)(15, 50)
Optional Arguments
It is often possible to define functions with optional arguments i.e. default sensible values for functions arguments so that the function can use that value if specific values are not provided. For example −
julia> function pos(ax, by, cz=0) println("$ax, $by, $cz") endpos (generic function with 2 methods)julia> pos(10, 30)10, 30, 0julia> pos(10, 30, 50)10, 30, 50
You can check in the above output that when we call this function without supplying third value, the variable cz defaults to 0.
Keyword Arguments
Some functions which we define need a large number of arguments but calling such functions can be difficult because we may forget the order in which we have to supply the arguments. For example, check the below function −
function foo(a, b, c, d, e, f)...end
Now, we may forget the order of arguments and the following may happen −
foo(“25”, -5.6987, “hello”, 56, good, ‘ABC’)orfoo(“hello”, 56, “25”, -5.6987, ‘ABC’, good)
Julia provides us a way to avoid this problem. We can use keywords to label arguments. We need to use a semicolon after the function’s unlabelled arguments and follow it with one or more keyword-value pair as follows −
julia> function foo(a, b ; c = 10, d = "hi") println("a is $a") println("b is $b") return "c => $c, d => $d" endfoo (generic function with 1 method)julia> foo(100,20)a is 100b is 20"c => 10, d => hi"julia> foo("Hello", "Tutorialspoint", c=pi, d=22//7)a is Hellob is Tutorialspoint"c => π, d => 22//7"
It is not necessary to define the keyword argument at the end or in the matching place, it can be written anywhere in the argument list. Following is an example −
julia> foo(c=pi, d =22/7, "Hello", "Tutorialspoint")a is Hellob is Tutorialspoint"c => π, d => 3.142857142857143"
Anonymous Functions
It is waste of time thinking a cool name for your function. Use Anonymous functions i.e. functions with no name instead. In Julia, such functions can be used in number of places such as map() and in list comprehensions.
The syntax of anonymous functions uses the symbol ->. You can check the below example −
A -> A^3 + 3A - 3
The above function is an anonymous function that takes an argument A and returns A^3 + 3A – 3.
It can be used with map() function whose first argument is a function and we can define an one-off function that exists just for one particular map() operation. The example is given below −
julia> map(A -> A^3 + 3A - 3, [10,3,-2])3-element Array{Int64,1}: 1027 33 -17
Once the map() function finishes, the function and argument both will disappear −
julia> AERROR: UndefVarError: A not defined
Recursive Functions
In Julia, the functions can be nested. It is demonstrated in the example given below −
julia> function add(x) Y = x * 2 function add1(Y) Y += 1 end add1(Y) endadd (generic function with 1 method)julia> d = 1010julia> add(d)21
In the same way, a function in Julia can be recursive also. It means the function can call itself. Before getting into details, we first need to test a condition in code which can be done with the help of ternary operator “?”. It takes the form expr ? a : b. It is called ternary because it takes three arguments. Here the expr is a condition, if it is true then a will be evaluated otherwise b. Let us use this in the following recursive definition −
julia> sum(x) = x > 1 ? sum(x-1) + x : xsum (generic function with 1 method)
The above statement calculates the sum of all the integers up to and including a certain number. But in this recursion ends because there is a base case, i.e., when x is 1, this value is returned.
The most famous example of recursion is to calculate the nth Fibonacci number which is defined as the sum of two previous Fibonacci numbers. Let us understand it with the below given example −
julia> fib(x) = x < 2 ? x : fib(x-1) + fib(x-2)fib (generic function with 1 method)
Therefore while using recursion, we need to be careful to define a base case to stop calculation.
Map
Map may be defined as a function that takes the following form −
map(func, coll)
Here, func is a function applied successively to each element of collection coll. Map generally contains the anonymous function and returns a new collection. The example is given below −
julia> map(A -> A^3 + 3A - 3, [10,3,-2])3-element Array{Int64,1}: 1027 33 -17
Filter
Filter may be defined as a function that takes the following form −
filter(function, collection)
Filter function returns a copy of collection and removes elements for which the function is false. The example is given below −
julia> array = Int[1,2,3]3-element Array{Int64,1}: 1 2 3 julia> filter(x -> x % 2 == 0, array)1-element Array{Int64,1}: 2
Generic Functions
In Julia, we saw that all the functions are inherently defined as Generic. It means that the functions can be used for different types of their arguments. In simple words, whenever the function will be called with arguments of a new type, the Julia compiler will generate a separate version of that function.
On the other hand, a function for a specific combination of arguments types is called a Method. So, in order to define a new method for a function, which is called overloading, we need to use the same function name but with different arguments types.
Multiple dispatch
Julia has a mechanism called Multiple Dispatch, which neither Python nor C++ implements. Under this mechanism, Julia will do a lookup in the vtable at runtime (whenever a function is called) to find which existing method it should call based on the types of all its arguments.
Let us understand the concept of multiple dispatch with the help of an example in which we will define a function that takes 2 arguments returning a string. But in some methods we will annotate the types of both arguments or single argument.
julia> foo(A, B) = "base case"foo (generic function with 1 method)julia> foo(A::Number, B::Number) = "A and B are both numbers"foo (generic function with 2 methods)julia> foo(A::Number, B) = "A is a number"foo (generic function with 3 methods)julia> foo(A, B::Number) = "B is a number"foo (generic function with 4 methods)julia> foo(A::Integer, B::Integer) = "A and B are both integers"foo (generic function with 5 methods)
We have seen that this returns foo with 5 methods. When A and B have no types(as in base case), then their type is any.
From the following, we can see how the appropriate method will be chosen −
julia> foo(4.5, 20)"A and B are both numbers"julia> foo(20, "Hello")"A is a number"julia> foo(50, 100)"A and B are both integers"julia> foo("Hello", [100,200])"base case"
The advantage of multiple dispatch is that it will never result in error because if no other method is matched, the base case method will be invoked, for sure.