Julia ♥ ABM #1: Starting from scratch

By: Frederik Banning

Re-posted from: https://forem.julialang.org/fbanning/issue-1-starting-from-scratch-1ibo

Let’s start from scratch. From this point onward, I’ll assume you’re already familiar with both agent-based modelling as a method[1] as well as basic familiarity with Julia as a programming language, i.e. you have it installed and understand the fundamentals of its syntax[2]. This series on agent-based modelling in Julia will be structured somewhat analogously to the regular modelling process that every agent-based modeller knows in some form or another: We start from the description of a situation, proceed with formulating a research question, and then identify relevant aspects. Afterwards we will formalise them as agent variables and model parameters and then put them into code.

You might wonder why we don’t immediately go to the coding part and just make some exemplary statements about agent behaviour and model evolution. Surely, that would be easier to just explain things, right? While certainly true at its core, I think there are two important points in favour of the approach chosen for this series:

  1. It’s harder to remember facts than it is to remember stories. Providing yet another bunch of blogposts about the technical details of Julia will likely just lead to readers skipping to the parts that they are currently having problems with. It would be somewhat redundant with well-written documentation. Instead, telling a story over the course of this series will hopefully increase the amount of things you can remember in the long run.[3]
  2. Modelling is always a subjective process and depends heavily on the choices made by the modeller. It is only an opinionated representation of the real world and its interdependencies. What I will present to you is just my interpretation of the situation. If you want to do things differently, you’re fully able to do so.

Model background

A day in the life

Our starting point will be a very simple example of two agents with heterogeneous properties that interact with each other on a regular basis. We will extend on this initial description over the course of the series, introducing new aspects, and making assumptions about how the world that they live in works.

Somehow we need to differentiate between our two agents, so we give them names: Sally and Richard. Their names, however, aren’t their only distinctive features. Humans are very diverse, hence we will proceed with a short description of each of them.

Richard is 33 years old and loves his job as a fisherman. Very early in the morning he happily rows out with his boat for a few hours of line fishing. Some days he’s lucky and catches multiple fish per hour (sometimes even up to three) while on other days it seems as if he’s cursed by Glaucus and the fish just won’t bite. In the end this means that Richard’s daily fishing efforts can take anything between one and five hours depending on his luck. After getting back home, he proceeds with different tasks for another one to three hours. Sometimes he takes care of maintenance work on his boat or fishing rod. When he doesn’t need to repair anything, he reads up on newly published reports regarding the overfishing of the seas and which types of fish can still be caught with good conscience. His fishing work is intense but he also has a decent amount of freely assignable time over the day which is less stressful and energy-sapping. In the evening he likes to knit sweaters and listen to records of his favourite Death Metal band.

Sally is 56 years old and loves her job as a baker. However, she has to deal with it all day, every day. It is a bit tiresome to get up early, prepare the dough, heat the oven, and so on but it allows her to produce a steady output of bread every day with the only limiting factor being the size of her oven which can fit up to 20 loaves at once. She has to do these steps, no matter how much bread she wants to produce, thus, her overall workload only changes slightly each day and her working times regularly end up between eight and ten hours, depending linearily on the amount of bread she bakes. When her work day is over, she likes to go trailrunning in the nearby mountains or to just enjoy having a Piña Colada in front of favourite bar (owned by Willy, a retired Barista from New York) near the oceanside.

As time goes by and they go about their work, their stocks in bread or fish increase and also decrease because they need to eat. In the short run, we may want to assume that one can live on bread and fish alone. But to be quite honest, nobody would like to eat only bread or fish every day. So both Sally and Richard have a deep desire to get some of the goods that only the other person produces.

Simply put, they have a regularly occurring need for both bread and fish that has to be satisfied. If this need is neglected over a longer period of time, their work productivity will decline. To avoid this undesirable situation, they take the opportunity to trade with each other every three days whenever they meet at the ocean side for a nightcap. When they do so, Richard trades with Sally for a few loaves of bread and she gets a few fish from Richard in return. As a starting point, let us assume that one fish is worth the same to them as one loaf of bread, i.e. they are willing to trade their goods in a 1:1 ratio, probably just because they know and like each other.

Question? Which question?

Although it is nice to just start coding and see whatever comes out of our endeavours, an agent-based model should aim to answer a specific question. We will now formulate one for the purpose of this exercise:

What is the minimum amount of work that Sally and Richard can do every day while still being able to satisfy their craving for fish and bread through mutual trade?

Given this question, we can now try to identify the aspects from the description above that are relevant to our planned ABM. Maybe first give this a try yourself before reading on. Which pieces of the provided information are interesting for us? How many agents will our model be comprised of? What are important environmental factors that have to be reflected in the model? Which information is incomplete and requires us to make assumptions?

