While working on the application of this Elvis rule to a very large project that was recently upgraded from OTP24 to OTP28 I found tons of examples of something like this in it…
[ do:something(X) || X <- …some list… ]
…which is something that can obviously be rewritten as [ do:something(X) || X <:- …some list… ] since we’re not filtering anything in that generator. But (since it doesn’t include any filters and the expression is just the application of a named function), it can also be written as…
lists:map(fun do:something/1, …some list…); or…
lists:foreach(fun do:something/1, …some list…) (if the results are discarded anyway).
So, what do you think fellow Erlangers? Which of the three syntaxes is preferable and why? How would you write such a thing?
For simple cases I generally opt for lists:map() and lists:foreach(). Using these functions has the benefit of making it more clear if the result list is intended to be used or not.
lists:foreach() also has the nice benefit of not building a new result list, which saves a bunch of unnecessary memory allocations and GC work.
For more complex list processing I tend to write recursive functions with multiple clauses and possibly multiple functions, as this makes it easier to make sense of what is going on.
I also use lists:foldl() reasonably frequently, when the code needs both a filter() and map() and where the result (the acumulator) is something else than a list.
There is no need to use a strict generator if the generator binds to a variable. I suppose you could have a rule to always use it for new code anyway, but it is unnecessary to rewrite old code.
I always prefer a list comprehension. The example above I would write like:
[do:something(X) || X <- …some list…]
If the value is not used, I write like this:
_ = [do:something(X) || X <- …some list…],
…more code…
When the result of the list comprehension is clearly not used, the compiler will not emit the code that builds the list. (This optimization was introduced a long time ago.)
If the list comprehension is at the end of a function and its value should be discarded, I write like this:
_ = [do:something(X) || X <- …some list…],
ok.
I personally find list comprehensions easier to read than the corresponding lists functions, and the lists functions force more indentation. I like to keep my lines reasonable short.
How I would write such a thing depends on the programming language I’m using.
Some, like Smalltalk and SML, do not have list comprehension syntax, so I’d
write the equivalent of the lists:map/2 or lists:foreach version without
thinking twice about it. In InterLISP or Common Lisp I might well use their
special loop syntaxes. In Erlang, I’d probably use
[… || …] rather than lists:map/2 and
_ = [… || …] rather than lists:foreach/2.
One issue for me is that I find the argument order of lists:map/2 and lists:foreach/2
(function then list) to be, let’s be polite, suboptimal. In a language written in a
left-to-write script, the “heavy” constituent should go on the right and the “light”
go on the left. There just doesn’t seem to be an attractive way to lay out a call
to a higher-order function with the function as the first argument.
For performance, there’s nothing to choose between them.
For readability, I would rather read a list comprehension.
The fundamental problem with list comprehensions in Erlang is that
they cannot be used more generally, as “sequence expressions” can in F#.
Given that list comprehensions can only do (possibly nested) maps and filters,
while folds are rather common too,
there’s an argument to be made for consistently using function call syntax.
I once proposed a syntax like
(Sum where Sum = 0 then Sum + X || X ← List)
basically a hybrid of Lisp’s “do” and list comprehensions. That would have
filled the fold gap nicely. Oh well.
One could have a lot of fun debating this particular style issue, but
practically speaking, it really doesn’t matter all that much and you have
to be able to read both.
Assuming the results are discarded I would historically have used lists:foreach() as this exactly matches the idiomatic use case of lists:foreach(). However, I do prefer the list comprehension from the conciseness perspective.
As soon as you have add additional code to force the compiler to do what you want (i.e. the _ = ..., ok.), there becomes a more compelling argument that foreach() places a lower cognitive strain on the reader (as opposed to the writer). As neatly illustrated in the preceding post!