Skip to content

Commit fd24b5d

Browse files
committed
Finished Chapter 1
1 parent 6804fbd commit fd24b5d

File tree

7 files changed

+200
-3
lines changed

7 files changed

+200
-3
lines changed

README.md

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ Moreover, don't expect direct quotes, changing the sentences vocbulary helps me
1717
- [What are Macros?](#what-are-macros)
1818
- [The Abstract Syntax Tree](#the-abstract-syntax-tree)
1919
- [Trying It All Together](#trying-it-all-together)
20+
- [Macro Rules](#macro-rules)
21+
- [The Abstract Syntax Tree - Demystified](#the-abstract-syntax-tree-demystified)
22+
- [High-Level Syntax vs. Low-level AST](#high-level-syntax-vs-low-level-ast)
23+
- [AST Literals](#ast-literals)
24+
- [Macros: The Building Blocks of Elixir](#macros-the-building-blocks-of-elixir)
2025

2126
### Chapter 1 - The Language of Macros
2227

@@ -71,7 +76,7 @@ iex> quote do: div(10, 2)
7176
In most languages, we would have to parse a string expression into something digestible by our program. With Elixir, we can access
7277
the representation of expressions directly with macros."
7378

74-
[First macro - `math.exs`](math.exs)
79+
[First macro - `math.exs`](math/math.exs)
7580

7681
<details>
7782
<summary>math.exs</summary>
@@ -144,7 +149,168 @@ Which is not the expected result. For this, we use unquote:
144149
I assume from this then when you pass variables to a macro they need to be `unqoute`-d, incontrast to passing a value directly.
145150
Which I'm not following as Elixir is pass-by-value so wouldn't the value just be known?
146151

147-
Yes that's correct because we are dealing with ASTs not the data it represents; therefore the pass-by-value argument doesn't hold.
152+
Turns out that yes that's correct because we are dealing with ASTs not the data it represents; therefore the pass-by-value argument doesn't hold.
148153
Much like interpolation from Ecto and the difference between `"Hello world"` and `"Hello #{world}`.
149154

150-
Back to the `math.exs`example.
155+
- We know that macros receive the AST representation of the arguments.
156+
- "To complete the macro, we used `quote` to return an AST for the caller to replace out `Math.say` invocations." I'm assuming by this that every macro needs some form of `quote`.
157+
158+
#### Macro Rules
159+
160+
- **Rule 1: Don't write macros.** We have to remember that writing code to produce code require special care. It's easy to get caught in a web of your own code generation. Too many macros can make debugging more difficult.
161+
162+
- **Rule: 2 Use macros gratuitously.** Metaprogramming is sometimes framed as complex and fragile as well as offering productive advantages in a fraction of the required code. It's important to keep this duality in mind when writing macros.
163+
164+
#### The Abstract Syntax Tree - Demystified
165+
166+
- Every expression you write in Elixir breaks down to a three-element tuple in the AST.
167+
- This uniform format makes pattern matching arguments a lot easier.
168+
- Quoting a couple more complex expressions to see how entire Elixir prograns are structure in the AST.
169+
170+
<details>
171+
<summary>(5 * 2 - 1 + 7)</summary>
172+
173+
```elixir
174+
iex(1)> quote do: (5 * 2) - 1 + 7
175+
{:+, [context: Elixir, import: Kernel],
176+
[
177+
{:-, [context: Elixir, import: Kernel],
178+
[{:*, [context: Elixir, import: Kernel], [5, 2]}, 1]},
179+
7
180+
]}
181+
```
182+
183+
</details>
184+
185+
<details>
186+
<summary>MyModule</summary>
187+
188+
```elixir
189+
iex(1)> quote do
190+
...(1)> defmodule MyModule do
191+
...(1)> def hello, do: "World"
192+
...(1)> end
193+
...(1)> end
194+
{:defmodule, [context: Elixir, import: Kernel],
195+
[
196+
{:__aliases__, [alias: false], [:MyModule]},
197+
[
198+
do: {:def, [context: Elixir, import: Kernel],
199+
[{:hello, [context: Elixir], Elixir}, [do: "World"]]}
200+
]
201+
]}
202+
```
203+
204+
</details>
205+
206+
- A stacking tuple was produced from each quoted exoression. The first example shows the familiar structures used by our `Math.say` macro, but multiple tuples are stacked into an embedded tree to represent the entire expression. The result of the second example shows how an entire Eliir module represented by a simple AST.
207+
208+
- **All elixir code is represented as a series of three-element tuples with the following format:**
209+
210+
- **The first element** is an atom denoting the funcation call, or another tuple, representing a nested node in the AST.
211+
- **The second element** represents metadata about the expression.
212+
- **The third element** is a list of arguments for the function call.
213+
214+
- Applying this to the AST of `(5 * 2) - 1 + 7` from before
215+
216+
```elixir
217+
iex(1)> quote do: (5 * 2) - 1 + 7
218+
{:+, [context: Elixir, import: Kernel],
219+
[
220+
{:-, [context: Elixir, import: Kernel],
221+
[{:*, [context: Elixir, import: Kernel], [5, 2]}, 1]},
222+
7
223+
]}
224+
```
225+
226+
- An AST tree strunction of functions and arguments has been made. If we were to format this output into a more readable tree:
227+
228+
```bash
229+
+
230+
├── -
231+
│   ├── *
232+
│   │   ├── 5
233+
│   │   └── 7
234+
│   └── 1
235+
└── 7
236+
```
237+
238+
- It seems easiest to start from the end of the AST and work up. The root AST node is the `+` operator, and its arguments are
239+
the number 7 combined with another nested node in the tree. We can see that the nested nodes contain out `(5 * 2)` expression,
240+
whose results are applied to the `- 1` branch.
241+
- `5 * 2` is syntactic sugar for `Kernel.*(5, 2)`. This means the `:*` atom is just a function call from the import of `Kernel` as
242+
you can see from the AST output from before.
243+
244+
#### High-Level Syntax vs. Low-level AST
245+
246+
Comparing the AST from elixir to the source of Lisp. If you look closely, you can see how elixir operates at a layer just above this format.
247+
248+
For example, Lisp source code:
249+
250+
```lisp
251+
(+ (* 2 3) 1)
252+
```
253+
254+
And Elixir from source to generated AST:
255+
256+
```elixir
257+
quote do: 2 * 3 + 1
258+
{:+, _, [{:*, _, [2, 3]}, 1]}
259+
```
260+
261+
If we compare the both you can see that the structure itself is nearly identical with only the syntax being different. The beauty here is that
262+
the transformation from high-level source to low-level AST requires only a `quote` invocation. On the contrary, with Lisp you have all the power
263+
of a programmable AST at the cost of a less natural and flexible syntax. José seperated the AST from the syntax, meaning we get the best of both worlds.
264+
265+
#### AST Literals
266+
267+
- Playing with elixir source, sometimes the results of quoted expressions can appear confusing and irregular. This is because of literals. Literals
268+
have the same representation within the AST and at high-lvel source. This includes atoms, ints, floats, lists, strings and any two-element tuples
269+
containing the former types. Such as the following:
270+
271+
```elixir
272+
iex> quote do: :atom
273+
:atom
274+
275+
iex> quote do: 123
276+
123
277+
278+
iex> quote do: [1, 2, 3]
279+
280+
iex> quote do: {:ok, [1, 2, 3]}
281+
{:ok, [1, 2, 3]}
282+
```
283+
284+
If you pass any of the example to a macro, the macro receives the literal arguments instead of an abstract representation.
285+
286+
```elixir
287+
quoute do: %{a: 1, b: 2}
288+
{:%{}, [], [a: 1, b: 2]}
289+
290+
iex> quoute do: Enum
291+
{:__aliases__, [alias: false], [:Enum]}
292+
```
293+
294+
In these two examples we see that there are two different ways in which elixir types are represented in the AST. Some values are passed through as
295+
is, while more complex tuples are returned as a quoted expression. It's useful to keep literals in mind when writing macros to avoid confusion.
296+
297+
#### Macros: The Building Blocks of Elixir
298+
299+
- Let's imagine that `unless` does not exist in the elixir language. `unless` is essentially the same as a negated if statement, e.g. unless(1 + 1 = 5) would return true.
300+
301+
[Second macro](unless/unless.exs) - `unless.exs`
302+
303+
<details>
304+
<summary>unless.exs</summary>
305+
306+
```elixir
307+
defmodule ControlFlow do
308+
defmacro unless(expression, do: block) do
309+
quote do
310+
if !unquote(expression), do: unquote(block)
311+
end
312+
end
313+
end
314+
```
315+
316+
</details>

callers-context/.iex.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
c("callers_context.exs")

callers-context/callers_context.exs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule Mod do
2+
defmacro definfo do
3+
IO.puts("In macro's context (#{__MODULE__}).")
4+
5+
quote do
6+
IO.puts("In caller's context (#{__MODULE__}).")
7+
8+
def friendly_info do
9+
IO.puts("""
10+
My name is #{__MODULE__}
11+
My functions are #{inspect(__info__(:functions))}
12+
""")
13+
end
14+
end
15+
end
16+
end
17+
18+
defmodule MyModule do
19+
require Mod
20+
Mod.definfo()
21+
end

.iex.exs renamed to math/.iex.exs

File renamed without changes.

math.exs renamed to math/math.exs

File renamed without changes.

unless/.iex.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
c("unless.exs")
2+
require ControlFlow

unless/unless.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule ControlFlow do
2+
defmacro unless(expression, do: block) do
3+
quote do
4+
if !unquote(expression), do: unquote(block)
5+
end
6+
end
7+
end

0 commit comments

Comments
 (0)