You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -5,14 +5,19 @@ In its essence, macro is a function, which
5
5
2. modify the expressions in argument
6
6
3. insert the modified expression at the same place as the one that is parsed.
7
7
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:
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. **Since they are executed during parsing, they do not have access to the values of their arguments, but only to their syntax**.
9
9
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
10
+
To illustrate the difference, consider the following example:
11
+
12
+
One of the very convenient and highly recommended ways to write macros is to write functions modifying the `Expr`ession and then call that function in the macro. Let's demonstrate on an example, where every occurrence of `sin` is replaced by `cos`.
13
+
We defined the function recursively traversing the AST and performing the substitution
- 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`
25
30
- when calling the macro, we signal to the compiler our intention by prepending the name of the macro with `@`.
26
31
- 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`
32
+
- when you are using macro, you should be as a user 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.
33
+
Inspecting the lowered code
34
+
```julia
35
+
Meta.@lower@replace_sin(cosp1(x) =1+sin(x))
36
+
```
37
+
We obeserve that there is no trace of macro in lowered code, which demonstrates that the macro has been after code has been parsed but before it has been lowered. In this sense macros are indispensible, as you cannot replace them simply by the combination of `Meta.parse` end `eval`. You might object that in the above example it is possible, which is true, but only because the effect of the macro is in the global scope.
30
38
```julia
31
39
ex = Meta.parse("cosp1(x) = 1 + sin(x)")
32
40
ex =replace_sin(ex)
33
41
eval(ex)
34
42
```
35
-
in the following we cannot do the same trick
43
+
The following example cannot be achieved by the same trick, as the output of the macro modifies just the body of the function
36
44
```julia
37
45
functioncosp2(x)
38
46
@replace_sin2+sin(x)
39
47
end
40
48
cosp2(1) ≈ (2+cos(1))
41
49
```
42
-
50
+
This is not possible
43
51
```julia
44
52
functionparse_eval_cosp2(x)
45
53
ex = Meta.parse("2 + sin(x)")
46
54
ex =replace_sin(ex)
47
55
eval(ex)
48
56
end
57
+
```
58
+
as can be seen from
59
+
```julia
60
+
julia>@code_loweredcosp2(1)
61
+
CodeInfo(
62
+
1 ─ %1= Main.cos(x)
63
+
│ %2=2+%1
64
+
└── return%2
65
+
)
49
66
50
67
julia>@code_loweredparse_eval_cosp2(1)
51
68
CodeInfo(
@@ -58,29 +75,29 @@ CodeInfo(
58
75
```
59
76
60
77
!!! 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
78
+
### Scope of eval
79
+
`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
80
+
```julia
81
+
add1(x) = x + 1
82
+
function redefine_add(x)
83
+
eval(:(add1(x) = x - 1))
84
+
add1(x)
85
+
end
86
+
julia> redefine_add(1)
87
+
2
71
88
72
-
julia> redefine_add(1)
73
-
0
74
-
```
75
-
89
+
julia> redefine_add(1)
90
+
0
91
+
92
+
```
76
93
77
-
`@macroexpand`can allow use to observe, how the macro will be expanded. We can use it for example
94
+
Macros are quite tricky to debug. Macro `@macroexpand`allows to observethe expansion of macros. Observe the effect as
78
95
```julia
79
-
@macroexpand@replace_sin(sinp1(x) =1+sin(x))
96
+
@macroexpand@replace_sin(cosp1(x) =1+sin(x))
80
97
```
81
98
82
-
## What goes under the hood?
83
-
Let's consider what the compiler is doing in this call
99
+
## What goes under the hood of macro expansion?
100
+
Let's consider that the compiler is compiling
84
101
```julia
85
102
functioncosp2(x)
86
103
@replace_sin2+sin(x)
@@ -103,7 +120,7 @@ ex.args[2].args[1].args[1] # which macro to call
103
120
ex.args[2].args[1].args[2] # line number
104
121
ex.args[2].args[1].args[3] # on which expression
105
122
```
106
-
let's run the `replace_sin` and insert it back
123
+
We can manullay run `replace_sin` and insert it back on the relevant sub-part of the sub-tree
but they use the very same multiple dispatch as functions
143
+
Macros use the very same multiple dispatch as functions, which allows to specialize macro calls
127
144
```julia
128
145
macroshowarg(x1, x2::Symbol)
129
146
println("two argument version, second is Symbol")
147
+
@show x1
148
+
@show x2
130
149
x1
131
150
end
132
151
macroshowarg(x1, x2::Expr)
133
-
println("two argument version, second is Symbol")
152
+
println("two argument version, second is Expr")
153
+
@show x1
154
+
@show x2
134
155
x1
135
156
end
136
157
@showarg(1+1, x)
137
158
@showarg(1+1, 1+3)
138
159
@showarg1+1, 1+3
139
160
@showarg1+11+3
140
161
```
141
-
(the `@showarg(1 + 1, :x) ` raises an error, since `:(:x)` is of Type `QuoteNode`).
162
+
(the `@showarg(1 + 1, :x)` raises an error, since `:(:x)` is of Type `QuoteNode`).
163
+
142
164
143
165
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
166
145
167
## 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.
168
+
In the previous lecture we have seen that we can *quote a block of code*, which tells the compiler to treat the input as a data and parse it. We have talked about three ways of quoting code.
147
169
1.`:(quoted code)`
148
170
2. Meta.parse(input_string)
149
171
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.
172
+
The truth is that Julia does not do full quotation, but a *quasiquotation*as 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
173
Observe the following difference in returned code
152
174
```julia
153
175
a =5
@@ -160,11 +182,19 @@ end
160
182
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:
When we need true quoting, i.e. we need something to stay quoted, we can use `QuoteNode` as
169
199
```julia
170
200
macrotrue_quote(e)
@@ -177,13 +207,22 @@ let y = :x
177
207
)
178
208
end
179
209
```
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
210
+
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 its. Also notice that the expression received by macro are quoted, not quasiquoted, since in the latter case `$y` would be replaced. We can demonstate it using the `@showarg` macro introduced earlier, as
181
211
```julia
182
212
@showarg(1+$x)
183
213
```
184
214
The error is raised after the macro was evaluated and the output has been inserted to parsed AST.
185
215
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.
or `@benchmark` support interpolation of values. This interpolation needs to be handled by the logic of the macro and is not automatically handled by Julia language.
224
+
225
+
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!
187
226
188
227
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
228
@@ -193,15 +232,15 @@ Instead, when a macro is given an expression with $ in it, it assumes you're goi
193
232
The `$` string for interpolation was used as it identifies the interpolation inside the string and inside the command. For example
194
233
```julia
195
234
a = 5
196
-
s = "a = $(5)"
235
+
s = "a = $(a)"
197
236
typoef(s)
198
237
println(s)
199
238
filename = "/tmp/test_of_interpolation"
200
239
run(`touch $(filename)`)
201
240
```
202
241
203
242
## Macro hygiene
204
-
Macro hygiene is a term coined in 1986. The problem it addresses is following: if you're automatically generating code, it's possible that you will introduce variable names in your generated code that will clash with existing variable names in the scope in which a macro is called. These clashes might cause your generated code to read from or write to variables that you should not interacting with. A macro is hygienic when it does not interact with existing variables, which means that when macro is evaluated, it should not have any effect on the surrounding code.
243
+
Macro hygiene is a term coined in 1986. The problem it addresses is following: if you're automatically generating code, it's possible that you will introduce variable names in your generated code that will clash with existing variable names in the scope in which a macro is called. These clashes might cause your generated code to read from or write to variables that you should not be interacting with. A macro is hygienic when it does not interact with existing variables, which means that when macro is evaluated, it should not have any effect on the surrounding code.
205
244
206
245
By default, all macros in Julia are hygienic which means that variables introduced in the macro have automatically generated names, where Julia ensures they will not collide with user's variable. These variables are created by `gensym` function / macro.
207
246
@@ -226,8 +265,10 @@ end
226
265
227
266
fib(n) = n <=1? n :fib(n-1) +fib(n -2)
228
267
let
268
+
tstart ="should not change the value and type"
229
269
t =@tooclean_elapsed r =fib(10)
230
270
println("the evaluation of fib took ", t, "s and result is ", r)
271
+
@show tstart
231
272
end
232
273
```
233
274
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.
@@ -249,8 +290,7 @@ let
249
290
println(tstart, "", typeof(tstart))
250
291
end
251
292
```
252
-
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`
253
-
293
+
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 just the expression `ex`
254
294
```julia
255
295
macrojustright_elapsed(ex)
256
296
quote
@@ -276,8 +316,7 @@ quote
276
316
Main.time() -var"#19#tstart"
277
317
end
278
318
```
279
-
and compare it to `Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10))`. We see that the experssion `ex` has its symbols intact.
280
-
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
319
+
and compare it to `Base.remove_linenums!(@macroexpand @justright_elapsed r = fib(10))`. We see that the expression `ex` has its symbols intact. 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
281
320
```julia
282
321
macrotoodirty_elapsed(ex)
283
322
ex =quote
@@ -309,7 +348,7 @@ From the above we can also see that hygiene-pass occurs after the macro has been
309
348
julia>esc(:x)
310
349
:($(Expr(:escape, :x)))
311
350
```
312
-
The definition in `essentials.jl:480` is pretty simple as `esc(@nospecialize(e)) = Expr(:escape, e)`.
351
+
The definition in `essentials.jl:480` is pretty simple as `esc(@nospecialize(e)) = Expr(:escape, e)`, but it does not tell anything about the actual implementation, which is hidden probably in the macro-expanding logic.
313
352
314
353
With that in mind, we can now understand our original example with `@replace_sin`. Recall that we have defined it as
315
354
```julia
@@ -351,12 +390,22 @@ CodeInfo(
351
390
352
391
### Why hygienating the function calls?
353
392
354
-
functin foo()
355
-
cos(x) = exp()
356
-
@repace_sin
393
+
```julia
394
+
functionfoo(x)
395
+
cos(x) =exp(x)
396
+
@replace_sin1+sin(x)
357
397
end
358
398
399
+
foo(1.0) ≈1+exp(1.0)
400
+
401
+
functionfoo2(x)
402
+
cos(x) =exp(x)
403
+
@hygienic_replace_sin1+sin(x)
404
+
end
359
405
406
+
x =1.0
407
+
foo2(1.0) ≈1+cos(1.0)
408
+
```
360
409
361
410
### Can I do the hygiene by myself?
362
411
Yes, it is by some considered to be much simpler (and safer) then to understand, how macro hygiene works.
@@ -404,10 +453,10 @@ also notice that the escaping is only partial (running `@macroexpand @m2 @m1 1 +
404
453
## Write @exfiltrate macro
405
454
Since Julia's debugger is a complicated story, people have been looking for tools, which would simplify the debugging. One of them is a macro `@exfiltrate`, which copies all variables in a given scope to a dafe place, from where they can be collected later on. This helps you in evaluating the function.
406
455
407
-
Let's try to implement such facility. What is our strategy
408
-
-we can collect names and values of variables in a given scope using the macro `Base.@locals`
409
-
- We will store variables in some global variable in a module, such that we have one place from which we can retrieve them and we are certain that this storage would not interact with existing code
410
-
- the `@exfiltrate`macro should be as easyto use as possible.
456
+
Let's try to implement such facility.
457
+
-We collect names and values of variables in a given scope using the macro `Base.@locals`
458
+
- We store variables in some global variable in some module, such that we have one place from which we can retrieve them and we are certain that this storage would not interact with any existing code.
459
+
-If the `@exfiltrate` should be easy, ideally called without parameters, it has to be implemented as a macro to supply the relevant variables to be stored.
411
460
412
461
```julia
413
462
module Exfiltrator
@@ -449,6 +498,23 @@ end
449
498
450
499
inside_function()
451
500
501
+
Exfiltrator.environment
502
+
503
+
functiona()
504
+
a =1
505
+
@exfiltrate
506
+
end
507
+
508
+
functionb()
509
+
b =1
510
+
a()
511
+
end
512
+
functionc()
513
+
c =1
514
+
b()
515
+
end
516
+
517
+
c()
452
518
Exfiltrator.environment
453
519
```
454
520
@@ -615,11 +681,24 @@ end
615
681
```
616
682
617
683
## non-standard string literals
618
-
```
619
-
macro r_str(p)
620
-
Regex(p)
684
+
Julia allows to customize parsing of strings. For example we can define regexp matcher as
685
+
`r"^\s*(?:#|$)"`, i.e. using the usual string notation prepended by the string `r`.
686
+
687
+
You can define these "parsers" by yourself using the macro definition with suffix `_str`
688
+
```julia
689
+
macrodebug_str(p)
690
+
@show p
691
+
p
621
692
end
622
693
```
694
+
by invoking it
695
+
```julia
696
+
debug"hello"
697
+
```
698
+
we see that the string macro receives string as an argument.
699
+
700
+
Why are they useful? Sometimes, we want to use syntax which is not compatible with Julia's parser. For example `IntervalArithmetics.jl` allows to define an interval open only from one side, for example `[a, b)`, which is something that Julia's parser would not like much. String macro solves this problem by letting you to write the parser by your own.
Copy file name to clipboardExpand all lines: docs/src/lecture_08/lecture.md
+3-2Lines changed: 3 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -61,9 +61,10 @@ We initialize the Jacobian by ``\frac{\partial y}{\partial y_0},`` which is agai
61
61
62
62
The fact that we need to store intermediate outs has a huge impact on the memory requirements. Therefore when we have been talking few lectures ago that we should avoid excessive memory allocations, here we have an algorithm where the excessive allocation is by design.
63
63
64
-
## Let's work an example
64
+
## Let's workout an example
65
65
66
-
### `n` to `1` function
66
+
### ``f: \mathbb{R}^2 \rightarrow \mathbb{R}``
67
+
The case, where the input dimension is large and output is one is prevalent in machine learning due to minimizing scalar loss function. Hence as explained above, the reverse-diff should be theoretically better. Let's try it. Let's consider function
0 commit comments