Category Archives: Julia

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:




The Zen of Missing in Julia

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2022/06/17/missing.html

Introduction

Some time ago I have written a post about
ABC of handling missing values in Julia. Its objective was to give
an introduction to the topic for the newcomers. However, occasionally
users complain that working with missing values in Julia is less convenient
than in e.g. Python or R.

Such opinions are always debatable, so recently I decided to run a small
pool on Julia Discourse about the skipmissing function.
The question was if we want to shorten the skipmissing name into something
that is more convenient to use in interactive work.
To my surprise, a vast majority of voters preferred a verbose and explicit
operation name. This preference regarding handling of missings, to my surprise,
reminded me of several passages from The Zen of Python:

  • Explicit is better than implicit.
  • Readability counts.
  • In the face of ambiguity, refuse the temptation to guess.

So given this preference, how should Julia users retain convenience. Let me
share some of my thoughts on this topic that did not make into my
previous post.

This post was written under Julia 1.7.2, Missings.jl 1.0.2,
MissingsAsFalse.jl 0.1, and StatsBase.jl 0.33.16.

Verbosity

Indeed writing missing, skipmissing, and passmissing (the last one is
defined in Missings.jl), in places where they are needed, might seem verbose.
However, the good news is that most of the time you do not have to type them
fully thanks to completions so in your editor/REPL:

  • instead of missing write mis<tab>;
  • instead of skipmissing write skipm<tab>;
  • instead of passmissing write pas<tab>.

Let us compare. In R:

sum(c(1, NA), na.rm=T)

vs Julia:

sum(skipmissing([1, missing]))

seems longer. However, if you take into account the amount of typing you need to
do (number of keystrokes) it is the same.

Missing values in logical conditions

As I have written in this post I personally strongly recommend using
coalesce to handle missing values in logical conditions. This allows you,
to follow the Explicit is better than implicit. principle by explicitly
showing in the code if missing should be treated as true or as false.

Here is a short example:

julia> c = missing
missing

julia> c ? "true" : "false"
ERROR: TypeError: non-boolean (Missing) used in boolean context

julia> coalesce(c, false) ? "true" : "false"
"false"

However, some users find it more convenient to use @mfalse macro from
the MissingsAsFalse.jl package:

julia> using MissingsAsFalse

julia> @mfalse c ? "true" : "false"
"false"

Correlation matrix with missing values

A common, and relatively complex case of handling missing values, is computing
of correlation matrix of data that contains missings. Let us check what Julia
offers here. We will use the pairwise function from StatsBase.jl.

julia> using Random

julia> using StatsBase

julia> using Statistics

julia> Random.seed!(1234);

julia> x = rand([1:10; missing], 16, 4)
16×4 Matrix{Union{Missing, Int64}}:
  4          missing   2         10
  7         8           missing   8
  3          missing   7          5
 10         9          8         10
  4         2          7          6
  5         6          1           missing
   missing  7          8          9
  9         3          2          6
  6         7         10           missing
  9         3          4          6
  7         2          9          8
  9         7          3          3
  1         8          6          5
  3         9          4          7
  5         7          3          2
  8         5           missing   4

julia> pairwise(cor, eachcol(x))
4×4 Matrix{Union{Missing, Float64}}:
 1.0        missing   missing   missing
  missing  1.0        missing   missing
  missing   missing  1.0        missing
  missing   missing   missing  1.0

julia> pairwise(cor, eachcol(x), skipmissing=:pairwise)
4×4 Matrix{Float64}:
  1.0        -0.218849    -0.0177704   0.0922413
 -0.218849    1.0          0.00973122  0.102969
 -0.0177704   0.00973122   1.0         0.364821
  0.0922413   0.102969     0.364821    1.0

julia> pairwise(cor, eachcol(x), skipmissing=:listwise)
4×4 Matrix{Float64}:
  1.0       -0.229568   -0.100028   0.247283
 -0.229568   1.0        -0.127153  -0.0420058
 -0.100028  -0.127153    1.0        0.67068
  0.247283  -0.0420058   0.67068    1.0

By default pairwise for cor returns missing when at least one of the
columns contains missing values. Use :pairwise value of skipmissing keyword
argument to skip entries with a missing value in either of the two vectors
passed to cor and use :listwise to skip entries with a missing value in
any of the vectors passed to pairwise.

In this example we see that since there are several ways how missing values
should be handled by cor in pairwise computations, instead of using the
skipmissing function, a keyword argument is used allowing to specify what
the data scientist wants exactly.

A similar situation is in the subset and subset! functions from
DataFrames.jl, that also take a skipmissing keyword argument, to simplify
handling of logical conditions specifying which rows from a source data frame
should be kept.

Conclusions

In Julia the approach is that handling of missing values is explicit. This
choice is guided by the fact that then in the code it is explicitly visible what
was the developer’s decision about how they should be treated. This choice makes
code more verbose. Today I have tried to show that, especially with a good
editor/REPL support, in practice it does not introduce a large overhead.

The @view and @views macros: are you sure you know how they work?

By: Blog by Bogumił Kamiński

Re-posted from: https://bkamins.github.io/julialang/2022/06/10/view.html

Introduction

