snake_case123
). Note that this is only a convention, not a language's requirement. <function>
or <operator>
).
This is just notation, and the symbols <
and >
should not be misconstrued as Julia's syntax.
Action | Keyboard Shortcut |
---|---|
Previous Section | Ctrl + 🠘 |
Next Section | Ctrl + 🠚 |
List of Sections | Ctrl + z |
List of Subsections | Ctrl + x |
Close Any Popped Up Window (like this one) | Esc |
Open All Codes and Outputs in a Post | Alt + 🠛 |
Close All Codes and Outputs in a Post | Alt + 🠙 |
Unit | Acronym | Measure in Seconds |
---|---|---|
Seconds | s | 1 |
Milliseconds | ms | 10-3 |
Microseconds | μs | 10-6 |
Nanoseconds | ns | 10-9 |
Broadly speaking, functions can be broken down into three categories:
built-in functions,
third-party functions, and
user-defined functions.
This section focuses on i) and ii), relegating iii) to the next section. We consider in particular how to call functions, which in turn leads into discussing packages.
snake_case123
). Note that this is only a convention, not a language's requirement.
When you start a new session in Julia, only a handful of very basic functions are available (e.g., those for sums, products, and subtractions). This is a deliberate choice made by Julia developers, who rely on packages to incorporate functions into the workspace. In fact, both built-in and third-party functions are contained in packages—the only difference is that the former are loaded by default.
The approach is not unique to Julia. However, Julia embraces this philosophy more profoundly than other programming languages. Thus, it doesn't even include standard functions such as averages or standard deviations, which are instead relegated to a package called Statistics
. [note] The extent to which Julia advocates for this principle is evident in Statistics
itself, where functions for computing distributions are included in another package called Distributions
.
This design philosophy is rooted on a programming principle known as modularity. The principle promotes the development of small reusable modules, rather than large intertwined code. Its main advantage is to let packages evolve independently, without bugs and deprecations spreading across the entire Julia ecosystem. The practical implication of this feature is that users need to load several packages in each session, even to perform simple tasks.
The concept of packages is tightly related to modules. Formally, modules are independent blocks of code, each acting as a separate workspace, that export a defined set of functions. In fact, when you start Julia, you're implicitly writing your script in a module called Main
.
Packages are a special types of modules, which additionally include information about their dependencies. Dependencies are defined as the necessary packages that must be load to run the package itself.
Getting access to a package's functions requires loading the package via either the keyword import
or using
. The primary difference between the two is how functions are eventually called in your code. If the package is loaded via import
, the function's name must include a prefix with the package's name. On the contrary, no prefix is needed when the package is loaded with using
.
Below, we demonstrate each approach by calling the function mean
from the package Statistics
. This package isn't loaded by default, but it comes pre-installed with Julia.
x = [1,2,3]
import Statistics #getting access to its functions will require the prefix `Statistics.`
Statistics.mean(x)
x = [1,2,3]
using Statistics #no need to add the prefix `Statistics.` to call its functions (although it's possible to do so)
mean(x)
Formally, Julia's built-in functions are contained in two packages known as Core
and Base
. Both are automatically loaded in every Julia session, with theirr functions accessible as if we had executed using Core
and using Base
. This determines that that their functions don't require adding a prefix to be called. [note] Some built-in functions may require a prefix. For instance, this is what occurs with the function called isgreater
, which must be called via Base.isgreater
. Furthermore, some submodules are also loaded by default in each session. For instance, the function Base.Iterators.accumulate
is part of the submodule Iterators
from Base
, and can be directly called using Iterators.accumulate
.
Among mathematical functions, the syntax of their most common ones is the following.
Function in Julia | Meaning |
---|---|
log(x) |
\(\ln\left(x\right)\) |
exp(x) |
\(\mathrm{e}^{x}\) |
sqrt(x) |
\(\sqrt{x}\) |
abs(x) |
\(\left|x\right|\) |
sin(x) |
\(\sin(x)\) |
cos(x) |
\(\cos(x)\) |
tan(x) |
\(\tan(x)\) |
+(2,3) # same as 2 + 3
-(2,3) # same as 2 - 3
*(2,3) # same as 2 * 3
/(2,3) # same as 2 / 3
^(2,3) # same as 2 ^ 3
When a function's name is shared across multiple packages, at least one of the packages must be loaded via import
to prevent naming conflicts. For instance, given the package Statistics
and another one called MyPackage
containing a function called mean
, Julia will throw an error if you don't load one of them with import
. [note] Defining a function that shares the name of another package's function isn't necessarily an oversight by developers. For instance, we could implement our own mean
function in a package called MyPackage
, which aims at computing averages more efficiently in certain applications.
Using import
not only avoids naming conflicts, but may also reduce ambiguity in the meaning of a function. For instance, consider a function called rank
. This name could reference a wide range concepts, depending on the context (e.g., the rank of a matrix, the order in a list). However, explicitly identifying the package when the function is called could shed some light on its intended meaning.
import
may also be useful if you have custom functions that are widely applied across your projects. For example, consider a function called table_in_pdf
, which exports Julia tables to a PDF with some predefined format. While the name of the function makes it clear what it's doing, a user could wonder if this function comes from a standard package. You could hint that this isn't the case, by placing the function in a package called userDefined
. In this way, you can load the package using import UserDefined
, and then calling the function via UserDefined.table_in_pdf
.
The concepts discussed so far will probably be all you need to use packages in Julia. However, there are a few additional features worth mentioning.
First, users can load only a subset of functions from a package. This possibility is particularly relevant for heavy packages, which may take a significant time to fully load. For instance, if we only need the function mean
from Statistics
, the following two approaches achieve the same result.
x = [1,2,3]
import Statistics: mean
mean(x) # no prefix needed
x = [1,2,3]
using Statistics: mean
mean(x)
Note that this approach deems it unnecessary to add the package's name as a prefix, even when the package is loaded via import
.
Another handy feature is the possibility of assigning custom names to either packages or functions. This becomes particularly useful when names are lengthy.
x = [1,2,3]
import Statistics as st
st.mean(x)
x = [1,2,3]
import Statistics: mean as average
average(x) # no prefix needed
using Statistics: mean as average
average(x)
Again, notice that the function's name doesn't require any prefix when it's called, even with import
.
Macros are ubiquitous in Julia. They enable the automation of tasks that otherwise would be tedious and time-consuming to perform. On this website, we'll only cover how to apply macros, without exploring how to define them. The reason is that creating macros requires knowledge of Julia's metaprogramming capabilities, which is beyond the scope of this website.
While the utility of macros may not be immediately obvious at this point, this will become clearer once we start applying them in subsequent sections.
Macros and functions share similarities, with both performing operations on inputs and producing outputs. Their key distinction lies in their handling of inputs and output: macros manipulate code syntax (statements or expressions), whereas functions process data values (variables or evaluated expressions).
Formally, macros are denoted by prefixing the symbol @
to their name. They take an entire code expression as their argument and transform it. For example, a macro might take x = some_function(y)
as input, potentially modifying each individual component (x
, =
, or some_function(y)
), inserting new code, or reorganizing the code structure. The final output is a modified version of the original expression, which then is integrated into the program during execution.
A key purpose of macros is to automate code transformations. For example, consider the @.
macro in Julia, which appends a dot .
to every operator and function call in a statement. For now, ignore the impact of adding dots to your code, which will be explained in an upcoming section. Instead, focus on how macros operate at the syntactic level to rewrite entire code blocks.
# both are equivalent
z .= foo.(x .+ y)
@. z = foo(x + y) # @. adds . to '=', 'foo', and '+'