Julia - Functions



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 * a
f (generic function with 1 method)

julia> f(5)
25

julia> 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
   ...
   expression
end

Example

julia> function bills(money)
      if money < 0
         return false
      else
         return true
      end
   end
bills (generic function with 1 method)

julia> bills(50)
true

julia> 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
               end
mul (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")
      end
pos (generic function with 2 methods)

julia> pos(10, 30)
10, 30, 0

julia> 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’)
or
foo(“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"
      end
foo (generic function with 1 method)

julia> foo(100,20)
a is 100
b is 20
"c => 10, d => hi"

julia> foo("Hello", "Tutorialspoint", c=pi, d=22//7)
a is Hello
b 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 Hello
b 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> A
ERROR: 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)
      end
add (generic function with 1 method)

julia> d = 10
10

julia> 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 : x
sum (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.

Advertisements