One interpretation of the model’s background described above could look as follows:

  • Our model will contain two agents, Sally and Richard. What they do in their free time is unimportant for our base model for now and only their jobs and their working times are relevant.
  • How many hours they work per day determines the amount of food they need to eat each day.
    • Sally works 8-10 hours each day (medium exertion) and can freely decide to bake 1-20 loaves of bread each day.
    • Richard goes fishing for 1-5 hours (high exertion) and does other work for 1-3 hours (low exertion). During his fishing hours, Richard has an independent chance to catch 0-3 fish each hour.
  • Both agents have variables tracking their stock of fish and bread as well as their hunger for each good.
  • If their aggregate hunger level stays above a certain threshold for a prolonged period of time, agents reduce their work efforts, thus producing less bread and spending less time on fishing.
  • There is an opportunity at the end of every third day to trade their current stocks in fish and bread with each other.
  • Most of what are about processes and do not need to be stored as agent variables. We can therefore identify five distinct agent variables to keep track of: job, pantry_fish, pantry_bread, hunger_fish, hunger_bread.

Wait, wasn’t this supposed to be a Julia tutorial?

Phew. Those were a lot of words about plenty of things and so far not a single thought was wasted on actual code. To avoid losing some precious readers, this is probably as good a time as any to finally start talking about Julia.

A few preparations

Feel free to create a new folder for this series to code along to the examples (call it whatever you want, e.g. “julia-loves-abm”). Open your terminal, navigate to this folder and start Julia from there by running julia. Now execute the following:

julia> using Pkg

julia> Pkg.activate(".")
  Activating new project at `~/Code/julia-loves-abm`

Congratulations, you’ve just mastered the highly sought after skill of creating a fresh Julia project environment with the same name as the folder you’ve started the Julia instance from. You should activate this environment every time you continue working on this project as it will allow Julia to know which packages you have installed and which dependencies or versions should be respected.

As is so often the case in programming, there’s also another way to do the same thing. If you just type ] in the julia> prompt you will enter the built-in pkg> mode:

(@v1.7) pkg> 

This is very easy and approachable as you do not need to run using Pkg before doing this. The pkg> mode is just always available to you. It allows you to quickly use some commands like activate . (to change environment where dot refers to the current working directory) and status (to check installed packages and their versions):

(@v1.7) pkg> activate .
  Activating project at `~/Code/julia-loves-abm`

(julia-loves-abm) pkg> status
      Status `~/Code/julia-loves-abm/Project.toml` (empty project)

As you can see, we’ve switched from the base environment @v1.7 to a newly created one which is automatically assigned the name of our current working directory julia-loves-abm. The status command tells us that the project environment is currently empty, meaning that we haven’t added any extra functionality through Julia packages. For now, you can safely ignore most of the details about environments but just keep in mind that they exist as they will be of great use to us at a later stage.

Creating agents

Let’s remember our story from above. We have two people, so it seems straightforward to initialise two variables called sally and richard that represent them.[4] A little bit earlier we’ve looked at the relevant aspects of their everyday lives, telling us what to formalise as agent variables. There are a few unifying features about them, for example they each have a job which allows them to produce a certain kind of food (fish or bread respectively). Since it’s important for what happens in the model, it needs to be reflected in the code which we could simply do by using a String (a sequence of characters like letters and whitespace) describing their job:

julia> sally = "Baker"
"Baker"

julia> richard = "Fisher"
"Fisher"

Now that’s already an (admittedly pretty crude) representation of what our two agents are. Whenever we call one of the variables, its evaluation will tell us the agent’s job. But we also want to keep track of their stock of food and their hunger levels. This confronts us with a decision about a more appropriate data structure to use for storing all the different kinds of information about Richard and Sally.

Lined up

Maybe the simplest approach would be to use a simple collection like an Array or a Tuple with the values of the agent variables in it. Let’s assume that Richard and Sally are both not hungry in the beginning of our simulation and that they each have a starting stock of 10 fish as well as 10 loaves of bread.

julia> sally = ("Baker", 10, 10, 0, 0)
("Baker", 10, 10, 0, 0)

julia> richard = ("Fisher", 10, 10, 0, 0)
("Fisher", 10, 10, 0, 0)

It becomes immediately obvious that this approach is not very practical. While we can reason about what "Fisher" and "Baker" stands for, it is pretty hard to know what exactly the numbers mean without having the verbal description from above at hand.

Give me names

Indeed, it would be nice if we could label all the entries so that it is clear what they mean. We might want to opt for a NamedTuple instead which allows to provide names to the fields.

julia> sally = (job = "Baker", pantry_fish = 10, pantry_bread = 10, hunger_fish = 0, hunger_bread = 0)
(job = "Baker", pantry_fish = 10, pantry_bread = 10, hunger_fish = 0, hunger_bread = 0)

julia> richard = (job = "Fisher", pantry_fish = 10, pantry_bread = 10, hunger_fish = 0, hunger_bread = 0)
(job = "Fisher", pantry_fish = 10, pantry_bread = 10, hunger_fish = 0, hunger_bread = 0)

