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:
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 apply functions, which in turn takes us to the discussion of 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 for incorporating functions into the workspace. In fact, both built-in and third-party developer 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 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
.
The logic is based on a programming principle known as modularity. This promotes the development of small reusable modules, rather than large intertwined code. Its main advantage is letting packages evolve independently, without bugs and deprecations spreading across the entire Julia ecosystem. The practical implication for users is that they 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 and exporting a set of functions. When you run Julia, you're in fact writing your script in a module called Main
.
Packages are a special types of modules. Their distinctive feature is that they include information about its dependencies, defined as the necessary packages to run the package itself. To get access to a package's functions, you need to load the package by either using the keyword import
or using
. The choice between these two determines how functions are eventually called. If you load a package with import
, you need to prefix the function's name 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
. Note that, although this package isn't loaded by default, 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)
Julia's built-in functions reside in two packages known as Core
and Base
, which are loaded automatically in every Julia session. They're incorporated as if we had executed using Core
and using Base
, so that their functions can be called without any prefix. [note] Some built-in functions may require a prefix. For instance, this occurs with the function isgreater
, which has to be called through Base.isgreater
. Furthermore, there are submodules loaded by default acting as subpackages. For instance, you have the function Base.Iterators.accumulate
, which is part of the submodule Iterators
from Base
. As Base
is loaded by using
, you can directly call this function through Iterators.accumulate
.
Among the built-in mathematical functions, the syntax of the most common is as follows.
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 two or more packages, you necessarily have to load the packages using import
. For instance, suppose we load Statistics
and another package called MyPackage
. Assume also that the latter contains a function called mean
. Then, if we want to use mean
, we need to use import
to load either Statistics
or MyPackage
. Otherwise, Julia will throw an error when mean
is called. [note] Adding a function with the same name as another package isn't necessarily a neglect. For example, mean
in MyPackage
could be computing the average through an alternative algorithm to the one in Statistics
.
Loading packages with import
could also be beneficial in other scenarios. For instance, it may reduce the possibility of ambiguity in the meaning of a function, thereby improving code readability. For instance, consider a script making use of the function rank
. This name could be a reference to various concepts depending on the context (e.g., the rank of a matrix, the order in a list). To shed some light on its particular meaning, you could indicate the package of rank
when the function is called.
import
is also useful if you have customized functions and they're commonly applied across 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 this isn't the case, by including the function in a package called userDefined
. In this way, you can load the package using import UserDefined
, and then calling the function through UserDefined.table_in_pdf
.
The features we've covered are probably all you need to use packages in Julia. However, there are some additional features worth mentioning.
First, it's possible to only load a subset of functions from a package. This feature 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 are equivalent.
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 unnecessary adding the package's name as a prefix, even when we load a package through import
.
You can also assign custom names to packages or functions. This is particularly useful for lengthy names. Below, we demonstrate the implementation with the keyword import
, which is equivalent to the approach for using
.
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)
Notice that, again, the function's name doesn't require any prefix when it's called, even when it's incorporated into the workspace 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. The reason is that defining custom macros requires knowledge of Julia's metaprogramming capabilities, which is beyond the scope of the website.
The usefulness of macros may not be immediately clear at this point. Hopefully, once we explore applications in later sections, many of your questions will be cleared up.
Macros and functions are similar, with both performing operations on an input. Their main difference is that macros take statements as their argument, while functions pass variables. Additionally, macros may return a statement as its output, a capability not shared by functions.
Specifically, a macro can take a whole expression x = some_function(y)
as its argument, possibly modifying x
, =
, and some_function(y)
, or add more lines of code. Eventually, it can return a modified version x = some_function(y)
.
Macros are denoted by prefixing the symbol @
to their name. One of their primary purposes is to transform statements. To illustrate this, let's consider the macro @.
, which appends a dot .
to each operator and function in a statement. This dot notation, also known as "broadcasting," enables operators and functions to apply element-wise. At this point, don't worry about what broadcasting is. Instead, focus on how macros modify an entire statement.
# both are equivalent
z .= foo.(x .+ y)
@. z = foo(x + y) # @. adds . to `foo`, `=`, and `+`