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?
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.
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
$ 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.
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.
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()
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 newx 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.