Last active
November 20, 2022 20:03
-
-
Save mgwidmann/8238ead88ff57088ca0b to your computer and use it in GitHub Desktop.
An example of metaprogramming, extending the Elixir language, to add the while keyword. Taken from Chris McCord's example in his Metaprogramming Elixir book.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# The Elixir language is very extensible to allow for future additions or | |
# third party developers to take the language in directions that the original | |
# authors could not predict. | |
# | |
# Lets start with understanding what an Elixir macro is | |
iex> quote do | |
...> 1 + 1 | |
...> end | |
{:+, [context: Elixir, import: Kernel], [1, 1]} | |
# All code in elixir can be transformed into the Abstract Syntax Tree (AST) | |
# which all languages have, but few expose. Calling the `quote/1` macro performs | |
# this transformation for the developer so that the code may be manipualted | |
# as data. The format is | |
# {function, metadata, arguments} | |
# Quoted code is executed in another context, so local variables do not apply | |
# unless we tell it to bring it in using the `unquote/1` function | |
iex> a = 1 | |
1 | |
iex> quote do | |
...> unquote(a) + 1 | |
...> end | |
{:+, [context: Elixir, import: Kernel], [1, 1]} | |
# See how it evaluated `a` and the AST returned doesn't include any | |
# reference to it anymore? This is basically string interpolation, | |
# but for code! | |
# The keyword we want to add will take the form | |
while some_expression do | |
a_statement | |
another_statement | |
whatever | |
end | |
# To create a macro, simply define a module and put a defmacro call | |
defmodule While do | |
defmacro while(expression, do: block) do | |
quote do | |
IO.puts "Got: #{unquote(inspect expression)}\n#{unquote(inspect block)}" | |
end | |
end | |
end | |
# Macros must return a quoted expression, or it will fail to compile. Here | |
# we always transform our current `while/2` call into a print statement to | |
# test it out. | |
iex> import While | |
nil | |
iex> while a < b do | |
...> call_a_function(with: data) # Notice how this stuff doesnt need to exist? | |
...> end # Thats because this code is never actually executed! | |
Got: {:<, [line: 18], [{:a, [line: 18], nil}, {:b, [line: 18], nil}]} | |
{:call_a_function, [line: 19], [[with: {:data, [line: 19], nil}]]} | |
# Macros receive the quoted expression rather than the evaluated | |
# expression, and they are expected to return another quoted expression. | |
# Lets write our while macro! | |
defmodule While do | |
defmacro while(expression, do: block) do | |
quote do | |
for _ <- Stream.cycle([:ok]) do # Stream.cycle will create an infinite list to loop through | |
if unquote(expression) do # Whenever this is true we want to execute the block code | |
unquote(block) | |
else | |
# break out somehow | |
end | |
end | |
end | |
end | |
end | |
# This works except we cannot stop the loop ever, since we cannot break out. | |
# One (less than ideal, but functional) way of breaking out is to throw an | |
# exception. This isn't a great pattern, but you'll find that this example is | |
# contrived because a while loop isn't even necessary in the Elixir language. | |
# Throw an exception to break out | |
defmodule While do | |
defmacro while(expression, do: block) do | |
quote do | |
try do # Surround whole for loop with try, so that we can catch when they want to break out | |
for _ <- Stream.cycle([:ok]) do # Stream.cycle will create an infinite list to loop through | |
if unquote(expression) do # Whenever this is true we want to execute the block code | |
unquote(block) | |
else | |
throw :break | |
end | |
end | |
catch | |
:break -> :ok # We only catch the value `:break` if it was thrown, all else is ignored | |
end | |
end | |
end | |
end | |
# This works now, but attempting to try it makes it seem like it doesn't work. | |
# If you try something like: | |
iex> a = 1 | |
1 | |
iex> while a < 10 do | |
...> a = a + 1 | |
...> end | |
# This spins forever and never exits. Thats because data is immutable in Elixir. | |
# The variable `a` is rebound, each loop of a for loop is effectively a new scope | |
# since variables created within cannot be referenced outside. Therefore, `a` in | |
# the while expression always refers to the outside `a` and the `a` created in the | |
# block is a new `a` which is immediately garbage collected. | |
# To actually test this we can rely on another process | |
# Spawn a proccess that will sleep for a minute | |
iex> pid = spawn fn -> :timer.sleep(60_000) end | |
#PID<0.183.0> | |
iex> while Process.alive?(pid) do | |
...> IO.puts "#{inspect :erlang.time} Still alive!" | |
...> end | |
# This prints out the time and the phrase "Still alive!" for less than a | |
# minute (or more if you're slow typer). | |
# Now to add the break feature, so users can exit when they choose. | |
# Simply replace the `throw :break` with `break` in the while macro | |
# and add this funciton in the same module: | |
def break, do: throw :break | |
# Now breaking is possible | |
iex> pid = spawn fn -> :timer.sleep(999_999_999) end | |
#PID<0.291.0> | |
iex> while Process.alive?(pid) do | |
...> if match?({_, _, 0}, :erlang.time) do | |
...> break | |
...> else | |
...> IO.puts "#{inspect :erlang.time} Waiting for the minute to end" | |
...> end | |
...> end | |
# This will print out the time and the phrase until it hits the 0 second, | |
# which will be when a new minute begins. | |
# You can see what something compiles to fairly easily. | |
iex> quote do | |
...> while true do | |
...> :ok | |
...> end | |
...> end |> Macro.expand(__ENV__) |> Macro.to_string |> IO.puts | |
# Printed to screen: | |
try() do | |
for(_ <- Stream.cycle([:ok])) do | |
if(true) do | |
:ok | |
else | |
break | |
end | |
end | |
catch | |
:break -> | |
:ok | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment