1.3. Functions#

A function is a named block of reusable code that performs a specific task. To use a function, you call it by its name, followed by parentheses. You can pass values into the function by placing them inside the parentheses; these values are called arguments.

For example, we’ve already used println(x), a built-in function that prints the value of x to the console.

Julia provides a rich library of standard mathematical functions. Here are a few common ones:

Function

Description

abs(x)

The absolute value of x

sign(x)

The sign of x (-1, 0, or 1)

sqrt(x) or √x

The square root of x

cbrt(x) or ∛x

The cube root of x

exp(x)

The natural exponential \(e^x\)

log(x)

The natural logarithm of x

log(b,x)

The base-b logarithm of x

log2(x)

The base-2 logarithm of x

log10(x)

The base-10 logarithm of x

1.3.1. Trigonometric and Hyperbolic Functions#

All standard trigonometric and hyperbolic functions are built-in. The default trigonometric functions expect angles in radians.

sin    cos    tan    cot    sec    csc
sinh   cosh   tanh   coth   sech   csch
asin   acos   atan   acot   asec   acsc
asinh  acosh  atanh  acoth  asech  acsch
sinc   cosc

For convenience, Julia also provides versions that work with angles in degrees:

sind   cosd   tand   cotd   secd   cscd
asind  acosd  atand  acotd  asecd  acscd

1.3.2. Example: Ramanujan’s Constant#

Let’s evaluate the expression \(e^\pi - \pi\). While not an integer, it is famously close to 20.

exp(π) - π
19.999099979189474

1.3.3. Example: Real Roots of a Quadratic#

We can use the quadratic formula to find the roots of the equation \(x^2 + 5x + 6 = 0\). $\( r = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} \)$ For now, we will assume the roots are real; we’ll cover complex numbers in a later chapter.

# Solve x² + 5x + 6 = 0 using the quadratic formula.

# Coefficients for ax² + bx + c = 0
a = 1
b = 5
c = 6

# Apply the quadratic formula
discriminant = sqrt(b^2 - 4a*c)
root1 = (-b - discriminant) / (2a)
root2 = (-b + discriminant) / (2a)

println("The roots are ", root1, " and ", root2, ".")
The roots are -3.0 and -2.0.

1.3.4. User-Defined Functions#

You can define your own functions to make your code more organized and reusable. Here’s a simple function named my_func that takes two arguments, x and y.

function my_func(x, y)
    x + y
end
my_func (generic function with 1 method)
  • The values passed to the function are assigned to its parameters, x and y.

  • By default, a function returns the value of the last expression evaluated inside it (in this case, x + y). You can also use the return keyword for explicit returns.

  • Once defined, you can call the function using the standard parentheses syntax:

my_func(1, 2)
3

1.3.4.1. Compact Assignment Form#

For functions that contain only a single expression, Julia offers a more concise syntax:

my_func2(x, y) = x + 2y

my_func2(3, 5)
13

1.3.4.2. Anonymous Functions#

You can also create anonymous functions—functions without a name. This is useful for short, one-off operations.

# Define an anonymous function and assign it to a variable
my_func3 = (x, y) -> x + 3y
println(my_func3(3, 5))

# Define and call an anonymous function in a single line
println(((x, y) -> x + 3y)(3, 5))
18
18

1.3.4.3. Multiple Return Values#

Functions can also return multiple values. Simply list them at the end of the function, separated by commas.

function my_new_func(x, y)
    out1 = x + y
    out2 = out1 * (2x + y)
    return out1, out2
end
my_new_func (generic function with 1 method)
y1, y2 = my_new_func(2, 1)
(3, 15)

When a function returns multiple values, it technically returns a single object called a tuple. A tuple is an ordered, immutable collection of elements.

You can create your own tuples with a comma-separated list of values. Parentheses () can be used for clarity and are required in contexts where the comma might be ambiguous (like inside a function call). In many situations, such as simple assignments, the parentheses can be omitted.

my_tuple = (123, 234, "hello")
(123, 234, "hello")

Access elements of a tuple using square brackets (Julia is 1-indexed):

my_tuple[2]   # Access the second element
234

You can unpack a tuple by assigning it to a comma-separated list of variables. This is exactly what we did with the return values of my_new_func.

num1, num2, greeting = my_tuple

println(greeting, ", you have numbers ", num1, " and ", num2, ".")
hello, you have numbers 123 and 234.

This is also a convenient way to assign values to multiple variables using only a single line of code:

α, β, γ = 1/2, 1/3, 1/4
(0.5, 0.3333333333333333, 0.25)

1.3.5. Example: Quadratic Roots Function#

Let’s revisit the quadratic roots problem, this time encapsulating the logic in a function.

function real_roots_of_quadratic(a, b, c)
    # Computes the real roots of the quadratic equation ax² + bx + c = 0.
    discriminant = sqrt(b^2 - 4a*c)
    r1 = (-b - discriminant) / (2a)
    r2 = (-b + discriminant) / (2a)
    return r1, r2
end
real_roots_of_quadratic (generic function with 1 method)
real_roots_of_quadratic(1, 5, 6)
(-3.0, -2.0)
real_roots_of_quadratic(-1, 5, 6)
(6.0, -1.0)

