A while loop macro hygiene problem

defmodule My do
  defmacro while(expression, do: block) do
    quote do
      try do
        for _ <- Stream.cycle([:ok]) do
          if unquote(expression) do
            unquote(block)
          else
            throw :break
          end
        end
      catch
        :break -> :ok
      end
    end
  end
end

defmodule Test do
  import My

  def go do
    x = 7

    while x > 4 do
      IO.puts x
      x = x - 1   # line 27
    end

  end
end

That just prints 7 in a never ending loop. That result was foreshadowed by the warning:

warning: variable “x” is unused
while.ex:27

Is there a way around that issue? Or, is it really not possible to write a while loop in Elixir other than one that takes an expression such as Process.alive?(pid) where the process just sleeps for N seconds then terminates?

This may be a helpful read https://elixir-lang.org/getting-started/recursion.html#loops-through-recursion

The immutability of data can be surprising when you first start. I’d suggest you try out a couple of examples using recursion where you’d use loops in other languages.

1 Like

At the end of your loop, x is really unused…

You might try a little recursion, as @fmcgeough said.

defmodule Test do
  def go(n) when is_integer(n) and n > 4 do
    IO.puts n
    go(n-1)
  end
  def go(_), do: IO.puts "This is the end"
end

Which output…

iex(1)> Test.go 7
7
6
5
This is the end
:ok

storing state in an Agent (to get around immutable data issue) would let you do a counter-type while loop.

defmodule Test do
  import My

  def go do
     {:ok, pid} = Agent.start_link(fn -> 7 end)

     while ( x = Agent.get(pid, fn state -> state end)) > 4 do 
          IO.puts(x)
         Agent.update(pid, fn state -> state - 1 end)
      end 
      Agent.stop(pid)
  end
end

The actual warning is much more detailed:

$ elixir while.exs
warning: variable "x" is unused

Note variables defined inside case, cond, fn, if and similar do not leak. If you want to conditionally override an existing variable "x", you will have to explicitly return the variable. For example:

    if some_condition? do
      atom = :one
    else
      atom = :two
    end

should be written as

    atom =
      if some_condition? do
        :one
      else
        :two
      end

Unused variable found at:
  while.exs:27

And it turns out it has nothing to do with macros:

defmodule Test do

  def go do
    x = 7

    try do
      for _ <- Stream.cycle([:ok]) do
        if x > 4 do
          IO.puts x
          x = x - 1
        else
          throw :break
        end
      end
    catch
      :break -> :ok
    end
  end

end

has exactly the same problem. for is a list comprehension (not a “for loop”) - so I suspect that each element (:ok) of the stream creates a new closure that initializes x to its current value 7. What the compiler is warning you about is that the closure’s value of x isn’t used anywhere.

There was a discussion about a “while” loop a while back.

If you look at the examples in the book (p.25) you should notice that there is nothing in block that is driving towards the terminating condition for expression - i.e. there is nothing happening in block that changes the state expression is inspecting.

1 Like

The Agent obscures the fact that at the lower level recursion is still being used to emulate mutation that is typical of iteration.

defmodule My do
  defmacro while(expression, do: block) do
    quote do
      try do
        for _ <- Stream.cycle([:ok]) do
          if unquote(expression) do
            unquote(block)
          else
            throw :break
          end
        end
      catch
        :break -> :ok
      end
    end
  end
end

defmodule Test do
  import My

  def go() do
    {predicate, next, done} = init(7,4)

    while predicate.() do
      next.()
    end
    done.()

  end

  def init(value, limit) do
    pid = spawn(__MODULE__, :tick_down, [value])
    {
      make_predicate(pid, limit),
      make_sender(pid, :tick),
      make_sender(pid, :done),
    }
  end

  def tick_down(current) do
    receive do
      {:compare, limit, from} ->
        send(from, {self(), current > limit})
        tick_down(current)     # recursion
      :tick ->
        IO.puts(current)
        tick_down(current - 1) # recursion emulating mutation
      _ ->
        :ok
    end
  end

  defp make_sender(pid, msg),
    do: fn -> send(pid, msg) end

  defp make_predicate(pid, limit) when is_integer(limit) do
    fn ->
      send(pid, {:compare, limit, self()})
      receive do
        {^pid, result} ->
          result
      end
    end
  end

end


Test.go()

The only thing I see is:

iex(1)> c "while.ex"
warning: variable "x" is unused
while.ex:29
[Test, My]
iex(2)>

And it turns out it has nothing to do with macros:

But…try is a macro, so I think we need to adjust the example:

defmodule Test do

  def go do
    try do
      x = 7  # Inside the try

      for _ <- Stream.cycle([:ok]) do
        if x > 4 do
          x = x - 1
          IO.puts x  # switched order
        else
          throw :break
        end
      end
    catch
      :break -> :ok
    end
  end

end

…and with the IO.puts statement after x = x - 1, I no longer get the warning. But, the result is the same–an endless loop of output, which I think proves your point about the closures. There’s no warning that x is undefined, so the value of x is retrieved from the surrounding context, and the statement x = x - 1 creates a new local x inside the for-block.

If you look at the examples in the book (p.25) you should notice that there is nothing in block that is driving towards the terminating condition for expression

Yes, that is what prompted me to try to create a more general purpose while loop. I thought the example in the book was a pretty crippled example of a while loop and didn’t live up to its hype.

Because of the nature how comprehensions (in Elixir or Erlang) work your code is essentially identical to:

defmodule Test do

  def go do
    x = 7

    try do
      for _ <- Stream.cycle([:ok]), do: next(x)
    catch
      :break -> :ok
    end
  end

  def next(x) do
    if x > 4 do
      x = x - 1
      IO.puts x  # switched order
    else
      throw :break
    end
  end
end

Test.go()

The above code should make it clear the x in block is shadowing the x in go(). So whenever block is executed, the value of the x in go() is copied to the new x inside block - and what gets rebound is the x inside block , not the x inside go().

You can actually make your macro work as you expect by collecting the used arguments, passing them through the accumulator and packing it all into the function in an Enum.reduce call.

But, why? ^.^