Ah, much better. Now we don’t have to remember the order of the data in the Tuple but can easily access the agent data by fieldnames:

julia> sally.job
"Baker"

julia> richard.pantry_bread
10

Again, sally.job is a String like in our initial approach of just assigning a string literal to each agent which described their job. However, richard.pantry_bread is also easily accessible now and shows us that he currently owns 10 loaves of bread.

To see all the fieldnames and the types of their values, we again call the typeof function on one of the agents:

julia> typeof(richard)
NamedTuple{(:job, :pantry_fish, :pantry_bread, :hunger_fish, :hunger_bread), Tuple{String, Int64, Int64, Int64, Int64}}

This tells us that richard is now depicted as a NamedTuple comprised of the fields (:job, :pantry_fish, :pantry_bread, :hunger_fish, :hunger_bread) and their values Tuple{String, Int64, Int64, Int64, Int64}. It is fundamentally the same as in the Tuple case before but enhanced with the information about which value means what.

Change is inevitable

But as we know, data in ABMs regularly change over the course of the simulation. And since a NamedTuple is immutable[5] by nature, this seems to be a really bad choice as a data structure for our agents. If we attempt to change one of the values, we get an error:

julia> sally.pantry_bread = 9
ERROR: setfield!: immutable struct of type NamedTuple cannot be changed
Stacktrace:
 [1] setproperty!(x::NamedTuple{(:job, :pantry_fish, :pantry_bread, :hunger_fish, :hunger_bread), Tuple{String, Int64, Int64, Int64, Int64}}, f::Symbol, v::Int64)
   @ Base ./Base.jl:43
 [2] top-level scope
   @ REPL[37]:1

So to change the value of any of the fields, we would need to reuse some of the old values and insert the new value in the appropriate field.

julia> sally = (sally..., pantry_bread = 9)
(job = "Baker", pantry_fish = 10, pantry_bread = 9, hunger_fish = 0, hunger_bread = 0)

While this approach does have its advantages (e.g. no accidental changes in agent-related data), it can quickly get a bit cumbersome to always overwrite the old variable with a new one. So it’s probably better to completely ditch the idea of using NamedTuples then.

The next best idea that might come to mind would be to use a Dict instead:

julia> sally = Dict(
           :job => "Baker", 
           :pantry_fish => 10, 
           :pantry_bread => 10, 
           :hunger_fish => 0, 
           :hunger_bread => 0
       )
Dict{Symbol, Any} with 5 entries:
  :pantry_fish   => 10
  :hunger_bread => 0
  :job          => "Baker"
  :hunger_fish  => 0
  :pantry_bread  => 10

julia> richard = Dict(
           :job => "Fisher", 
           :pantry_fish => 10, 
           :pantry_bread => 10, 
           :hunger_fish => 0, 
           :hunger_bread => 0
       )
Dict{Symbol, Any} with 5 entries:
  :pantry_fish   => 10
  :hunger_bread => 0
  :job          => "Fisher"
  :hunger_fish  => 0
  :pantry_bread  => 10

The keys and values of dictionaries can be comprised of just about any type that you can think of. Hence you have to take care to use Symbols as keys of the dictionary (or maybe Strings, if you prefer that), because unlike NamedTuples, Dicts don’t just automatically convert the fieldnames into Symbols (which always start with a colon :).

To retrieve data from our agents, we use the regular syntax for dictionaries:

julia> sally[:job]
"Baker"

julia> richard[:pantry_bread]
10

Changing the value of an agent variable is now as easy as writing:

julia> sally[:pantry_bread] -= 1
9

julia> sally
Dict{Symbol, Any} with 5 entries:
  :pantry_fish   => 10
  :hunger_bread => 0
  :job          => "Baker"
  :hunger_fish  => 0
  :pantry_bread  => 9

