@@ -0,0 +1,144 @@
# 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 Symbol 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 )
... > end
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.