1.3.6. Optional and Keyword Arguments#

Functions can be made more flexible by using optional and keyword arguments. This allows you to write functions that are easier to call in different situations.

1.3.6.1. Optional Arguments#

You can make function arguments optional by providing them with a default value. If a caller doesn’t provide a value for that argument, the default is used automatically.

Let’s consider solving the equation \(2x = \tan x\) using the fixed-point iteration \(x_{n+1} = \tan^{-1}(2x_n)\). Our function could take two arguments:

  • A termination tolerance δ.

  • A starting value x0.

By setting default values, we can make both of these optional.

function solve_fixed_point(δ=1e-3, x0=1.0)
    x = x0
    iter = 0
    
    while true
        iter += 1
        x_prev = x
        x = atan(2 * x_prev)
        
        println("Iteration ", iter, ": x = ", x)
        
        if abs(x - x_prev)  δ
            println("\nConverged after ", iter, " iterations.")
            return x
        end
    end
end
solve_fixed_point (generic function with 3 methods)

Now we can call the function in several ways:

# 1. No arguments: use both default values.
solve_fixed_point()
Iteration 1: x = 1.1071487177940904
Iteration 2: x = 1.1466039045135867
Iteration 3: x = 1.1595864584933437
Iteration 4: x = 1.1636959143671342
Iteration 5: x = 1.1649805955397385
Iteration 6: x = 1.165380637446075

Converged after 6 iterations.
1.165380637446075
# 2. One argument: overrides the first default value (δ).
solve_fixed_point(1e-5)
Iteration 1: x = 1.1071487177940904
Iteration 2: x = 1.1466039045135867
Iteration 3: x = 1.1595864584933437
Iteration 4: x = 1.1636959143671342
Iteration 5: x = 1.1649805955397385
Iteration 6: x = 1.165380637446075
Iteration 7: x = 1.1655050559893403
Iteration 8: x = 1.1655437371644453
Iteration 9: x = 1.1655557615495353
Iteration 10: x = 1.165559499298558

Converged after 10 iterations.
1.165559499298558
# 3. Two arguments: overrides both default values.
solve_fixed_point(1e-5, 0.5)
Iteration 1: x = 0.7853981633974483
Iteration 2: x = 1.0038848218538872
Iteration 3: x = 1.108697830869966
Iteration 4: x = 1.147128141357208
Iteration 5: x = 1.1597539140293118
Iteration 6: x = 1.1637484136986498
Iteration 7: x = 1.1649969581251391
Iteration 8: x = 1.1653857278508495
Iteration 9: x = 1.1655066387105377
Iteration 10: x = 1.1655442291805418
Iteration 11: x = 1.165555914492745
Iteration 12: x = 1.1655595468401398

Converged after 12 iterations.
1.1655595468401398

1.3.6.2. Keyword Arguments#

When a function has many arguments, it can be hard to remember their correct order. Keyword arguments solve this problem by allowing you to specify arguments by their name.

To define keyword arguments, use a semicolon ; in the function signature:

function my_func(arg1, arg2; keyword1=val1, keyword2=val2)
    # ...
end

When calling the function, you also use the names to pass values, and their order doesn’t matter.

Let’s add a maxiter keyword argument to our solver to prevent it from running forever.

function solve_fixed_point_v2(δ=1e-3, x0=1.0; maxiter=20)
    x = x0
    
    for iter = 1:maxiter
        x_prev = x
        x = atan(2 * x_prev)
        
        println("Iteration ", iter, ": x = ", x)
        
        if abs(x - x_prev)  δ
            println("\nConverged after ", iter, " iterations.")
            return x
        end
    end
    
    println("\nDid not converge within ", maxiter, " iterations.")
    return x
end
solve_fixed_point_v2 (generic function with 3 methods)

Now we can provide the optional positional arguments (δ, x0) and the keyword argument (maxiter) in a clear and flexible way.

# Use default values for δ and x0, but specify maxiter.
solve_fixed_point_v2(maxiter=5)
Iteration 1: x = 1.1071487177940904
Iteration 2: x = 1.1466039045135867
Iteration 3: x = 1.1595864584933437
Iteration 4: x = 1.1636959143671342
Iteration 5: x = 1.1649805955397385

Did not converge within 5 iterations.
1.1649805955397385
# Specify δ and maxiter, but use the default x0.
# Note that the keyword argument can be placed anywhere after the semicolon.
solve_fixed_point_v2(1e-7; maxiter=15)
Iteration 1: x = 1.1071487177940904
Iteration 2: x = 1.1466039045135867
Iteration 3: x = 1.1595864584933437
Iteration 4: x = 1.1636959143671342
Iteration 5: x = 1.1649805955397385
Iteration 6: x = 1.165380637446075
Iteration 7: x = 1.1655050559893403
Iteration 8: x = 1.1655437371644453
Iteration 9: x = 1.1655557615495353
Iteration 10: x = 1.165559499298558
Iteration 11: x = 1.1655606611549096
Iteration 12: x = 1.1655610223095458
Iteration 13: x = 1.1655611345717263
Iteration 14: x = 1.165561169467562

Converged after 14 iterations.
1.165561169467562