To see the current keys of a dictionary, you can just start typing the name of the dictionary followed by [: and then press Tab twice[6]:

julia> sally[:
:hunger_bread :hunger_fish   :job           :pantry_bread   :pantry_fish

Again, there are often multiple ways to do the same thing when coding and none of them is more or less correct than the other. Another way to retrieve the current set of keys of a dictionary is to call the keys function on it:

julia> keys(sally)
KeySet for a Dict{Symbol, Any} with 5 entries. Keys:
  :pantry_fish
  :hunger_bread
  :job
  :hunger_fish
  :pantry_bread

While Tab completion is a nice way to interactively explore the current state of your agents, having a KeySet also allows you to go through the agent variables one after another in a programmatic way. Which of these approaches you will choose heavily depends on the current use case you are facing. Generally though, it’s just good to have some options available.

Work smart, not hard

Luckily we currently only have two agents in our model, so it’s not really that problematic to create them one by one. In bigger models, however, we often want to create a relatively high number of agents and that could then quickly get a bit tedious to do one by one. Here’s a general word of advice about coding:

If you have to write something repeatedly, there’s probably a better way to do it. 🙂

Indeed, we can build a custom type for our agents, allowing us to predefine the structure of the data that we want to store. So instead of having to spell out the agent variables each and every time we want to create a new agent, we can just tell Julia to use our custom struct to lay out and label the data that we provide to it. The keyword to create such a data structure (also referred to as a composite type) is struct but we also have to prepend it with mutable to make sure that we are able to change its values (see the problem about immutability described above).

julia> mutable struct Agent
           job::String
           pantry_fish::Int
           pantry_bread::Int
           hunger_fish::Int
           hunger_bread::Int
       end

As you can see, we also had to explicitly define the types for each field of the struct as these are not automatically inferred like when creating a NamedTuple or a Dict. Without going into detail about the variety of types, we just use what we already know. From our previous attempt to create our agents as NamedTuples, we could see that the values we provided to it have been interpreted as String and Int64 types. In the definition of the struct above, we’ve simply used this knowledge and also changed Int64 for the more generalised form Int.[7]

We can now initialise Richard and Sally as two variables of our custom-made and highly specific Agent type:

julia> richard = Agent("Fisher",  10, 10, 0, 0)
Agent("Fisher", 10, 10, 0, 0)

julia> sally   = Agent( "Baker",  10, 10, 0, 0)
Agent("Baker", 10, 10, 0, 0)

This newly created type also allows us to directly access the agent variables. We can do this just like we did it in the case of a NamedTuple:

julia> richard.pantry_fish
10

julia> sally.pantry_bread
10

Changing the values is also possible and just as easy as it was in the case of using a Dict:

julia> richard.pantry_fish -= 1
9

julia> richard
Agent("Fisher", 9, 10, 0, 0)

One of the major downsides of defining our own composite type is that we have to restart our Julia session to change anything about it. Say we would like to add a new field to our agent struct that describes in one convenient number how satisfied they currently are with their life:

julia> mutable struct Agent
           job::String
           pantry_fish::Int
           pantry_bread::Int
           hunger_fish::Int
           hunger_bread::Int
           life_satisfaction::Int
       end
ERROR: invalid redefinition of constant Agent
Stacktrace:
 [1] top-level scope
   @ REPL[2]:1

This might seem inconvenient at first glance but in reality it doesn’t happen all too often. Indeed, we have been smart modellers and took enough time to first deliberate about what we actually want to model and which agent variables are important for answering the underlying question of our model.

Great! Now that we’ve settled on a convenient way how to represent our agents in code, our next step will be dealing with the tasks that our agents do every day and how they affect the filling of their pantries and their hunger levels.

[1]: Explaining agent-based modelling in detail is way out of the scope of this series. If you are unfamiliar with the method but generally interested in learning more about it, you may want to start reading this text book or this introductory article or watch this video series by Complexity Explorer.

[2]: Should you find yourself wondering how to install Julia, it’s really as simple as downloading it from the Julia language website and running the installer. There are also other ways to setup your Julia installation but the aforementioned method works just as fine as anything else. Just to provide an example, my preferred way is Juliaup as a platform-independent tool to manage multiple Julia versions and keep them up to date. Once you’re all set up, you might want to check out the Getting Started section of the official Julia documentation or follow an introductory video course to learn about syntax and basic usage. Done that? Let’s get back to ABM stuff then. 🙂

[3]: I sincerely believe that teaching by telling a simple story is very approachable for most people. Of course there’s room for everything on the world wide web and no approach to learning is inherently better or worse than any other. It very much depends on your preferred style of learning, your previous knowledge, and maybe even your current state of mood. For example, if you already know how to work with Julia and are already very knowledgeable in the arcane arts of agent-based modelling, you might as well just skip this introductory series and go straight to the documentation of Agents.jl and read/work through the well-written tutorial and examples.

[4]: It’s the Julian way to use lower case names with underscores for our variables. This is often referred to as snake_case. There are some more style recommendations that established themselves over time in the Julia community which we will try to adhere to as closely as possible. If you want to read up on the idiomatic Julia coding style, have a look at the official Julia style guide. In the end, however, it doesn’t matter too much as long as you don’t have to share your code with others. In the latter case it is highly recommended to try and stick to a unified coding style as it allows others (not only colleagues but maybe even strangers at some point) to more easily read and understand your code and potentially comment on it, extend it, fix it, et cetera.

[5]: Immutability means that a variable cannot be changed after its creation. This applies to both its value(s) and its composition. Here’s the exemplary case of trying to modify one of the elements in a Tuple and attempting to add a new element to it:

julia> t = (1,2,3)
(1, 2, 3)

julia> t[1] = 1
ERROR: MethodError: no method matching setindex!(::Tuple{Int64, Int64, Int64}, ::Int64, ::Int64)
Stacktrace:
[1] top-level scope
@ REPL[74]:1

julia> t[end+1] = t[end] + 1
ERROR: MethodError: no method matching setindex!(::Tuple{Int64, Int64, Int64}, ::Int64, ::Int64)
Stacktrace:
[1] top-level scope
@ REPL[73]:1

However, this does not mean that we cannot redefine the variable t to refer to something else:

julia> t = 1
1

[6]: This Tab completion also works with a lot of other things, for example NamedTuples. Just write its variable name followed by a single dot:

julia> nt = (a = 1, b = 2)
(a = 1, b = 2)

julia> nt.
a b

[7]: Although definitely not necessary at this stage, you might be interested in what all these types mean. Let’s dive a bit deeper. You might wonder why it is called Int64 and not just Number, Real or Integer. Simply put, every Integer is a Real but not every Real is an Integer. Julia provides us with an easy way to find out about this type hierarchy:

julia> supertypes(Integer)
(Integer, Real, Number, Any)

To go up through the hierarchy, you can read this tuple of types from left to right. If you want to explore it in the opposite direction, there’s also a way to do this:

julia> subtypes(Real)
4-element Vector{Any}:
AbstractFloat
AbstractIrrational
Integer
Rational

As you can see, Integer is necessarily a subtype of Real. We can also programmatically test this with a specific syntax in Julia by writing:

julia> Integer <: Real
true

Now an Int64 is a specific subtype of a signed Integer number with a size of 64 bit.

julia> subtypes(Integer)
3-element Vector{Any}:
Bool
Signed
Unsigned

Being Signed means that the Integer uses a bit of memory to store its mathematical sign. This means the resulting number can take both positive and negative values.

julia> subtypes(Signed)
6-element Vector{Any}:
BigInt
Int128
Int16
Int32
Int64
Int8

There’s no general type called Int in here but only types with predetermined size, e.g. 8 or 64 bit. If we use Int as a type for our Agent struct, Julia asserts that we want the possible size of the Int to be as big as possible. Thus, it is automatically determined by the underlying architecture of our computer (most modern computers are built on 64 bit). When we create an instance of our Agent struct, those fields typed as Int will indeed be of type Int64. This is nice to take into account for the hypothetical case of somebody with a 32 bit computer trying to run our model which is then possible precisely because we didn’t restrict the size of the Int too much (e.g. to always use Int64).

And while all of this is actually very interesting, we luckily don’t have to worry about it in greater detail for now. If you still want to read more about Julia’s type system, have a look at the well-written section of the official docs. Let’s get back to work on our ABM. 🙂

Breaking: Julia ranks in the top 5 most loved programming languages for 2022

By: Logan Kilpatrick

Re-posted from: https://blog.devgenius.io/breaking-julia-ranks-in-the-top-5-most-loved-programming-languages-for-2022-6cb7740240e1?source=rss-2c8aac9051d3------2

It should come as no surprise to those following the growth and expansion of the Julia Programming Language ecosystem that in this year’s…

My first macro in Julia

By: Mosè Giordano

Re-posted from: http://giordano.github.io/blog/2022-06-18-first-macro/

This post isn’t about the first macro I wrote in the Julia programming
language
, but it can be about your first macro.

Frequently Asked Questions

Question: What are macros in Julia?

Answer: Macros are sort of functions which take as input unevaluated expressions
(Expr) and return as output
another expression, whose code is then regularly evaluated at runtime. This post isn’t a
substitute for reading the section about macros in the Julia
documentation
, it’s
more complementary to it, a light tutorial for newcomers, but I warmly suggest reading the
manual to learn more about them.

Calls to macros are different from calls to regular functions because you need to prepend
the name of the macro with the at-sign @ sign: @view A[4, 1:5] is a call to the
@view macro, view(A, 4, 1:5)
is a call to the view function.
There are a couple of important reasons why macro calls are visually distinct from function
calls (which sometimes upsets Lisp purists):

  • the arguments of function calls are always evaluated before entering the body of the
    function: in f(g(2, 5.6)), we know that the function g(2, 5.6) will always be
    evaluated before calling f on its result. Instead, the expression which is given as
    input to a macro can in principle be discarded and never taken into account and the macro
    can be something completely different: in @f g(2, 5.6), the expression g(2, 5.6) is
    taken unevaluated and rewritten into something else. The function g may actually never
    be called, or it may be called with different arguments, or whatever the macro @f has
    been designed to rewrite the given expression;
  • nested macro calls are evaluated left-to-right, while nested function calls are evaluated
    right-to-left: in the expression f(g()), the function g() is first called and then fed
    into f, instead in @f @g x the macro @f will first rewrite the unevaluated
    expression @g x, and then, if it is still there after the expression-rewrite operated by
    @f (remember the previous point?), the macro @g is expanded.

Writing a macro can also be an unpleasant experience the first times you try it, because a
macro operates on a new level (expressions, which you want to turn into other expressions)
and you need to get familiar with new concepts, like
hygiene (more on this below), which can be
tricky to get initially right. However it may help remembering that the Expr macros
operate on are regular Julia objects, which you can access and modify like any other Julia
structure. So you can think of a macro as a regular function which operates on Expr
objects, to return a new Expr. This isn’t a simplification: many macros are actually
defined to only call a regular function which does all the expression rewriting business.

Q: Should I write a macro?

A: If you’re asking yourself this question, the answer is likely “no”. Steven G. Johnson
gave an interesting keynote speech at JuliaCon
2019
about metaprogramming (not just in
Julia), explaining when to use it, and more importantly when not to use it.

Also, macros don’t compose very well: remember that any macro can rewrite an expression in a
completely arbitrary way, so nesting macros can sometimes have unexpected results, if the
outermost macro doesn’t anticipate the possibility the expressions it operates on may
contain another macro which expects a specific expression. In practice, this is less of a
problem than it may sound, but it can definitely happens if you overuse many complicated
macros. This is one more reason why you should not write a macro in your code unless it’s
really necessary to substantially simplify the code.

Q: So, what’s the deal with macros?

A: Macros are useful to programmatically generate code which would be tedious, or very
complicated, to type manually. Keep in mind that the goal of a macro is to eventually get a
new expression, which is run when the macro is called, so the generated code will be
executed regularly, no shortcut about that, which is sometimes a misconception. There is
very little that plain (i.e. not using macros) Julia code cannot do that macros can.

Q: Why I should keep reading this post?

A: Writing a simple macro can still be a useful exercise to learn how it works, and
understand when they can be useful.

Our first macro: @stable

Globals in Julia are usually a major performance
hit
, when their type is not
constant, because the compiler have no idea what’s their actual type. When you don’t need
to reassign a global variable, you can mark it as constant with the
const keyword, which greatly improves
performance of accessing a global variable, because the compiler will know its type and can
reason about it.

Julia v1.8 introduces a new way to have non-horribly-slow global variables: you can annotate
the type of a global variable, to say that its type won’t change:

julia> x::Float64 = 3.14
3.14

julia> x = -4.2
-4.2

julia> x = 10
10

julia> x
10.0

julia> x = nothing
ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type Float64
Closest candidates are:
  convert(::Type{T}, ::T) where T<:Number at number.jl:6
  convert(::Type{T}, ::Number) where T<:Number at number.jl:7
  convert(::Type{T}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:273
  ...

This is different from a const variable, because you can reassign a type-annotated global
variable to another value with the same type (or
convertible to the annotated
type), while reassigning a const variable isn’t possible (nor recommended). But the type
would still be constant, which helps the compiler optimising code which accesses this global
variable. Note that x = 10 returns 10 (an Int) but the actual value of x is 10.0
(a Float64) because assignment returns the right-hand side but the value 10 is converted to Float64 before
assigning it to x.

Wait, there is no macro so far! Right, we’ll get there soon. The problem with
type-annotated globals is that in the expression x::Float64 = 3.14 it’s easy to predict
the type we want to attach to x, but if you want to make x = f() a type-annotated global
variable and the type of f() is much more involved than Float64, perhaps a type with
few parameters
, then doing
the type annotation can be annoying. Mind, not impossible, just tedious. So, that’s where
a macro could come in handy!

The idea is to have a macro, which we’ll call @stable, which operates like this:

@stable x = 2

will automatically run something like

x::typeof(2) = 2

so that we can automatically infer the type of x from the expression on the right-hand
side, without having to type it ourselves. A useful tool when dealing with macros is
Meta.@dump (another
macro, yay!).

julia> Meta.@dump x = 2
Expr
  head: Symbol =
  args: Array{Any}((2,))
    1: Symbol x
    2: Int64 2

This tells us how the expression x = 2 is parsed into an Expr, when it’s fed into a
macro. So, this means that our @stable x = 2 macro will see an Expr whose ex.args[1]
field is the name of the variable we want to create and ex.args[2] is the value we want to
assign to it, which means the expression we want to generate will be something like
ex.args[1]::typeof(ex.args[2]) = ex.args[2], but remember that you need to
interpolate
variables inside a quoted
expression
:

julia> macro stable(ex::Expr)
           return :( $(ex.args[1])::typeof($(ex.args[2])) = $(ex.args[2]) )
       end
@stable (macro with 1 method)

julia> @stable x = 2
2

Well, that was easy, it worked at the first try! Now we can use our brand-new type-stable
x! Let’s do it!

julia> x
ERROR: UndefVarError: x not defined

Waaat! What happened to our x, we just defined it above! Didn’t we? Well, let’s use
yet another macro,
@macroexpand, to see
what’s going on:

julia> @macroexpand @stable x = 2
:(var"#2#x"::Main.typeof(2) = 2)

Uhm, that looks weird, we were expecting the expression to be x::typeof(2), what’s that
var"#2#x"?
Let’s see:

julia> var"#2#x"
ERROR: UndefVarError: #2#x not defined

Another undefined variable, I’m more and more confused. What if that 2 in there is a
global counter? Maybe we need to try with 1:

julia> var"#1#x"
2

julia> var"#1#x" = 5
5

julia> var"#1#x"
5

julia> var"#1#x" = nothing
ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type Int64
Closest candidates are:
  convert(::Type{T}, ::T) where T<:Number at number.jl:6
  convert(::Type{T}, ::Number) where T<:Number at number.jl:7
  convert(::Type{T}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:273
  ...

Hey, here is our variable, and it’s working as we expected! But this isn’t as convenient
as calling the variable x as we wanted. I don’t like that. what’s happening?
Alright,
we’re now running into hygiene, which we mentioned above: this isn’t about washing your
hands, but about the fact macros need to make sure the variables in the returned expression
don’t accidentally clash with variables in the scope they expand to. This is achieved by
using the gensym function to
automatically generate unique identifiers (in the current module) to avoid clashes with
local variables.

What happened above is that our macro generated a variable with a gensym-ed name, instead
of the name we used in the expression, because macros in Julia are hygienic by default. To
opt out of this mechanism, we can use the
esc function. A rule of thumb is
that you should apply esc on input arguments if they contain variables or identifiers from
the scope of the calling site that you need use as they are, but for more details do read
the section about hygiene in the Julia
manual
. Note also that
the pattern var"#N#x", with increasing N at every macro call, in the gensym-ed
variable name is an implementation detail which may change in future versions of Julia,
don’t rely on it.

Now we should know how to fix the @stable macro:

julia> macro stable(ex::Expr)
           return :( $(esc(ex.args[1]))::typeof($(esc(ex.args[2]))) = $(esc(ex.args[2])) )
       end
@stable (macro with 1 method)

julia> @stable x = 2
2

julia> x
2

julia> x = 4.0
4.0

julia> x
4

julia> x = "hello world"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Int64
Closest candidates are:
  convert(::Type{T}, ::T) where T<:Number at number.jl:6
  convert(::Type{T}, ::Number) where T<:Number at number.jl:7
  convert(::Type{T}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:273
  ...

julia> @macroexpand @stable x = 2
:(x::Main.typeof(2) = 2)

Cool, this is all working as expected! Are we done now? Yes, we’re heading into the
right direction, but no, we aren’t quite done yet. Let’s consider a more sophisticated
example, where the right-hand side is a function call and not a simple literal number, which
is why we started all of this. For example, let’s define a new type-stable variable with a
rand() value, and let’s print
it with one more macro, @show,
just to be sure:

julia> @stable y = @show(rand())
rand() = 0.19171602949009747
rand() = 0.5007039099074341
0.5007039099074341

julia> y
0.5007039099074341

Ugh, that doesn’t look good. We’re calling rand() twice and getting two different
values?
Let’s ask again our friend @macroexpand what’s going on (no need to use @show
this time):

julia> @macroexpand @stable y = rand()
:(y::Main.typeof(rand()) = rand())

Oh, I think I see it now: the way we defined the macro, the same expression, rand(), is
used twice: once inside typeof, and then on the right-hand side of the assignment, but
this means we’re actually calling that function twice, even though the expression is the
same.
Correct! And this isn’t good for at least two reasons:

  • the expression on the right-hand side of the assignment can be expensive to run, and
    calling it twice wouldn’t be a good outcome: we wanted to create a macro to simply things,
    not to spend twice as much time;
  • the expression on the right-hand side of the assignment can have side effects, which is
    precisely the case of the rand() function: every time you call rand() you’re advancing
    the mutable state of the random number generator, but if you call it twice instead of
    once, you’re doing something unexpected. By simply looking at the code @stable y =
    rand()
    , someone would expect that rand() is called exactly once, it’d be bad if users
    of your macro would experience undesired side effects, which can make for hard-to-debug
    issues.

In order to avoid double evaluation of the expression, we can assign it to another temporary
variable, and then use its value in the assignment expression:

julia> macro stable(ex::Expr)
           quote
               tmp = $(esc(ex.args[2]))
               $(esc(ex.args[1]))::typeof(tmp) = tmp
           end
       end
@stable (macro with 1 method)

julia> @stable y = @show(rand())
rand() = 0.5954734423582769
0.5954734423582769

This time rand() was called only once! That’s good, isn’t it? It is indeed, but I think
we can still improve the macro a little bit. For example, let’s look at the list of all
names defined in the current module with
names. Can you spot anything
strange?

julia> names(@__MODULE__; all=true)
13-element Vector{Symbol}:
 Symbol("##meta#58")
 Symbol("#1#2")
 Symbol("#1#x")
 Symbol("#5#tmp")
 Symbol("#@stable")
 Symbol("@stable")
 :Base
 :Core
 :InteractiveUtils
 :Main
 :ans
 :x
 :y

I have a baad feeling about that Symbol("#5#tmp"). Are we leaking the temporary variable
in the returned expression?
Correct! Admittedly, this isn’t a too big of a deal, the
variable is gensym-ed and so it won’t clash with any other local variables thanks to
hygiene, many people would just ignore this minor issue, but I believe it’d still be nice to
avoid leaking it in the first place, if possible. We can do that by sticking the
local keyword in front of the
temporary variable:

julia> macro stable(ex::Expr)
           quote
               local tmp = $(esc(ex.args[2]))
               $(esc(ex.args[1]))::typeof(tmp) = tmp
           end
       end
@stable (macro with 1 method)

julia> @stable y = rand()
0.7029553059625194

julia> @stable y = rand()
0.04552255224129409

julia> names(@__MODULE__; all=true)
13-element Vector{Symbol}:
 Symbol("##meta#58")
 Symbol("#1#2")
 Symbol("#1#x")
 Symbol("#5#tmp")
 Symbol("#@stable")
 Symbol("@stable")
 :Base
 :Core
 :InteractiveUtils
 :Main
 :ans
 :x
 :y

Yay, no other leaked temporary variables! Are we done now? Not yet, we can still make it
more robust. At the moment we’re assuming that the expression fed into our @stable macro
is an assignment, but what if it isn’t the case?

julia> @stable x * 12
4

julia> x
4

Uhm, it doesn’t look like anything happened, x is still 4, maybe we can ignore also
this case and move on.
Not so fast:

julia> 1 * 2
ERROR: MethodError: objects of type Int64 are not callable
Maybe you forgot to use an operator such as *, ^, %, / etc. ?

Aaargh! What does that even mean?!? Let’s ask our dear friends Meta.@dump and
@macroexpand:

julia> Meta.@dump x * 2
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol *
    2: Symbol x
    3: Int64 2

julia> @macroexpand @stable x * 12
quote
    var"#5#tmp" = x
    (*)::Main.typeof(var"#5#tmp") = var"#5#tmp"
end

julia> *
4

Let me see if I follow: with @stable x * 12 we’re assigning x (which is now
ex.args[2]) to the temporary variable, and then the assignment is basically * = 4,
because ex.args[1] is now *. Ooops.
Brilliant! In particular we’re shadowing * in
the current scope (for example the Main module in the REPL, if you’re following along in
the REPL) with the number 4, the expression 1 * 2 is actually equivalent to *(1, 2),
and since * is 4

julia> 4(1, 2)
ERROR: MethodError: objects of type Int64 are not callable
Maybe you forgot to use an operator such as *, ^, %, / etc. ?

Gotcha! So we should validate the input? Indeed, we should make sure the expression
passed to the macro is what we expect, that is an assignment. We’ve already seen before
that this means ex.head should be the symbol =. We should also make sure the left-hand
side is only a variable name, we don’t want to mess up with indexing expressions like A[1]
= 2
:

julia> Meta.@dump A[1] = 2
Expr
  head: Symbol =
  args: Array{Any}((2,))
    1: Expr
      head: Symbol ref
      args: Array{Any}((2,))
        1: Symbol A
        2: Int64 1
    2: Int64 2

Right, so ex.head should be only = and ex.args[1] should only be another symbol. In
the other cases we should throw a useful error message.
You’re getting the hang of it!

julia> macro stable(ex::Expr)
           (ex.head === :(=) && ex.args[1] isa Symbol) || throw(ArgumentError("@stable: `$(ex)` is not an assigment expression."))
           quote
               tmp = $(esc(ex.args[2]))
               $(esc(ex.args[1]))::typeof(tmp) = tmp
           end
       end
@stable (macro with 1 method)

julia> @stable x * 12
ERROR: LoadError: ArgumentError: @stable: `x * 12` is not an assigment expression.

julia> @stable A[1] = 2
ERROR: LoadError: ArgumentError: @stable: `A[1] = 2` is not an assigment expression.

Awesome, I think I’m now happy with my first macro! Love it! Yes, now it works pretty
well and it has also good handling of errors! Nice job!

Conclusions

I hope this post was instructive to learn how to write a very basic macro in Julia. In the
end, the macro we wrote is quite short and not very complicated, but we ran into many
pitfalls along the way: hygiene, thinking about corner cases of expressions, avoiding
repeated undesired evaluations and introducing extra variables in the scope of the macro’s
call-site. This also shows the purpose of macros: rewriting expressions into other ones, to
simplify writing more complicated expressions or programmatically write more code.

This post is inspired by a macro which I wrote some months ago for the Seven Lines of
Julia

thread on JuliaLang Discourse.

This was cool! Where can I learn more about macros? Good to hear! I hope now you aren’t
going to abuse macros though! But if you do want to learn something more about macros, in
addition to the official documentation, some useful resources are: