Skip to content

Commit 3aae06f

Browse files
committed
after 7th lecture
1 parent bc8f523 commit 3aae06f

File tree

2 files changed

+134
-54
lines changed

2 files changed

+134
-54
lines changed

docs/src/lecture_07/lecture.md

Lines changed: 131 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ In its essence, macro is a function, which
55
2. modify the expressions in argument
66
3. insert the modified expression at the same place as the one that is parsed.
77

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**.
99

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
1114
```julia
1215
replace_sin(x::Symbol) = x == :sin ? :cos : x
1316
replace_sin(e::Expr) = Expr(e.head, map(replace_sin, e.args)...)
1417
replace_sin(u) = u
15-
18+
```
19+
and then we define the macro
20+
```julia
1621
macro replace_sin(ex)
1722
replace_sin(esc(ex))
1823
end
@@ -24,28 +29,40 @@ notice the following
2429
- 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`
2530
- when calling the macro, we signal to the compiler our intention by prepending the name of the macro with `@`.
2631
- 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.
3038
```julia
3139
ex = Meta.parse("cosp1(x) = 1 + sin(x)")
3240
ex = replace_sin(ex)
3341
eval(ex)
3442
```
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
3644
```julia
3745
function cosp2(x)
3846
@replace_sin 2 + sin(x)
3947
end
4048
cosp2(1) (2 + cos(1))
4149
```
42-
50+
This is not possible
4351
```julia
4452
function parse_eval_cosp2(x)
4553
ex = Meta.parse("2 + sin(x)")
4654
ex = replace_sin(ex)
4755
eval(ex)
4856
end
57+
```
58+
as can be seen from
59+
```julia
60+
julia> @code_lowered cosp2(1)
61+
CodeInfo(
62+
1%1 = Main.cos(x)
63+
%2 = 2 + %1
64+
└── return %2
65+
)
4966

5067
julia> @code_lowered parse_eval_cosp2(1)
5168
CodeInfo(
@@ -58,29 +75,29 @@ CodeInfo(
5875
```
5976

6077
!!! 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
7188

72-
julia> redefine_add(1)
73-
0
74-
```
75-
89+
julia> redefine_add(1)
90+
0
91+
92+
```
7693

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 observe the expansion of macros. Observe the effect as
7895
```julia
79-
@macroexpand @replace_sin(sinp1(x) = 1 + sin(x))
96+
@macroexpand @replace_sin(cosp1(x) = 1 + sin(x))
8097
```
8198

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
84101
```julia
85102
function cosp2(x)
86103
@replace_sin 2 + sin(x)
@@ -103,7 +120,7 @@ ex.args[2].args[1].args[1] # which macro to call
103120
ex.args[2].args[1].args[2] # line number
104121
ex.args[2].args[1].args[3] # on which expression
105122
```
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
107124
```julia
108125
ex.args[2].args[1] = replace_sin(ex.args[2].args[1].args[3])
109126
ex |> dump
@@ -123,31 +140,36 @@ end
123140
@showarg 1 + 1
124141
@showarg(1 + 1)
125142
```
126-
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
127144
```julia
128145
macro showarg(x1, x2::Symbol)
129146
println("two argument version, second is Symbol")
147+
@show x1
148+
@show x2
130149
x1
131150
end
132151
macro showarg(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
134155
x1
135156
end
136157
@showarg(1 + 1, x)
137158
@showarg(1 + 1, 1 + 3)
138159
@showarg 1 + 1, 1 + 3
139160
@showarg 1 + 1 1 + 3
140161
```
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+
142164

143165
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.
144166

