The goal of this chapter is to understand the concept of functions in programming and how they are implemented in Julia. We will explore how to create our own functions, how they can receive parameters and return values, and we will also introduce the concept of recursion.
Functions as Natural Abstractions¶
In the previous class, we already used some predefined functions in Julia. Functions are blocks of code that perform specific tasks and can be reused whenever needed. They let us abstract complex operations into simple commands, making code more readable and modular.
Let’s recall some of the functions we’ve already used:
typeof()- Takes a value as a parameter and returns its type.div()- Takes two numbers and returns the integer division of the first by the second.print()andprintln()- Print values to the console, with the second one adding a line break after printing.
So far, we’ve seen examples of function calls like sin(0.5) or sqrt(4). When we write the name of a function followed by parentheses containing arguments, we are “calling” or “invoking” that function.
But how are these functions created? In Julia, we can declare our own functions using the function keyword:
function dobro(x)
return x * 2
enddobro (generic function with 1 method)Here, dobro is the name of the function, x is a parameter (a value that the function receives), and return x * 2 specifies what the function should compute and return when called. Providing parameters to a function is optional.
After declaring the function, we can call it multiple times with different arguments:
resultado1 = dobro(5) # Chama a função com o argumento 5
println(resultado1) # Imprime 10
resultado2 = dobro(3.5) # Chama a função com o argumento 3.5
println(resultado2) # Imprime 7.010
7.0
Declaring a function and calling it are two different actions:
Declaration: Defines how the function should behave (happens once)
Call: Executes the function with specific values (can happen multiple times)
Functions like sin(), sqrt(), and big() already come declared in Julia, which is why we can use them directly.
Functions Calling Other Functions¶
A function can call another function, allowing for the composition of more complex operations:
function imprime(a)
println("Vou imprimir ", a)
end
function imprimeduasvezes(a)
imprime(a)
imprime(a)
endimprimeduasvezes (generic function with 1 method)Let’s test our new function:
imprimeduasvezes(13)Vou imprimir 13
Vou imprimir 13
Accessing Function Documentation¶
We can ask the interpreter for help to better understand how these functions work. To do this, we use the question mark ? or the @doc macro before the function name:
# Exemplos de como acessar a documentação
@doc typeof@doc div@doc printlnBy checking the documentation, we discover that some functions such as div() can be used with an alternative syntax, such as \div. This kind of notation is particularly useful for mathematical operations.
Conversion Functions¶
An important category of functions in Julia are conversion functions, which transform values from one type into another. Let’s look at some examples:
# Converte uma string para um número em ponto flutuante
parse(Float64, "32")32.0# Converte um número em ponto flutuante para um inteiro (removendo a parte decimal)
trunc(Int64, 2.25)2# Converte um inteiro para um número em ponto flutuante
float(2)2.0# Converte um número para uma string
string(3)"3"# Converte um número em ponto flutuante para uma string
string(3.57)"3.57"Mathematical Functions¶
Julia has a large library of ready-to-use mathematical functions. Here are some of the most common ones:
| Function | Description |
|---|---|
sin(x) | Computes the sine of ( x ) in radians |
cos(x) | Computes the cosine of ( x ) in radians |
tan(x) | Computes the tangent of ( x ) in radians |
deg2rad(x) | Converts ( x ) from degrees to radians |
rad2deg(x) | Converts ( x ) from radians to degrees |
log(x) | Computes the natural logarithm of ( x ) |
log(b, x) | Computes the logarithm of ( x ) in base ( b ) |
log2(x) | Computes the base-2 logarithm of ( x ) |
log10(x) | Computes the base-10 logarithm of ( x ) |
exp(x) | Computes the natural exponential of ( x ) |
abs(x) | Computes the absolute value of ( x ) |
sqrt(x) | Computes the square root of ( x ) |
cbrt(x) | Computes the cube root of ( x ) |
factorial(x) | Computes the factorial of ( x ) |
A good way to get familiar with these functions is to experiment with different values and check the results. For more complex functions, there may already be ready-made implementations in Julia. A useful tip is to search the internet using keywords like “julia lang hyperbolic sin” to find the function you need. In general, searching in English tends to produce better results.
Function Overloading¶
In Julia, we can have functions with the same name but with different numbers or types of parameters. This is called “function overloading”:
function recebe(a)
println("Recebi um parâmetro: ", a)
end
function recebe(a, b)
println("Recebi dois parâmetros: ", a, " e ", b)
endrecebe (generic function with 2 methods)The interpreter decides which version of the function to call based on the arguments provided:
recebe(1)Recebi um parâmetro: 1
recebe(1, 2)Recebi dois parâmetros: 1 e 2
We can also call functions using variables and expressions as arguments:
a = 10
recebe(a)
recebe(a, a + 1)Recebi um parâmetro: 10
Recebi dois parâmetros: 10 e 11
Functions That Return Values¶
So far, we’ve seen functions that only print messages but don’t return any value. The return type of these functions is Nothing, indicating that they don’t produce a value that can be assigned to a variable.
However, we often want our functions to compute and return values. For this, we use the return keyword:
function soma1(a)
return a + 1
endsoma1 (generic function with 1 method)Now we can use this function in expressions and assignments:
resultado = soma1(5)
println("O resultado é: ", resultado)O resultado é: 6
# Também podemos usar o resultado em outras expressões
println("Resultado multiplicado por 2: ", soma1(5) * 2)Resultado multiplicado por 2: 12
We can create functions for more complex calculations:
function hipotenusa(a, b)
hip = sqrt(a^2 + b^2)
return hip
endhipotenusa (generic function with 1 method)Let’s test our function:
# Calculando a hipotenusa de um triângulo 3-4-5
hipotenusa(3, 4)5.0Introduction to Recursion¶
Now let’s explore a fundamental concept in programming: recursion. A recursive function is one that calls itself as part of its execution. This may seem strange at first, but it’s a powerful technique for solving certain kinds of problems.
Let’s start with a simple example: computing the factorial of a number. The factorial of (written ) is the product of all positive integers less than or equal to . For example, .
The factorial can be defined recursively as:
Base case:
Recursive case:
Let’s implement this in Julia:
function fatorial(n)
if n == 0
return 1 # Caso base
else
return n * fatorial(n - 1) # Chamada recursiva
end
endfatorial (generic function with 1 method)Let’s test our function:
fatorial(5)120To understand how recursion works, let’s trace through the computation of fatorial(3) step by step:
We call
fatorial(3)Since 3 is not equal to 0, we execute
return 3 * fatorial(2)
Now we need to compute
fatorial(2)Since 2 is not equal to 0, we execute
return 2 * fatorial(1)
Now we need to compute
fatorial(1)Since 1 is not equal to 0, we execute
return 1 * fatorial(0)
Now we need to compute
fatorial(0)Since 0 equals 0, we return 1
Now we can complete the computation of
fatorial(1)=Now we can complete the computation of
fatorial(2)=Finally, we complete the computation of
fatorial(3)=
Recursion has two fundamental parts:
A base case that ends the recursion (in our example, when )
A recursive case that moves the problem closer to the base case (in our example, by reducing by 1)
The recursion must always reach the base case, otherwise the function will keep calling itself indefinitely, causing a stack overflow error.
More Recursion Examples¶
Let’s implement a recursive function for a countdown:
function contagem(n)
if n < 0
println("Fim!")
else
print(n, " ")
contagem(n - 1)
end
endcontagem (generic function with 1 method)Let’s test our function:
contagem(5)5 4 3 2 1 0 Fim!
We can also use recursion to compute the sum of the first integers:
function soma(n)
if n == 0
return 0 # Caso base
else
return n + soma(n - 1) # Caso recursivo
end
endsoma (generic function with 1 method)Let’s test our function:
soma(10)55Another interesting example is computing the sum of the terms of the harmonic series:
function somaharmonica(atual, n)
# Caso base: quando chegamos ao último termo
if atual > n
return 0.0
else
# Caso recursivo: somamos o termo atual e chamamos a função para o próximo termo
return 1.0 / atual + somaharmonica(atual + 1, n)
end
endsomaharmonica (generic function with 1 method)Let’s compute the sum of the first 10 terms of the harmonic series:
somaharmonica(1, 10)2.9289682539682538Check Your Understanding¶
What is the difference between a function that prints a value and a function that returns a value? Why does this distinction matter?
Explain the concept of recursion in your own words. What are the essential components of a recursive function?
Write a function that takes a positive integer and returns the sum of its digits. For example, for the number 123, the function should return .
Implement a function that computes the -th number of the Fibonacci sequence using recursion. Recall that
, if .
Write a function that takes two numbers as parameters and returns their greatest common divisor (GCD) using Euclid’s algorithm recursively.
Explore on Your Own¶
Research the concept of the “call stack” and how it relates to recursion. What are the practical limitations of recursion due to the call stack?
Think about how you could optimize the recursive Fibonacci function to avoid repeated computations.
Hint: look up “memoization”.
Explore functions with a variable number of arguments in Julia using “splat” syntax (
...).Find out how you can define default values for function parameters in Julia.