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 |
---|---|
|
The absolute value of |
|
The sign of |
|
The square root of |
|
The cube root of |
|
The natural exponential \(e^x\) |
|
The natural logarithm of |
|
The base- |
|
The base-2 logarithm of |
|
The base-10 logarithm of |
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. Numerical Curiosities: Almost Integers#
Mathematics is full of fascinating coincidences! A famous example is Ramanujan’s constant, \(e^{\pi\sqrt{163}}\), which is amazingly close to being a perfect integer. Let’s try to calculate it and see just how close it is.
1.3.2.1. Attempt 1: Standard Floating-Point Arithmetic#
Let’s start with the most direct approach. We’ll use Julia’s standard numeric types to calculate the value. As we’ll see, this approach can sometimes be misleading.
# This calculation uses the default `Float64` precision.
# Notice the result! It looks like a perfect integer.
# This is due to floating-point rounding; the actual value is so close to an integer
# that at this precision, the tiny fractional part is completely lost.
exp(π * sqrt(163))
2.6253741264076826e17
1.3.2.2. Attempt 2: Using High-Precision Arithmetic#
To get around the rounding issue, we need more precision. We can tell Julia to perform the calculation using BigFloat
, a type that allows for an arbitrarily high number of digits.
# By wrapping numbers with `big()`, we convert them to high-precision types.
# The calculation is now accurate, but Julia's default output for `BigFloat`
# uses scientific notation, which makes it hard to see the 'almost integer' property.
ramanujan_constant = exp(big(π) * sqrt(big(163)))
2.62537412640768743999999999999250072597198185688879353856337336990862707537278e+17
1.3.2.3. Attempt 3: Formatting the Output for Clarity#
We have the correct value stored, but we need to display it differently. A helpful package called Printf
gives us fine-grained control over how numbers are printed.
# First, we load the package.
using Printf
# The `@printf` macro lets us specify a format string.
# "%.30f" means 'print a floating-point number with 30 digits after the decimal.'
# Now we can clearly see the long string of nines!
@printf "%.30f" ramanujan_constant
262537412640768743.999999999999250072597198185689
1.3.2.4. Another Near-Integer: \(e^\pi - \pi\)#
Another well-known mathematical curiosity is the expression \(e^\pi - \pi\). While not as close to an integer as Ramanujan’s constant, it’s famously near the number 20. In this case, regular Float64
calculations are sufficient and the number will be printed without exponents by default.
# Let's evaluate this second expression.
# It's a fun example of how fundamental constants can combine in unexpected ways.
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
andy
.By default, a function returns the value of the last expression evaluated inside it (in this case,
x + y
). You can also use thereturn
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 model the vertical motion of an object under gravity using the formula \(y(t) = y_0 + v_0 t - \frac{1}{2} g t^2\). Our function will take three arguments:
The time
t
(required).The initial height
y0
(optional).The initial velocity
v0
(optional).
By setting default values for y0
and v0
, we can easily model an object starting from the ground at rest.
function calculate_height_v1(t, y0=0.0, v0=0.0)
# In this version, gravity 'g' is hard-coded to Earth's value.
g = 9.81
return y0 + v0*t - 0.5*g*t^2
end
calculate_height_v1 (generic function with 3 methods)
Now we can call the function in several ways:
# 1. One argument: uses default values for y0 and v0.
# This calculates the height of an object dropped from the ground after 3 seconds.
calculate_height_v1(3)
-44.145
# 2. Two arguments: overrides y0, but uses the default for v0.
# This calculates the height of an object dropped from a 100m cliff after 3 seconds.
calculate_height_v1(3, 100.0)
55.855
# 3. Three arguments: overrides both default values.
# An object THROWN UPWARDS at 20 m/s from a 100m cliff. Height after 3 seconds.
calculate_height_v1(3, 100.0, 20.0)
115.85499999999999
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 improve our function by making the acceleration due to gravity, g
, a keyword argument. This makes it easy to model the same scenario on different planets without changing the positional arguments.
function calculate_height(t, y0=0.0, v0=0.0; g=9.81)
return y0 + v0*t - 0.5*g*t^2
end
calculate_height (generic function with 3 methods)
Now we can provide the optional positional arguments (y0
, v0
) and the keyword argument (g
) in a clear and flexible way.
# Use default values for y0 and v0, but specify 'g' for the Moon.
# Height of an object dropped on the Moon (g ≈ 1.62) after 3 seconds.
calculate_height(3; g=1.62)
-7.290000000000001
# Specify y0 and g, but use the default v0.
# Height of an object dropped from 100m on Mars (g ≈ 3.71) after 3 seconds.
calculate_height(3, 100.0; g=3.71)
83.305