
Functional Imperative Programming with Elixir (2018) - type0
https://bitwalker.org/posts/2018-03-18-functional-imperative-programming-with-elixir/
======
elcritch
Fun to see someone make a complete while-loop macro. Occasionally I get
tempted to make one, but almost always find it's better to rethink a while
loop into a Stream or just a for loop if possible to keep my logic in-place.

Take:

    
    
        while(fn -> Node.ping(some_node) != :ping end, 30_000)
    

Could be written:

    
    
        Stream.unfold(:pang, fn
           :pang -> {:some_node, Node.ping(:"some_node")}
           :ping -> nil
        end) 
        |> Stream.zip(Stream.interval(1_000)) 
        |> Stream.take_while(fn {_, ts_cnt} -> ts_cnt < 30 end) 
        |> Stream.run() 
    

Which in Python would probably be something like (not using the Walrus, and
made up system lib names):

    
    
        node_response = Node.ping("some_node")
        start_time = Time.millis()
        while node_response == "pang":
           node_response = Node.ping("some_node")
           if Time.time() - start_time > 30000:
              break
           Time.sleep(1000)
    

The Python version is pretty simple, but really isn't any shorter. It also
intertwines the timing logic and node ping actions. Although it takes a bit of
practice to become fluent with the stream approach, it's much more compose-
able. Take the example in the article where you end up wanting to run a bunch
of functions with a timeout on some interval, you can cut the middle 2 lines
into a new function in a utility library. Then you could rewrite the code like
(notice it's just cutting and pasting complete lines):

    
    
        Stream.unfold(:pang, fn
           :pang -> {:"some_node", Node.ping(:"some_node")}
           :ping -> nil
        end)
        |> MyUtils.with_time(max: 30_000, every: 1_000)
        |> MyUtils.log_max_ping_count() 
        |> Stream.run()
    

Usually re-writing something into this form makes me think of many programming
tasks as a set of discrete steps. Do the first stream, then add the second
step, and so forth. This tends to make it easier to dive back into code after
a while as each step has to be incremental. Also a side note, if you _really_
want to cheat and need to use a mutable variable, you can use (abuse?)
`Process.get/1` and `Process.put/2` with a for-loop. CouchDB uses a similar
idea to create stateful iterators if I recall the code correctly.

~~~
bitwalker
Good points! The time where the `while` loop is actually better is very
situational, but when it is a better fit, it is definitely handy to have in
your back pocket. But more generally I just think it is useful to know how you
can implement language constructs like this in Elixir with nothing more than
the primitives it provides, some macro glue, and a bit of creativity :)

A couple things though; one issue with your first example is that you incur a
1s wait for either success or failure conditions, where what you typically
want is for only the failure condition to impose that cost. Luckily, you can
keep this approach and fix it by removing `Stream.zip(Stream.interval(1_000))`
and replacing it with `Stream.intersperse(Stream.interval(1_000))`.

The other problem though is that you end up incurring quite a few additional
function calls vs the `while`, since the `while` inlines the predicate and
associated machinery. You end up with precisely one function call per
iteration, with the ability to exit early on failure or timeout, whereas the
function-based form is going to incur potentially several function calls,
depending on how the `Stream` is being constructed - if I recall
implementation details correctly, your first example would be a minimum of 3
calls per iteration; one for the unfold, one for the interval, and one for the
take_while.

Of course, that's all really just optimization, and your `Stream` based
solution is perfectly fine in general. The only additional benefit (IMO) to
the `while` form, is that it reads very succinctly, and due to most being
familiar with imperative constructs like that, they can read it with virtually
no effort, where reading the `Stream` form takes some care to understand what
you get at each step, and what it produces in the end.

RE: Mutability, I think I mentioned it in the post, but I really didn't want
_actual_ mutability, just the appearance of it. So while one could use the
process dictionary, or ETS, or even another process, to get something like
"real" mutability, that kind of defeats the point ;)

~~~
elcritch
It was a good read! Especially since you went through all the details.
Handling different inputs and alternates like `after` are a bit tricky. And
you're right on the downsides of the "stream" style. There's a bit of overhead
compared to the macro form which can inline things a bit. It also takes a
couple of more "cognitive" cycles to write, but usually I can read the results
more quickly later.

In general I do wish Elixir provided a `reduce` macro similar to the for-loop
one with the ability to halt. While-loop logic is easier to refashion as a
reduce but it's harder to recall the argument orders for various Enum.reduce
forms.

Do you use this while-macro in production code or just test code? Yah the
"real" mutability tricks are more for those wondering if you _can_ do it.
Sometimes it's not obvious, but one of the nice things in BEAM is that you can
cheat.

P.S. thanks for all of your Elixir tools!

~~~
bitwalker
> Do you use this while-macro in production code or just test code?

I use the `while` macro in the Distillery tests, since it made expressing
certain tests much less verbose - namely those dealing with building up a
release, spinning it up in a new node, and then verifying conditions on the
running node meet expectations. In general though, I don't. I haven't needed
it elsewhere (well, I have one other library I have considered using it in,
but haven't yet). I would be hesitant to introduce it without a clear need,
just because it would likely catch other developers off guard unless clearly
documented.

To be clear though, I would support something like it in the language, but
also understand why it isn't there; there just isn't a clear enough need, and
as you've demonstrated, `Stream` can more or less be used to cover the general
cases.

> P.S. thanks for all of your Elixir tools!

Thanks for the kind words!

------
tutfbhuf
Imperative Programming in Haskell with Python and JS comparisons:
[https://beuke.org/pure-imperative-haskell/](https://beuke.org/pure-
imperative-haskell/)

------
HorizonXP
I think this was a fantastic blog post that really showed off the power of
macros. It's the one area of Elixir I haven't tried to delve into. Being able
to see the author's thought process really helped me understand what was
happening too. Kudos!

~~~
bitwalker
Thanks! I don't write very often, since I don't really know if people get
anything out of it, but feedback like this is definitely encouraging :)

~~~
HorizonXP
I need to start writing too. I just subscribed to your RSS feed.

------
wwright
Haskell has monadic loops ([https://hackage.haskell.org/package/monad-
loops-0.4.3/docs/C...](https://hackage.haskell.org/package/monad-
loops-0.4.3/docs/Control-Monad-Loops.html)):

    
    
        let limit = secondsToDiffTime 60
        start <- getCurrentTime
    
        iterateUntil id $ andM 
          [ someApplicationLogicReturningBoolean
          , do    
            now <- getCurrentTime
            return $ (diffUtcTime start now) >= limit
          ]
    

This is a really rough sketch from someone who just looked all this up, of
course :-)

Recursion is probably still much more natural here.

------
galaxyLogic
Seems quite complicated. Why not just use while-loop? :-)

(in other words a language which provides it)

~~~
sixstring982
Or just use recursion?

But hey, this article is an interesting way to get around that!

~~~
bitwalker
Well, I pointed out at the beginning that one can use recursion, and the
implementation of the `while` construct itself uses recursion as well (in
fact, you can't implement `while` without it in Elixir).

In any case, expressing the equivalent of a `while` loop with predicates and
timeouts is quite syntactically noisy in Elixir - it is certainly doable, but
much less clear than the imperative equivalent.

