Skip to content

Instantly share code, notes, and snippets.

@qubbit
Forked from mgwidmann/while.ex
Created February 7, 2019 23:11
Show Gist options
  • Save qubbit/38fdd92c34189ac5d1b4edc9bd476f06 to your computer and use it in GitHub Desktop.
Save qubbit/38fdd92c34189ac5d1b4edc9bd476f06 to your computer and use it in GitHub Desktop.

Revisions

  1. @mgwidmann mgwidmann revised this gist Mar 18, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion while.ex
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,7 @@ iex> quote do
    ...> end
    {:+, [context: Elixir, import: Kernel], [1, 1]}

    # All code in elixir can be transformed into the Abstract Symbol Tree (AST)
    # 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
  2. @mgwidmann mgwidmann revised this gist Mar 18, 2016. 1 changed file with 24 additions and 3 deletions.
    27 changes: 24 additions & 3 deletions while.ex
    Original file line number Diff line number Diff line change
    @@ -50,8 +50,8 @@ end
    iex> import While
    nil
    iex> while a < b do
    ...> call_a_function(with: data)
    ...> end
    ...> 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}]]}

    @@ -141,4 +141,25 @@ iex> while Process.alive?(pid) do
    ...> end

    # This will print out the time and the phrase until it hits the 0 second,
    # which will be when a new minute begins.
    # 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
  3. @mgwidmann mgwidmann created this gist Mar 18, 2016.
    144 changes: 144 additions & 0 deletions while.ex
    Original file line number Diff line number Diff line change
    @@ -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.