Last week I wrote about Stream patterns in Elixir based on examples I found throughout the Elixir project. I also mentioned that I was having trouble finding good examples of Stream
usage. This week I was reading through Chris McCord’s Metaprogramming Elixir. So far it has been a great read and I’ve learned a lot about macros in Elixir. And, more relevant to this post, I found a great
Stream
example which I would like to analyze and develop into a pattern in this post.
Comprehension over Stream.cycle
Chapter 2 of Metaprogramming Elixir builds a macro to implement a while loop construct. The book builds up the example over several interations so its first version, shown here, is incomplete but illustrative:
# From Chris McCord's “Metaprogramming Elixir” chapter 2
defmacro while(expression, do: block) do
quote do
for _ <- Stream.cycle([:ok]) do
if unquote(expression) do
unquote(block)
else
# break out of loop
end
end
end
end
This stream pattern boils down to
for _ <- Stream.cycle(list), do: f
The pattern gives us a way to repeat the same function over and over again. The pattern I’ve shown is really equivalent to Stream.repeatedly(f)
. The elements of the list, [:ok]
in the book, are unused within the do block and the function f
. We could ask ourselves, why not just use Stream.repeatedly(f)
here in this case? I believe, the example used in the book was choosen to be more illustrative - the block is in the for comprehension rather than an agrument to Stream.repeatedly/1
. This makes the example easier to follow.
But, in addition to clarity, this pattern can lead us to another. Generalizing, we can expand this pattern’s utility.
Comprehension over an infinite stream
We can generalize the pattern to
for x <- an_infinite_stream, do: f
That is, an infinite stream can be used as the source of a for comprehension. This allows us to write an infinite loop, or more accurately an infinite for comprehension. Depdending on the situation this infinite comprehension could be useful.
One example to consider would be a server process which is usually written as an infinite recursion. We can use the pattern to implement something like a loop calling receive
over and over again in order to receive messages from other processes. Let’s consider a simple echo server and compare examples of the recursive and comprehension versions:
Recursive version:
defp receive_recursive do
receive do
arg -> IO.puts("#{arg}")
end
receive_recursive
end
Using the comprehension of an infinite stream pattern we could rewrite the server as:
defp receive_stream do
for arg <- stream_of_events do
IO.puts("#{arg}")
end
end
defp stream_of_events do
Stream.repeatedly(fn ->
receive do
arg -> arg
end
end)
end
The infinite comprehension version required writing an anonymous function around receive
because receive
is not a function itself and this makes the code a bit longer. But, the major difference is that there isn’t a really clean way to exit from receive_stream
. The recursive solution gives a cleaner path to exiting - by simply not making the recursive call. For example we could rewrite the code like this:
defp receive_loop do
receive do
:exit -> IO.puts("Exiting")
arg -> IO.puts("#{arg}")
receive_loop
end
end
Now, sending the server :exit
will cause it to exit. Chapter 2 in Metaprogramming Elixir gives a solution for exiting from a for comprehension. But in some server situations there is never a reason to exit. This comes down to a matter of style: which code do you prefer to read?
If you inerested in the book then sign up below to follow its progress and be notified when it is launched. You'll also receive two free chapters as a sample of what the book will contain.
Along the way you will also receive Elixir tips based on my research for the book.
Conclusion
This week we looked at another Stream example and generalized it to drive an infinite for comprehension. I worked through an example of a server process and wrote it using both the infinite for comprehension format as well as using the more traditional recursive function. The two forms are essentially equivalent, and I think the real difference is only a matter of style. And, as Chris McCord showed in Metaprogramming Elixir the infinite for comprehension style is more readable and illustrative in some situations.