Macros in Julia are a very nice element of the language. You can easily visually
identify macros as they are called using the @ prefix. I most often use the
@assert, @show, @spawn, @edit, and @time macros. However, one needs to
understand how macros work to confidently use them.

Today I want to write about typical situations when using
the @view and @views macros to create views can lead to surprising results.

The post was tested under Julia 1.7.2.

The @view macro and expression range

Assume we have a vector representing 24 months of data and we want to compute
correlation between the first 12 observations and the last 12 observations.

Here is a simple way to do it:

julia> using Random

julia> using Statistics

julia> Random.seed!(1234);

julia> x = randn(24);

julia> cor(x[1:12], x[13:24])
-0.018264675865149734

However, this operation copies data. If we want to avoid this we can use
views. To start let us check the view function:

julia> cor(view(x, 1:12), view(x, 13:24))
-0.018264675865149734

Let us check if indeed this reduces allocations first (using the @allocated
macro):

julia> @allocated cor(x[1:12], x[13:24])
336

julia> @allocated cor(view(x, 1:12), view(x, 13:24))
112

Indeed it does. Of course in our toy example the benefit is minimal.

Now we are ready to get to the main point of my post. Suppose we have
cor(x[1:12], x[13:24]) and want to do use views. As you can see in
codes above turning indexing into view call requires re-writing of the
expressions. This is where the @view macro comes handy.

julia> cor(@view x[1:12], @view x[13:24])
ERROR: LoadError: ArgumentError: Invalid use of @view macro: argument must be a reference expression A[...].

Or does it? We have some problem when using the macro. Let us check the
Julia Manual:

Macros are invoked with the following general syntax:

@name expr1 expr2 ... or @name(expr1, expr2, ...)

Since we invoked @view using the first style Julia eagerly considers
everything that follows it as a single expression. In this case the whole
x[1:12], @view x[13:24] part of code is passed to @view as a single
expression and we get an error.

We need to use the second macro invocation style in this case:

julia> cor(@view(x[1:12]), @view(x[13:24]))
-0.018264675865149734

Now all worked as expected. The only downside is that it feels a bit
inconvenient to write @view(...). Fortunately, Julia’s creators have thought
about it and designed a @views macro which turns every array slicing
(i.e., array[...]) operation in a passed expression to a view. In our case
this would be:

julia> @views cor(x[1:12], x[13:24])
-0.018264675865149734

The @views macro surprises

Let us check using the @macroexpand macro if indeed @views works as I promised:

julia> @macroexpand @views cor(x[1:12], x[13:24])
:(cor((Base.maybeview)(x, 1:12), (Base.maybeview)(x, 13:24)))

What is this strange Base.maybeview function? Is like getindex, but returns
a view for array slicing operations, while remaining equivalent to getindex
for scalar indices and non-array types. So it is almost the same as view.

Let us see the difference between using @view and @views:

julia> @view x[1]
0-dimensional view(::Vector{Float64}, 1) with eltype Float64:
0.9706563288552144

julia> @views x[1]
0.9706563288552144

Most of the time using a 0-dimensional view or a scalar will not make a
difference, however, sometimes it does. Let us have a look:

julia> similar(@view x[1])
0-dimensional Array{Float64, 0}:
1.40721121e-315

julia> similar(@views x[1])
ERROR: MethodError: no method matching similar(::Float64)

So things are, unfortunately, not as simple as you might expect, especially if
you are writing generic code and do not know upfront if you will use a scalar
index or not.

Having learned what I have written above you might think that the following code
will not work:

julia> x, y, z = [1, 2, 3], [2, 3, 4], [4, 5, 6]
([1, 2, 3], [2, 3, 4], [4, 5, 6])

julia> @views x[1] = y[1] + z[1]
6

julia> x
3-element Vector{Int64}:
 6
 2
 3

However, it produces a correct result. What is the reason? Let us check:

julia> @macroexpand @views x[1] = y[1] + z[1]
:(x[1] = (Base.maybeview)(y, 1) + (Base.maybeview)(z, 1))

As we can see @views is smart enough not to apply the view transformation
to the left hand side of the assignment. This feature is not documented in its
docstring, but can be found (and is even commented about) in the source code of
the @views macro (in the _views function to be precise).

Interestingly, this rule is in play even in the example given in
the docsting of the @views macro. Let us check it:

julia> A = zeros(3, 3);

julia> @macroexpand @views for row in 1:3
                    b = A[row, :]
                    b[:] .= row
                end
:(for row = 1:3
      #= REPL[67]:2 =#
      b = (Base.maybeview)(A, row, :)
      #= REPL[67]:3 =#
      b[:] .= row
  end)

We can see that since b[:] is on left hand side of the assignment it is not
touched by @views.

Conclusions

What are the lessons learned?

  1. Macros in Julia are powerful. Well designed macros can make developer’s
    life easier and code more readable.
  2. Macros can be tricky. The most common problem when using macros is the
    @name expr1 style of invocation, which can process “too much” of your code
    unexpectedly.
  3. @views is not equivalent to multiple invocations of @view. There are
    subtle differences between them. Fortunately these differences matter
    mostly in generic code and thus package developers need to be aware of them.