145167
## 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.
147169
1. `:(quoted code)`
148170
2. Meta.parse(input_string)
149171
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.
151173
Observe the following difference in returned code
152174
```julia
153175
a = 5
@@ -160,11 +182,19 @@ end
160182
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:
161183
```julia
162184
(
163-
:(1 + x), # Quasiquotation
185+
:(1 + x), # Quasiquotation
164186
Expr(:call, :+, 1, Expr(:$, :x)), # True quotation
165187
)
166188
```
167189

190+
```jula
191+
for (v, f) in [(:sin, :foo_sin)]
192+
quote
193+
$(f)(x) = $(v)(x)
194+
end |> dump
195+
end
196+
```
197+
168198
When we need true quoting, i.e. we need something to stay quoted, we can use `QuoteNode` as
169199
```julia
170200
macro true_quote(e)
@@ -177,13 +207,22 @@ let y = :x
177207
)
178208
end
179209
```
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
181211
```julia
182212
@showarg(1 + $x)
183213
```
184214
The error is raised after the macro was evaluated and the output has been inserted to parsed AST.
185215

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.
216+
!!! info
217+
Some macros like `@eval` (recall last example)
218+
```julia
219+
for f in [:setindex!, :getindex, :size, :length]
220+
@eval $(f)(A::MyMatrix, args...) = $(f)(A.x, args...)
221+
end
222+
```
223+
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!
187226

188227
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.
189228

@@ -193,15 +232,15 @@ Instead, when a macro is given an expression with $ in it, it assumes you're goi
193232
The `$` string for interpolation was used as it identifies the interpolation inside the string and inside the command. For example
194233
```julia
195234
a = 5
196-
s = "a = $(5)"
235+
s = "a = $(a)"
197236
typoef(s)
198237
println(s)
199238
filename = "/tmp/test_of_interpolation"
200239
run(`touch $(filename)`)
201240
```
202241

203242
## 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.
205244

206245
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.
207246

@@ -226,8 +265,10 @@ end
226265

227266
fib(n) = n <= 1 ? n : fib(n-1) + fib(n - 2)
228267
let
268+
tstart = "should not change the value and type"
229269
t = @tooclean_elapsed r = fib(10)
230270
println("the evaluation of fib took ", t, "s and result is ", r)
271+
@show tstart
231272
end
232273
```
233274
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
249290
println(tstart, " ", typeof(tstart))
250291
end
251292
```
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`
254294
```julia
255295
macro justright_elapsed(ex)
256296
quote
@@ -276,8 +316,7 @@ quote
276316
Main.time() - var"#19#tstart"
277317
end
278318
```
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
281320
```julia
282321
macro toodirty_elapsed(ex)
283322
ex = quote
@@ -309,7 +348,7 @@ From the above we can also see that hygiene-pass occurs after the macro has been
309348
julia> esc(:x)
310349
:($(Expr(:escape, :x)))
311350
```
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.
313352

314353
With that in mind, we can now understand our original example with `@replace_sin`. Recall that we have defined it as
315354
```julia
@@ -351,12 +390,22 @@ CodeInfo(
351390

352391
### Why hygienating the function calls?
353392

354-
functin foo()
355-
cos(x) = exp()
356-
@repace_sin
393+
```julia
394+
function foo(x)
395+
cos(x) = exp(x)
396+
@replace_sin 1 + sin(x)
357397
end
358398

399+
foo(1.0) 1 + exp(1.0)
400+
401+
function foo2(x)
402+
cos(x) = exp(x)
403+
@hygienic_replace_sin 1 + sin(x)
404+
end
359405

406+
x = 1.0
407+
foo2(1.0) 1 + cos(1.0)
408+
```
360409

361410
### Can I do the hygiene by myself?
362411
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 +
404453
## Write @exfiltrate macro
405454
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.
406455

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 easy to 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.
411460

412461
```julia
413462
module Exfiltrator
@@ -449,6 +498,23 @@ end
449498

450499
inside_function()
451500

501+
Exfiltrator.environment
502+
503+
function a()
504+
a = 1
505+
@exfiltrate
506+
end
507+
508+
function b()
509+
b = 1
510+
a()
511+
end
512+
function c()
513+
c = 1
514+
b()
515+
end
516+
517+
c()
452518
Exfiltrator.environment
453519
```
454520

@@ -615,11 +681,24 @@ end
615681
```
616682

617683
## 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+
macro debug_str(p)
690+
@show p
691+
p
621692
end
622693
```
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.
701+
623702
## sources
624703
Great discussion on evaluation of macros
625704
https://discourse.julialang.org/t/interpolation-in-macro-calls/25530

docs/src/lecture_08/lecture.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,10 @@ We initialize the Jacobian by ``\frac{\partial y}{\partial y_0},`` which is agai
6161

6262
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.
6363

64-
## Let's work an example
64+
## Let's workout an example
6565

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
6768

6869
### `1` to `n` function
6970

0 commit comments

Comments
 (0)