3 ms·
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 S
by elcritch 7y ago
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 7y agoGood 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 7y agoIt 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 7y ago> 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!