Earlier this year I spent a few weeks playing with Erlang. I wanted to make something out of it, but despite an encouraging start I found it too frustrating to use.
I got excited about Erlang because a lot of interesting things have been done in Erlang. Like CouchDB, RabbitMQ, Riak and so forth. Besides that, Erlang is a dynamic language and I generally find those quite nice to use.
I won't recap Erlang's selling points here - I assume you've heard them. These are my discoveries.
- The language feels very static. Dynamic type checking and code reloading are about the only things that seem dynamic about it. There is no introspection. The build system is make. You can't change the structure of a record once the program is running. There is no meta programming. And so on and so forth. It really isn't a dynamic language at all in the sense of Python, Ruby or Javascript.
- ...some of these static restrictions are plain weird. If you want to call a function in the same module you don't have to do anything special, but if you're going to export the function you need to declare it as an export as func/2 (where 2 is its arity), and maintaining consistency between the function definition and the export declaration is another source of bugs. The question is: what's the point of having this? The arity is a kind of statically enforced type, but you can still call the function with any arguments you want - you can even give all your functions a single parameter and then call it with a tuple and Erlang has nothing to say about what that tuple contains.
- The data abstraction is very weak. There is no object system - all you have is records and they are compile time defined and provided to client code as header files, like in c. This is very rigid.
If you don't like records you can just use tuples. Tuples don't have to be declared in a header file and compiled in, just start using them freely. But now you have a container with positional arguments and if you want to change the structure of the tuple you have to update all your code, because any code using this tuple has to pattern match against all of its fields. That's even worse. - The syntax is Prolog and that's not a good thing. I didn't even realize how much it is Prolog until I read a Prolog tutorial a few weeks ago. It may be alright for Prolog, but it has definitely not been extended in an elegant way by Erlang. The tuple syntax is probably the worst part of it, but control flow structures too are so easy to get wrong with so many different line termination characters in use.
- Strings are just lists, and there's no way to detect whether a list merely contains integers or whether it's supposed to be a string. If you wanted to be very generous you could call this sloppy.
- Single assignment may be a nice idea, but it makes code ugly. At first I was naming my variables things like Something = func(OriginalSomething), but then I realized I had to plan very carefully what I would name the variable to always have a descriptive name. So I abandoned that and started using Houses2 = func(Houses) and so on. That is a bit more flexible at least, but now it's like I'm maintaining a list in sorted order and if I discover that I need to add a step in between Houses3 = func(Houses4) I can either introduce Houses7 or I have to update all the indexes. This plain sucks. (And no: Haskell's solution of adding apostrophes is just a different numbering syntax, it doesn't make it any better.)
- Dynamic typing + pattern matching can make a mess. I was looking at ibrowse, which is a very feature rich and mature library. But some of its function definitions are 3 pages long. Instead of splitting it up into several helper functions, it's simply defining the same function with many different clauses, each of which has a different signature (obviously), but also a different arity and sometimes expecting different types for those positional arguments.
This is of no concern to the caller, because I just call the top level clause of this function and I don't see what happens behind the scenes. But what you actually end up with in the background is a call graph of essentially different functions (but with the same name), heavily recursive, that is hell to try to make sense of. And because there is no static type information, well good luck. - OTP is a straightjacket. OTP kind of assumes that you will be using a supervisor, that you will be using gen_server and that you will be using OTP code patterns, OTP packaging and an OTP directory layout. I found it quite hard to write a program in a single module just to try out an idea first. I would later have migrated it to OTP style, but it was hard to get the benefits of OTP without going all the way in. What is the point of a supervisor that supervises a single process without restart behavior? It's pure overhead. Yet in OTP everything is supervised, whether or not that's useful. Also, the lack of a proper data abstraction is nowhere more painful than in OTP where you're constantly passing nested tuples around and trying to figure out which part is used by the function you're calling directly, and which part is propagated as part of the message payload. If you happen to know the OTP API like the back of your hand then this is probably a non-issue, but you can say that about any awful API.
- The tooling is low quality. It makes you think noone has made a real effort to improve it in a good 10-15 years. Like the REPL. There is one, but it sucks. It doesn't support readline. It has tab completion, but it doesn't complete all function names. You think you can explore the libraries this way, but some modules don't show up at all in the completer, or some of their functions are missing. The same goes for all the process introspection tools. They exist, but they've been programmed against Motif or something and they're terrible to use. And stack traces are formatted in a weird way that makes it hard to see exactly what the failing thing is. And if you use sasl then the actual output you care about it drowned out in a bunch of other output that you don't care about.
So yes, several very interesting ideas in Erlang, but a poor development experience. Unfortunately, most of these problems are deep in the language and the culture of Erlang and not likely to ever change. Someone could write a better REPL, but it would take a lot of community work to improve records or the syntax. There are other problems in Erlang that are probably more tractable, like a certain amount of duplication of functionality in the standard library (several competing dictionary implementations), but not even that seems to be worked on.
UPDATE: Erik Ridderby has written a response that includes a fascinating piece of history painting a nice picture of where Erlang came from and what sort of development environments it was used in.