Functions and Modules

Encapsulate your Julia code in functions and modules, for convenience and performance.

by Martin D. Maas, Ph.D

@MartinDMaas

Last updated: 2021-09-30

Functions play a key role in structuring Julia code

As Julia is an interpreted language with a JIT compiler, if we care about performance, we need to be running compiled sections of the code. And, in order to get our code compiled automatically, we need to wrap it up inside a function.

So the number one performance tip is to learn how to write a function.

Fortunately, this is very easy to do, as shown below.

Basic function

As an example, lets define a function that will evaluate the first \( N \) terms of the quadratic series

function sum_series(n)
    x = 0
    for k in 1:n
        x = x + (1/k)^2
    end
    return x
end

After executing that code, we can call our function from the REPL, by typing:

sum_series(100000)
1.6449240668982423

One-line functions

In Julia, we can also define a function with a single line of code. For example, by doing:

sum_series2(N) = sum(1/n^2 for n=1:N)

We can then call it just like above:

sum_series2(100000)
1.6449240668982423

Array Functions and the Dot Operator

It often happens that we have a function which can be applied to a scalar number, but we want to apply it to an array. Of course, we could resort to a for loop to this element-wise function application. But that would be too much hassle.

Fortunately, in Julia we can easily turn a function than accepts a scalar value, and apply it element-wise to an array. The way to do this is to employ a ‘dot’ after the function’s name.

For example, lets define a scalar function ‘f’, and apply it to an array.

f(x) = 3x^3/(1+x^2)
x = [2π/n for n=1:30]
y = f.(x)

Failing to use the dot in the above example would have lead to an error.

A common ‘gotcha’ is that, for example, trigonometric functions (or even arithmetic functions) can also require this treatment. We have to do:

y = sin.(x)

In case we need to use the dot operator a lot of times in an expression, instead of doing

y = 2x.^2 + 3x.^5 - 2x.^8

we can resort to broadcasting the dot operator, like this

y = @. 2x^2 + 3x^5 - 2x^8

Performance Tip #1

The most basic performance tip for writing efficient Julia code is the following: Write performance-critical code inside functions.

Actually, the way Julia’s JIT compiler works, is that the first time we call a given functions with specific types of inputs, the function gets compiled. So, after the first time we call the function, it will generally run much faster.

Alternatively, we can pre-compile our function, alerting the JIT compiler of the input variable types. For example, in our example above we should alert the JIT compiler than the variable n is an integer. We can do this by running:

precompile(sum_series, (Int,))

For additional ways of improving code speed, make sure to check out the Performance Tips section of the official documentation. Performance is actually a huge topic in Julia!

Modules

In order to put our function in a module and import it, we first create a new ``myModule.jl’‘ file, with the following content

module myModule

export sum_series

function sum_series(n)
    x = 0
    for k in 1:n
        x = x + (1/k)^2
    end
    return x
end

end

and then we can call it from an other file, by doing

include("myModule.jl")

Then, our module will get compiled and available. We can access the exported function, by doing

x = myModule.sum_series(100000)

Conclusion

This post covered the basics of how to structure computations in Julia with functions and modules.