My favorite Erlang Program (2013)
joearms.github.io
joearms.github.io
I remember having a coworker who was in love with Akka. He was trying to sell everyone on it, despite my team's having a strong Erlang presence. He showed us Akka's "become", talking about how powerful that could be, and how Erlang didn't have anything comparable. He didn't seem to realize that Akka's needing a special keyword for it was actually a limitation rather than a feature, that Erlang didn't have a special keyword to allow you to do it because it didn't -need- a special keyword to allow you to do it. That's not a dig at Akka, or Scala (it's an impressive language in its own right), but rather to point out how sometimes we miss the complex capabilities a simple set of abstractions can give us.
Also to add onto yetihehe's comment, it's -really freaking easy- to debug such a server as Joe illustrates, or anything else that behaves in such a way (changing behavior by calling another function, to eventually wind up in a different receive statement), in Erlang. It's effectively an FSM determining what kind of messages you handle, the stack traces are clean (TCO means any time you get an exception, it's only in the current 'state'); the only complexities are the fact you're building an FSM, i.e., making sure your transitions are right, and what to do about out of order messages (i.e., a message that doesn't match the current state). There's nothing specific about the Erlang code that makes it harder to debug than the Akka code; in fact, just the fact it's pure Erlang makes it easier; your stack traces are going to be just your own code, no libraries muddying them up.
As for demarcation...it's not really any different. That is, Scala's
def func:Receive = {
//cases
}
def receive = {
case "foo" => become(func)
}
is equivalent to Erlang's func() ->
receive
%cases
end.
receive
"foo" -> func()
end.
But you'll note that in Joe's example he even uses the atom 'become' to indicate the transition is happening, i.e., receive {become, F} => F() end). It's totally equivalent. I'm not saying Scala is less clean, or whathaveyou, just that Akka felt they had to create a new keyword, for some reason, rather than leave it up to the user to implement. Exactly why, or whether it's equivalent to calling a function, I don't know (having never seen an example of the latter, nor commentary on its characteristics re: call stack); my point was simply that it's a natural extension of the language fundamentals in Erlang, but Akka felt the need to create a new keyword just for it, and that people often miss that needed complexity can arise out of a solid set of fundamentals.All that said, I think I'm going to stick around with these languages. Everything feels very well designed and battle ready, in stark contrast to using JS. Elixir also seems to remove a lot of the cruft and awkward syntax of Erlang, and having Lisp-style macros makes the GenServer boilerplate go away entirely.
For example, the other day, I was looking at filtering a next parameter to remove the domain... however it's unnecessary as redirect/2 in phoenix if passed a URL rather than a path will throw an error by default. As an aside different exceptions will return different HTTP error codes - for example a database not found exception from Ecto will cause a 404. There are hundreds or thousands of these little decisions that are the correct/simpler decision.
Elixir and Phoenix, for want of a better way of saying it, show great programming taste. So to say it's a coat of paint is a bit harsh; it's the right coat of paint, in my opinion.
Fair enough. On rereading my comment, it came off more brash than I intended. I completely agree with this statement.
It is amazing what kind of middleware bloat is forced unto people these days.
Overall though, it was a total blast the other day to just run 10 000 processes so easily, within milliseconds, and watch all my cores go to max without messing with threads or even the (somewhat obtuse) Go channels. Actors really are easy to reason about. I like this model so much that I'm even looking at it for C (C++ Actor Framework).
[0]: https://github.com/elixir-lang/elixir/blob/master/lib/elixir...
This isn't in any way to talk negative about Joe Armstrong or Erlang, but more to show how cool the language is: even non-idiomatic code scales and is wonderful.
For instance, I had an instance where I needed to serialize requests out to an external piece of hardware. Various user or system events would determine hardware control events that needed to be sent out, and then responses needed to be listened for to determine if they were successful or not, and return that error the user. Essentially an asynchronous, but serial, process. I had a gen_server to synchronize access to the hardware.
Now, the way I implemented this was, with each call that came into the gen_server, I'd send a message via gen_tcp, and then listen for the response as a raw receive inside of the same call. The alternative, to stay in the gen_* structure, would have been to send and finish, and then handle the response in the handle_info (since gen_* is coming back as a raw message). But that would have allowed more sends to occur in between the initial send and getting a response back, which would break the serialization we needed to ensure, and would have lost the reference to who had sent the initial request (since I was reusing the socket, and the acknowledgements from the hardware didn't include any sort of session), making it impossible for me to respond properly to the original caller.
So with a raw receive in there, it effectively became like a 'become' server, in that at the top it was a gen_server that implemented a gen_tcp "send" server, and then after sending it temporarily took on the characteristics of a gen_tcp "listen" server via a raw request (though obviously it was listening on the socket even before sending), before reverting back to the "send" server that waited for the next thing to send.
In fact, as I recall, while we were serializing our sends, there were certain types of events being broadcast by the hardware that we needed to drop even in the raw receive, so it was recursive. That is, our send server became a listen server, and stayed as a listen server until either we got the response we wanted, or a timeout was hit, at which point we'd revert back to the send server.
Thanks for sharing this. It gives me more confidence in what we have done!
(I'm about two-thirds joking)