pic
Personal
Website

3b. Function Calls and Packages

PhD in Economics

Introduction

Broadly speaking, functions can be broken down into three:

  1. built-in functions,

  2. third-party functions, and

  3. 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.

Notation for Functions
Julia's developers suggest a snake-case format for function names. This consists of lowercase letters, numbers, and possibly underscores to separate words (e.g., snake_case123). Note that this is only a convention, not a language's requirement.

Packages

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.

Loading Packages and Calling Functions

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)

Built-in Functions

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)\)

Operators as Functions
Most of the symbols employed as operators are also available as functions. This is illustrated below for several arithmetic operators:

Arithmetic Operators as Functions

+(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

Why Using "Import" if It's More Verbose?

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.

Remark
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.

Approaches for Loading Packages and Calling Functions

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

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.

Applying Macros

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.

@. Macro

# both are equivalent
   z .= foo.(x .+ y)
@. z  = foo(x  + y)          # @. adds . to `foo`, `=`, and `+`

Warning!
Using macros requires extreme caution, as they could act as black boxes and hence lead to unexpected behaviors. In fact, macros tend to be a common source of bugs. If you apply a macro, make sure you understand which part of the expression is modified and how.