|
1 | 1 | # [Macros](@id macro_lecture) |
2 | 2 | What is macro? |
3 | 3 | In its essence, macro is a function, which |
4 | | -1. takes as an input an expression (or more expression) |
| 4 | +1. takes as an input an expression (parsed input) |
5 | 5 | 2. modify the expressions in argument |
6 | | -3. evaluate the modified expression exactly once during the compile time. The expression returned from the macro is inserted at the position of the macro and evaluated. After the macro is applied there is no trace of macro being called (from the syntactic point of view). |
| 6 | +3. insert the modified expression at the same place as the one that is parsed. |
7 | 7 |
|
8 | | -Thus, the macro can be thus viewed as a convenience for performing |
9 | | -``` |
10 | | -ex = Meta.parse(some_source_code) |
11 | | -eval(replace_sin(ex)) |
12 | | -``` |
| 8 | +Macros are necessary because they execute when code is parsed, therefore, macros allow the programmer to generate and include fragments of customized code before the full program is run. To illustrate the difference, consider the following example: |
13 | 9 |
|
14 | | -We can instantiate the aboce with the following example |
| 10 | +One of the very conveninet ways to write macros is to write functions modifying the `Expr`ession and then call that function in macro as |
15 | 11 | ```julia |
16 | 12 | replace_sin(x::Symbol) = x == :sin ? :cos : x |
17 | 13 | replace_sin(e::Expr) = Expr(e.head, map(replace_sin, e.args)...) |
18 | 14 | replace_sin(u) = u |
19 | | -some_source_code = "sinp1(x) = 1 + sin(x)" |
20 | 15 |
|
21 | | -ex = Meta.parse(some_source_code) |
22 | | -eval(replace_sin(ex)) |
23 | | -sinp1(1) == 1 + cos(1) |
24 | | -``` |
25 | | -which can be equally written by defining macro using keyword `macro` |
26 | | -``` |
27 | 16 | macro replace_sin(ex) |
28 | | - o = replace_sin(ex) |
| 17 | + replace_sin(esc(ex)) |
29 | 18 | end |
30 | | -@replace_sin(sinp1(x) = 1 + sin(x)) |
31 | | -sinp1(1) == 1 + cos(1) |
| 19 | + |
| 20 | +@replace_sin(cosp1(x) = 1 + sin(x)) |
| 21 | +cosp1(1) == 1 + cos(1) |
32 | 22 | ``` |
33 | 23 | notice the following |
34 | 24 | - the definition of the macro is similar to the definition of the function with the exception that instead of the keyword `function` we use keyword `macro` |
35 | 25 | - when calling the macro, we signal to the compiler our intention by prepending the name of the macro with `@`. |
36 | | -- the macro receives the expression(s) as the argument instead of the evaluated argument |
37 | | -- when you are invoking the macro, you should be aware that the code you are entering can be arbitrarily modified and you can receive something completely different (this of course does not make sense from the functional perspective). |
| 26 | +- the macro receives the expression(s) as the argument instead of the evaluated argument and also returns an expression that is placed on the position where the macro has been called |
| 27 | +- when you are invoking the macro, you should be aware that the code you are entering can be arbitrarily modified and you can receive something completely different. This meanst that `@` should also serve as a warning that you are leaving Julia's syntax. In practice, it make sense to make things akin to how they are done in Julia or to write Domain Specific Language with syntax familiar in that domain. |
| 28 | + |
| 29 | +We have mentioned above that macros are indispensible in the sense they intercept the code generation after parsing. You might object that I can achieve the above using the following combination of `Meta.parse` and `eval` |
| 30 | +```julia |
| 31 | +ex = Meta.parse("cosp1(x) = 1 + sin(x)") |
| 32 | +ex = replace_sin(ex) |
| 33 | +eval(ex) |
| 34 | +``` |
| 35 | +in the following we cannot do the same trick |
| 36 | +```julia |
| 37 | +function cosp2(x) |
| 38 | + @replace_sin 2 + sin(x) |
| 39 | +end |
| 40 | +cosp2(1) ≈ (2 + cos(1)) |
| 41 | +``` |
| 42 | + |
| 43 | +```julia |
| 44 | +function parse_eval_cosp2(x) |
| 45 | + ex = Meta.parse("2 + sin(x)") |
| 46 | + ex = replace_sin(ex) |
| 47 | + eval(ex) |
| 48 | +end |
| 49 | + |
| 50 | +julia> @code_lowered parse_eval_cosp2(1) |
| 51 | +CodeInfo( |
| 52 | +1 ─ %1 = Base.getproperty(Main.Meta, :parse) |
| 53 | +│ ex = (%1)("2 + sin(x)") |
| 54 | +│ ex = Main.replace_sin(ex) |
| 55 | +│ %4 = Main.eval(ex) |
| 56 | +└── return %4 |
| 57 | +) |
| 58 | +``` |
| 59 | + |
| 60 | +!!! info |
| 61 | + ### Scope of eval |
| 62 | + `eval` function is always evaluated in the global scope of the `Module` in which the macro is called (note that there is that by default you operate in the `Main` module). Moreover, `eval` takes effect **after** the function has been has been executed. This can be demonstrated as |
| 63 | + ```julia |
| 64 | + add1(x) = x + 1 |
| 65 | + function redefine_add(x) |
| 66 | + eval(:(add1(x) = x - 1)) |
| 67 | + add1(x) |
| 68 | + end |
| 69 | + julia> redefine_add(1) |
| 70 | + 2 |
| 71 | + |
| 72 | + julia> redefine_add(1) |
| 73 | + 0 |
| 74 | + ``` |
| 75 | + |
| 76 | + |
| 77 | +`@macroexpand` can allow use to observe, how the macro will be expanded. We can use it for example |
| 78 | +```julia |
| 79 | +@macroexpand @replace_sin(sinp1(x) = 1 + sin(x)) |
| 80 | +``` |
| 81 | + |
| 82 | +## What goes under the hood? |
| 83 | +Let's consider what the compiler is doing in this call |
| 84 | +```julia |
| 85 | +function cosp2(x) |
| 86 | + @replace_sin 2 + sin(x) |
| 87 | +end |
| 88 | +``` |
38 | 89 |
|
| 90 | +First, Julia parses the code into the AST as |
| 91 | +```julia |
| 92 | +ex = Meta.parse(""" |
| 93 | + function cosp2(x) |
| 94 | + @replace_sin 2 + sin(x) |
| 95 | +end |
| 96 | +""") |> Base.remove_linenums! |
| 97 | +dump(ex) |
| 98 | +``` |
| 99 | +We observe that there is a macrocall in the AST, which means that Julia will expand the macro and put it in place |
| 100 | +```julia |
| 101 | +ex.args[2].args[1].head # the location of the macrocall |
| 102 | +ex.args[2].args[1].args[1] # which macro to call |
| 103 | +ex.args[2].args[1].args[2] # line number |
| 104 | +ex.args[2].args[1].args[3] # on which expression |
| 105 | +``` |
| 106 | +let's run the `replace_sin` and insert it back |
| 107 | +```julia |
| 108 | +ex.args[2].args[1] = replace_sin(ex.args[2].args[1].args[3]) |
| 109 | +ex |> dump |
| 110 | +``` |
| 111 | +now, `ex` contains the expanded macro and we can see that it correctly defines the function |
| 112 | +```julia |
| 113 | +eval(ex) |
| 114 | +``` |
39 | 115 | ## Calling macros |
40 | 116 | Macros can be called without parentheses |
41 | 117 | ```julia |
@@ -64,21 +140,202 @@ end |
64 | 140 | ``` |
65 | 141 | (the `@showarg(1 + 1, :x) ` raises an error, since `:(:x)` is of Type `QuoteNode`). |
66 | 142 |
|
67 | | -<!-- We can observe the AST corresponding to the macro call using |
| 143 | +Observe that macro dispatch is based on the types of AST that are handed to the macro, not the types that the AST evaluates to at runtime. |
| 144 | + |
| 145 | +## Notes on quotation |
| 146 | +In the previous lecture we have seen that we can *quote a block of code*, which tells the compiler to treat the input as an data and parse it. We have talked about three ways of quoting code. |
| 147 | +1. `:(quoted code)` |
| 148 | +2. Meta.parse(input_string) |
| 149 | +3. `quote ... end` |
| 150 | +The truth is that Julia does not do full quotation, but a *quasiquotation* is it allows you to **interpolate** expressions inside the quoted code using `$` symbol similar to the string. This is handy, as sometimes, when we want to insert into the quoted code an result of some computation / preprocessing. |
| 151 | +Observe the following difference in returned code |
| 152 | +```julia |
| 153 | +a = 5 |
| 154 | +:(x = a) |
| 155 | +:(x = $(a)) |
| 156 | +let y = :x |
| 157 | + :(1 + y), :(1 + $y) |
| 158 | +end |
| 159 | +``` |
| 160 | +In contrast to the behavior of `:()` (or `quote ... end`, true quotation would not perform interpolation where unary `$` occurs. Instead, we would capture the syntax that describes interpolation and produce something like the following: |
68 | 161 | ```julia |
69 | | -eval(Expr(:macrocall, Symbol("@showarg"), :(1 + 1))) |
70 | | -Meta.parse("@showarg 1 + 1") |
71 | | -``` --> |
| 162 | +( |
| 163 | + :(1 + x), # Quasiquotation |
| 164 | + Expr(:call, :+, 1, Expr(:$, :x)), # True quotation |
| 165 | +) |
| 166 | +``` |
72 | 167 |
|
73 | | -`@macroexpand` can allow use to observe, how the macro will be expanded. We can use it for example |
| 168 | +When we need true quoting, i.e. we need something to stay quoted, we can use `QuoteNode` as |
74 | 169 | ```julia |
75 | | -@macroexpand @replace_sin(sinp1(x) = 1 + sin(x)) |
| 170 | +macro true_quote(e) |
| 171 | + QuoteNode(e) |
| 172 | +end |
| 173 | +let y = :x |
| 174 | + ( |
| 175 | + @true_quote(1 + $y), |
| 176 | + :(1 + $y), |
| 177 | + ) |
| 178 | +end |
76 | 179 | ``` |
| 180 | +At first glance, `QuoteNode` wrapper seems to be useless. But `QuoteNode` has clear value when it's used inside a macro to indicate that something should stay quoted even after the macro finishes executing. Also notice that the expression received by macro was quoted, not quasiquoted, since in the latter case `$y` would be replaced. We can demonstate it using the `@showarg` macro introduced earlier, as |
| 181 | +```julia |
| 182 | +@showarg(1 + $x) |
| 183 | +``` |
| 184 | +The error is raised after the macro was evaluated and the output has been inserted to parsed AST. |
77 | 185 |
|
| 186 | +Macros do not know about runtime values, they only know about syntax trees. When a macro receives an expression with a $x in it, it can't interpolate the value of x into the syntax tree because it reads the syntax tree before x ever has a value! So the interpolation syntax in macros is not given any actual meaning in julia. |
| 187 | + |
| 188 | +Instead, when a macro is given an expression with $ in it, it assumes you're going to give your own meaning to $x. In the case of BenchmarkTools.jl they return code that has to wait until runtime to receive the value of x and then splice that value into an expression which is evaluated and benchmarked. Nowhere in the actual body of the macro do they have access to the value of x though. |
| 189 | + |
| 190 | + |
| 191 | +!!! info |
| 192 | + ### Why `$` for interpolation? |
| 193 | + The `$` string for interpolation was used as it identifies the interpolation inside the string and inside the command. For example |
| 194 | + ```julia |
| 195 | + a = 5 |
| 196 | + s = "a = $(5)" |
| 197 | + typoef(s) |
| 198 | + println(s) |
| 199 | + filename = "/tmp/test_of_interpolation" |
| 200 | + run(`touch $(filename)`) |
| 201 | + ``` |
78 | 202 |
|
79 | | -## Basics |
80 | | -## non-standard string literals |
81 | 203 | ## Macro hygiene |
| 204 | +Macro hygiene is a term coined in 1986 and it says that the evaluation of the macro should not have an effect on the surrounding call. By default, all macros in Julia are hygienic which means that variables introduced in the macro are `gensym`ed to have unique names and function points to global functions. |
| 205 | + |
| 206 | +Let's demonstrate it on our own version of an macro `@elapsed` which will return the time that was needed to evaluate the block of code. |
| 207 | +```julia |
| 208 | +macro tooclean_elapsed(ex) |
| 209 | + quote |
| 210 | + tstart = time() |
| 211 | + $(ex) |
| 212 | + time() - tstart |
| 213 | + end |
| 214 | +end |
| 215 | + |
| 216 | +fib(n) = n <= 1 ? n : fib(n-1) + fib(n - 2) |
| 217 | +let |
| 218 | + t = @tooclean_elapsed r = fib(10) |
| 219 | + println("the evaluation of fib took ", t, "s and result is ", r) |
| 220 | +end |
| 221 | +``` |
| 222 | +We see that variable `r` has not been assigned during the evaluation of macro. We have also used `let` block in orders not to define any variables in the global scope. |
| 223 | +Why is that? |
| 224 | +Let's observe how the macro was expanded |
| 225 | +```julia |
| 226 | +julia> Base.remove_linenums!(@macroexpand @tooclean_elapsed r = fib(10)) |
| 227 | +quote |
| 228 | + var"#12#tstart" = Main.time() |
| 229 | + var"#13#r" = Main.fib(10) |
| 230 | + Main.time() - var"#12#tstart" |
| 231 | +end |
| 232 | +``` |
| 233 | +We see that `tstart` in the macro definition was replaced by `var"#12#tstart"`, which is a name generated by Julia's gensym to prevent conflict. The same happens to `r`, which was replaced by `var"#13#r"`. Notice that in the case of `tstart`, we actually want to replace `tstart` with a unique name, such that if we by a bad luck define `tstart` in our code, it would not be affected, as we can see in this example. |
| 234 | +```julia |
| 235 | +let |
| 236 | + tstart = "should not change the value and type " |
| 237 | + t = @tooclean_elapsed r = fib(10) |
| 238 | + println(tstart, " ", typeof(tstart)) |
| 239 | +end |
| 240 | +``` |
| 241 | +But in the second case, we would actually very much like the variable `r` to retain its name, such that we can accesss the results (and also, `ex` can access and change other local variables). Julia offer a way to `escape` from the hygienic mode, which means that the variables will be used and passed as-is. Notice the effect if we escape jus the expression `ex` |
| 242 | + |
| 243 | +```julia |
| 244 | +macro justright_elapsed(ex) |
| 245 | + quote |
| 246 | + tstart = time() |
| 247 | + $(esc(ex)) |
| 248 | + time() - tstart |
| 249 | + end |
| 250 | +end |
| 251 | + |
| 252 | +let |
| 253 | + tstart = "should not change the value and type " |
| 254 | + t = @justright_elapsed r = fib(10) |
| 255 | + println("the evaluation of fib took ", t, "s and result is ", r) |
| 256 | + println(tstart, " ", typeof(tstart)) |
| 257 | +end |
| 258 | +``` |
| 259 | +which now works as intended. We can inspect the output again using `@macroexpand` |
| 260 | +```julia |
| 261 | +julia> Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10)) |
| 262 | +quote |
| 263 | + var"#19#tstart" = Main.time() |
| 264 | + r = fib(10) |
| 265 | + Main.time() - var"#19#tstart" |
| 266 | +end |
| 267 | +``` |
| 268 | +and compare it to `Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10))`. We see that the experssion `ex` has its symbols intact. |
| 269 | +To use the escaping / hygience correctly, you need to have a good understanding how the macro evaluation works and what is needed. Let's now try the third version of the macro, where we escape everything as |
| 270 | +```julia |
| 271 | +macro toodirty_elapsed(ex) |
| 272 | + ex = quote |
| 273 | + tstart = time() |
| 274 | + $(ex) |
| 275 | + time() - tstart |
| 276 | + end |
| 277 | + esc(ex) |
| 278 | +end |
| 279 | + |
| 280 | +let |
| 281 | + tstart = "should not change the value and type " |
| 282 | + t = @toodirty_elapsed r = fib(10) |
| 283 | + println("the evaluation of fib took ", t, "s and result is ", r) |
| 284 | + println(tstart, " ", typeof(tstart)) |
| 285 | +end |
| 286 | +``` |
| 287 | + |
| 288 | +```julia |
| 289 | +julia> Base.remove_linenums!(@macroexpand @toodirty_elapsed r = fib(10)) |
| 290 | +quote |
| 291 | + tstart = time() |
| 292 | + r = fib(10) |
| 293 | + time() - tstart |
| 294 | +end |
| 295 | +``` |
| 296 | + |
| 297 | + |
| 298 | +!!! info |
| 299 | + ### gensym |
| 300 | + |
| 301 | + `gensym([tag])` Generates a symbol which will not conflict with other variable names. |
| 302 | + ```julia |
| 303 | + julia> gensym("hello") |
| 304 | + Symbol("##hello#257") |
| 305 | +``` |
| 306 | +
|
| 307 | +
|
| 308 | +
|
| 309 | +but if we look, how the function is expanded, we see that it is not as we have expected |
| 310 | +```julia |
| 311 | +
|
| 312 | +macro replace_sin(ex) |
| 313 | + replace_sin(esc(ex)) |
| 314 | +end |
| 315 | +
|
| 316 | +function cosp2(x) |
| 317 | + @replace_sin 2 + sin(x) |
| 318 | +end |
| 319 | +
|
| 320 | +julia> @code_lowered(cosp2(1.0)) |
| 321 | +CodeInfo( |
| 322 | +1 ─ %1 = Main.cos(Main.x) |
| 323 | +│ %2 = 2 + %1 |
| 324 | +└── return %2 |
| 325 | +) |
| 326 | +``` |
| 327 | +why is that |
| 328 | + |
82 | 329 | ## DSL |
| 330 | +## non-standard string literals |
| 331 | +``` |
| 332 | +macro r_str(p) |
| 333 | + Regex(p) |
| 334 | +end |
| 335 | +``` |
83 | 336 | ## Write @exfiltrate macro |
84 | | -`Base.@locals` |
| 337 | +`Base.@locals` |
| 338 | + |
| 339 | +## sources |
| 340 | +Great discussion on evaluation of macros |
| 341 | +https://discourse.julialang.org/t/interpolation-in-macro-calls/25530 |
0 commit comments