Yesterday I was invited to do an interview with the man behind Erlang Punch, Mathieu Kerjouan. Mathieu is a truly great programmer, an absolutely excellent interviewer, and it was a lot of fun to get to do something task-focused with him like this. He asked some pretty broad questions about Erlang as a language, as an ecosystem, and its role in my professional development and journey as a programmer.
He asked what I like and what I don’t like about the language, and as anyone who knows me well as a programmer may imagine, I gave a rather extensive answer. In the course of giving my answer the main theme that emerged was that Erlang’s greatest trait as a language is its simplicity and consistency, and the worst parts are where this simplicity and consistency is violated. Most of the complexity of the system is actually in OTP as a system, rather than Erlang itself as a language, and this seems to be a profoundly beneficial tradeoff.
One point of inconsistency I mentioned as a dislike was the the shadowing of variable names declared in an outer scope in the heads of lambda functions, and another was the introduction of the alien ?=
operator instead of relying on functional constructs that are far more flexible (such as adding a pipeline library module to the standard library). I went into some detail in my answer about the exact reasons why these are examples of stupid warts in the language and the history behind them so I won’t recount them here, but it is sufficient to say that there is a cardinal sin being committed in these two cases: the crime of inconsistency.
To expand on that point a bit, I would like to illustrate briefly how it would actually be possible to reduce Erlang’s syntax to bring even further consistency to it. I’m not saying this is something I’m going to write an EEP about any time soon (the juice just isn’t worth the squeeze), but it is illustrative of how easily a minimalist approach can be applied to language design in ways that comply with core elements of the language’s existing fundamentals. In fact the point of thinking this way is to explore and discover what those fundamentals might actually be as this is not always immediately obvious, even to someone experienced with using it or (ironically) the language’s original designer.
Let’s consider the syntax of Erlang’s “send” and “receive” operations.
Currently there is a magic token !
for “send” and another magic token, the word receive
, for “receive”. Imagine if instead of these magical tokens we had the following functions as BIFs:
send(Recipient, Message) ->
Recipient ! Message,
ok.
receive() ->
receive Any -> Any end.
receive(Timeout) ->
receive
Any -> {ok, Any};
after Timeout -> timeout
end.
Suddenly we realize that these could actually be BIFs, privileged with whatever magical implementation or compilation cheats we might come up with, and invoked very simply as:
ok = send(Recipient, Message),
Message = receive(),
case receive(10000) of
{ok, Message} -> % …
timeout -> % …
end,
It would be possible to implement the above functions exactly as demonstrated above and use them without any interruption or loss of meaning. This would reduce the syntactic complexity of the language, and in fact in the case of send/2
there actually is a function in the standard library that has turned the magic !
syntax into an optional function for exactly this reason: because it is useful in a functional context to have functions with which to work!
Is there anything else we can (and in a better world, probably should) adapt in this way?
Why yes.
Yes, in fact there is: the notorious try
which we somehow got along just fine without once upon a time…
try(Operation) ->
try {ok, Operation()}
catch C:E:S -> {C, E, S}
end.
This could be written any number of other ways (in the old days this would have been speculative execution in a monitored process spawned specifically for this reason, for example, which you can still do and parallel map implementations actually do), and of course we would want to make it into a magic BIF almost certainly, but my point here is that the complexity can be pushed back into the compiler and system rather than remaining a point of syntactic complexity that has its own little magic (and totally unique) notation that even graybeards occasionally forget the details of and have to look up on a cheatsheet from time to time (“wait, can we still call a function to get a stacktrace or is that in the… ohhhh yeah, it’s a syntax thing now… [typing noises]”).
Protip for language designers: the more things a programmer has to remember about your language’s little quirks the less mental bandwidth they have remaining for whatever problem they are actually trying to solve.
Why even have such inconsistency in the language when other features of the language that take advantage the existing syntax so well could simply have been implemented instead (and often retrospectively actually are, such as with send/2
)?
In a word: habit
This is a bad habit. Languages in general, not just Erlang, would benefit greatly from a reduction in magic syntax and glyphy operators and we should move to minimize languages rather than add every allegedly “good idea” that the Fairy of Warped Ego poops out on our shoulders.
Try it out…
It’s really not all that insane.
C’mon, man, everyone’s doing it…
-module(should_be_bifs).
-export([send/2,
recv/0,
recv/1,
tryy/1]).
send(Recipient, Message) ->
Recipient ! Message,
ok.
recv() ->
receive Any -> Any end.
recv(Timeout) ->
receive
Any -> {ok, Any}
after Timeout -> timeout
end.
tryy(Operation) ->
try {ok, Operation()}
catch C:E:S -> {C, E, S}
end.
How does your proposal solve selective `receive`? The code you suggested always picks any message, with zero control over the filter. Sure, you can pattern match on it later and optionally put it back into the queue, but by doing so you possibly loose some ordering invariants, and spinlock if waiting on a message which hasn’t been delivered yet.
I suggested a syntax and merely showed a code example.
Your criticisms of the code example are accurate, of course, there is no special filtering of the mailbox and to really nail the current behavior would require implementing a (far less efficient) second queue layer, which is silly. But that was not the point of this post. The point was reduction rather than addition of syntax to the language.
The whole point is that this function could be a BIF with a special behavior rather than add extra syntax to the language.
…hmmm… Come to think of it with additional optional arguments the selective receive behavior could be implemented even in the function, but I’m not overwhelmingly interested in that. The point was more syntax reduction than anything else.
EDIT: BTW, sorry about the delay. Evidently the current cache settings and the spam filter have some weird interaction that makes things not show up until the cache is wiped. One more reason I should move back to self-hosting eventually…
Okay, I see your point. However, I would argue that expanding syntax for cases like try and receive is a good thing (i.e. more is actually more). The difference between them and normal functions you want try&receive to pretend to be is that they follow non-standard semantics regarding control flow. In my opinion, by reducing syntax this way you are making code paradoxally more implicit, as you hide the fact that some “functions” have distinct operational features (this is different from a function doing “magic” stuff behind the scenes to compute the result, like in the case of send or get/put). Different syntax indicates that some funky stuff is going on regarding where the instruction pointer is going to land – denying it in the syntax is what I would label as a “crime of inconsistency”. Sure, it is fine to wrap some of their uses in functions, although as a situational feature, not ultimate replacement.
You can argue that with sufficient abuse of explicit continuation lambdas you can make anything “pure”. My concern is that this syntax facism would quickly reduce to lambda calculus or, more yucky, lisp, where you practically write denotational semantics in CPS. You also don’t seem to be concerned with commas – they can be implemented with functions too, right?
Having a bit boring time in the airplane, I got some fantasies about it. Receive may take a filtering function which would return {ok, Thing} or ‘nah’ to skip a message. Could work, but I doubt it would be cleaner than the current syntax. It would also have nasty edge cases (what if I store a message in the process dictionary and then return ‘nah’? Or send a message to myself while filtering?). Try/catch/throw is a funny case, because it stays in opposition to many “sane” behaviors. First just to mention that try would have to take a lambda, because you don’t want a function to be suddenly lazy. That would be inconsistent, wouldn’t it?
longjmpthrow is even funnier to think about. You could take some inspiration from Scheme and embrace CallCC, but come on, this is not cleaner by any means. It also does not fully solve the problem, just pushes it down towards hell.Tldr I see that if a language feature computes something in a parameters-to-value manner, it can be beneficial to pretend it’s just a normal function. But if that feature makes the execution perform a 270° salto, then I would prefer to have it as a distinct syntax construct.
Or maybe I misunderstood your point
No! You’re thinking in exactly the right way, just perhaps missing that I’m not seriously recommending we do away with
receive
but rather using it as an example. I am however stating, not merely suggesting, that getting rid of the magic?=
nonsense would be a good thing, and if the stdlib needs a pipeline module, then it should have a pipeline module. I am also stating flat out that the “pinning operator” suggestion for lamdba head masking/matching is pure poison and matching trumps masking in every case there.These are the things that should be thought about very carefully before allowing whatever the Good Idea Fairy may have pooped out onto one’s shoulder to consume one’s mind so fully they wind up on a screw-the-plebs quest to add new syntax to a language every time any idea pops into their head!
The idea “I want to just do X” hides a dangerous use of the word “just”.
I do see some merit in the implementation of
receive
, in particular, as a special syntactic form. And since you bring up “ewww lisp”… lisp has a few “special forms” and wherever you encounter this you really are looking at a core part of the language, andreceive
in particular can definitely be viewed this way in Erlang. I disagree that implementing receive as a special function would be committing a major crime in terms of surprise, as we absolutely cannot give every single semi-magical feature of Erlang a special syntax. (Should ETS, NIFs, signal handling, exits, etc. all have their own syntax? How about every new idea we come up with in the future that involves signal handling? Process dictionary access? Persistent terms? Obviously the addition of new syntax merely to indicate that the runtime is going to do something special here under the hood has a limit.)And that’s my point: there are limits to adding new syntax.
This is the third time I’ve seen a wave of new language designers get obsessed with adding new syntax for everything (late 1980’s through 1990’s, late 1990’s/2000’s, and from around 2016-ish to now). It is simply a part of the waterbed problem in complexity management, and Erlang’s current syntax (to include
receive
as-is) seems to be a pretty solid example of having just enough complexity to be quite expressive (if you take care to structure your data properly in the first place, which rarely happens with unmanaged code, which is a whole different subject), and not much more. Fiddling with that balance is dangerous.try
is still evil, though.My whole point was to exercise a way of thinking, not to make concrete recommendations about Erlang, specifically.
Cardinal rule: Before thinking “what cool syntax can I add?” a language designer should think “what would this look like as a function call, unification with the rest of the language, or similar, and would that be cleaner?”