What Sucks About Erlang
There are the languages everyone complains about, and there are the languages no one uses.
Having said that, it's time to whine about my favorite language I use quite extensively. Erlang, I love ya, but we need to have a word.
Basic Syntax
Erlang is based originally on Prolog, a logic programming language that was briefly hot in the 80's. Surely you've seen other languages based on Prolog, right? No? Why not? Because Prolog sucks ass for building entire applications. But that hasn't deterred Erlang from stealing it's dynamite syntax.
The problem is, unlike the C and Algol based languages, Erlang's syntax does away with nested statement terminators and instead uses expression separators everywhere. Lisp suffers the same problem, but Erlang doesn't have the interesting properties of a completely uniform syntax and powerful macro system to redeem itself. Sometimes a flaw is really a strength. And sometimes it's just a flaw.
Because Erlang's expression terminators vary by context, editing code is much harder than conventional languages. Refactoring -- cutting and pasting and moving code around -- is particularly hard to do without creating a bunch of syntax errors.
Consider this code:
blah(true) ->
foo(),
bar();
blah(false) ->
baz().
Lets say I want to reorder the branches:
blah(false) ->
baz();
blah(true) ->
foo(),
bar().
Or change the order which foo() and bar() are called.
blah(true) ->
bar(),
foo();
blah(false) ->
baz().
What's the problem? Note in each example the bar() lines, they have a different character ending each line: bar(); bar(), bar().
In Algol based languages the statement terminators are the same everywhere (usually semi-colon, newline or both). The Javascript version:
function blah(flag) {
if (flag) {
bar();
foo();
} else {
baz();
}
}
Erlang expression separators vary context to context and it's simply more mental work to get right.
If Expressions
You might think if branching as something that no language could ever get wrong. Doesn't seem possible does it? I was young once too.
The first problem is that every time an if executes it should match at least one of the conditional expression branches. When it does not, an exception is thrown.
In this example:
if
X == foo ->
foo();
X == bar ->
bar()
end
X must be foo or bar, or an if_clause exception is thrown.
This sorta makes some sense since the if's aren't statements like in C language family, they are more like the C ternary operator (x == foo ? foo() : bar()) and so must return a value to the caller.
The problem is it prevents simple code like this:
if
Logging ->
log("Something happened")
end
Because if Logging is false, the if throws an if_clause exception!
Instead you are forced to do something like this:
The only purpose of the
if
Logging ->
log("Something happened");
true -> ok
end
true -> ok line is to give it an else condition to match. That weird taste in the back of your throat? It's probably vomit.
Erlang ifs could be so much more useful if it would just return a sensible default when no conditionals match, like an empty list [] or the undefined atom. But instead it blows up with an exception. If Erlang were a side-effect free functional language, such a restriction would make sense. But it's not side effect free, so instead it's idiotic and painful.
It gets worse. You cannot even call user defined functions in if conditional expressions! For example, this won't even compile because of the call to user defined should_foo(X):
should_foo(X) ->
X == foo.
bar() ->
if
should_foo(X) -> % compile error on this line!
foo();
true -> ok
end.
This limitation is due to Erlang's "when clause" pattern matching engine, which needs certain guarantees from the expressions for static optimization. Erlang allows a subset of the built-in functions (BIFs) in conditional expressions, but no user defined functions can be called whatsoever.
How can a language have butchered the if and still be useful? Well, fortunately case expressions in Erlang are powerful and damn useful, and a decent substitute for most uses of if:
case should_foo(X) of
true -> foo();
false -> ok
end
But like if expressions, case expression also have the limitation that it must match at least one conditional or an exception is thrown. Bleh.
You Say String of Characters, I Say List of Integers
The most obvious problem Erlang has for applications is sucky string handling. In Erlang, there is no string type, strings are just a list of integers, each integer being an encoded character value in the string.
It's not all bad. It has the benefit of taking the same built-in list operations, libraries and optimizations and reusing them for string processing. But it also means you can't distinguish easily at runtime between a string and a list, and especially between a string and a list of integers.
From the Erlang Console you get this:
1> [100,111,103] == "dog".
true
Erlang string operations are just not as simple or easy as most languages with integrated string types. I personally wouldn't pick Erlang for most front-end web application work. I'd probably choose PHP or Python, or some other scripting language with integrated string handling.
Functional Programming Mismatch
Erlang has been a great fit for CouchDB, a network database server. Once I got over Erlang's weirdness and accepted its warts, I almost couldn't imagine using anything else. So much of the code seems to want to be expressed in a recursive, functional manner and the lightweight, shared nothing concurrency is a great match for network servers and database internals. The code is typically much more compact, elegant and reliable than it would be in more conventional languages.
But when it came time to write the test suite code for CouchDB, I found Erlang to be needlessly cumbersome, verbose and inflexible.
Immutable variables in Erlang are hard to deal with when you have code that tends to change a lot, like user application code, where you are often performing a bunch of arbitrary steps that need to be changed as needs evolve.
In C, lets say you have some code:
int f(int x) {
x = foo(x);
x = bar(x);
return baz(x);
}
And you want to add a new step in the function:
int f(int x) {
x = foo(x);
x = fab(x);
x = bar(x);
return baz(x);
}
Only one line needs editing,
Consider the Erlang equivalent:
f(X) ->
X1 = foo(X),
X2 = bar(X1),
baz(X2).
Now you want to add a new step, which requires editing every variable thereafter:
f(X) ->
X1 = foo(X),
X2 = fab(X1),
X3 = bar(X2),
baz(X3).
Erlang's context dependent expression separators and immutable variables end up being huge liabilities for certain types of code, and the result is far more line edits for otherwise simple code changes. For the CouchDB test suite I could feel the syntax fighting me at every turn. When I switched over to writing the tests in Javascript, things just flowed faster and edits were easier.
Erlang wasn't a good match for tests and for the same reasons I don't think it's a good match for front-end web applications.
Records
The "records" feature provides a C-ish structure facility, but it's surprisingly limited and verbose, requiring you to state the type of the record for each reference in the code.
Not once for each variable binding, but you must state the variable's type each and every place a member of the variable record is referenced.
-record(foo, {
a=0,
b=0,
c=0}).
bar(F) ->
baz1(F#foo.a),
baz2(F#foo.b),
F#foo{c=F#foo.c + 1}.
Each of those F#foo is a statement that says "I'm a record variable of type foo". And it's not enough to say it once. We must say it over and over again each time we use it.
Here is a more idiomatic use of records, which uses pattern matching to extract members into local variables:
bar(#foo{a=A,b=B,c=C}=F) ->
baz1(A),
baz2(B),
F#foo{c=C + 1}.
Which is still noisy compared to the equivalent in C:
struct foo {
int a;
int b;
int c;
}
foo bar(foo f) {
baz1(f.a);
baz2(f.b);
f.c += 1;
return f;
}
Another problem is records often feel like a tacked-on hack. They are compile-time static and record members cannot be added or removed at runtime, and don't fit with Erlang's otherwise dynamic nature.
Records are a compile time feature -- not a VM feature -- and are statically compiled down to regular tuples, with the first slot holding the record name atom, and each slot N + 1 corresponding to the Nth entry in record declaration. At compile time the record member references are converted to integer offsets for tuple operations.
The most noticeable problem is they aren't usable from the REPL command line, it won't accept record syntax without special steps and it still doesn't show you result records in record syntax. Same when debugging and dumping stack traces and symbols, the records always look like the tuples they are under the covers, requiring you to mentally decode which tuple slot corresponds to a record member. Erlang records give you most of the penalties of static typing with very little of the benefit.
Give me memory, or give me death!
Update: On OS X with the most recent Erlang VM (R12B-1, emulator version 5.6.1), I can no longer reproduce this problem. Yay!
With CouchDB we discovered the hard way how Erlang handles memory allocation errors from the OS:
exit(1);
When the VM cannot get memory from the OS, it just commits hara-kiri. It doesn't just kill the virtual Erlang "process" that needs the memory. It kills the whole VM, taking along any child OS processes with it. But at least it's an honorable death.
See for yourself, try this at the Erlang console:
So no problem you might think, given Erlang's robust, fail-fast and restart nature, it will restart itself automatically and barely miss a beat. Well, that's what I thought, but then I'm generally a positive guy.
Eshell V5.5.3 (abort with ^G)
1> <<0:429967295>>.
beam(722,0xa000d000) malloc: *** vm_allocate(size=1782579200) failed (error code=3)
[....snip stack trace....]
Crash dump was written to: erl_crash.dump
eheap_alloc: Cannot allocate 1781763260 bytes of memory (of type "heap").
Abort trap
Nope, Erlang won't restart itself automatically, that's something you have to build all by yourself. The only solution we've found is to create a parent watchdog process to monitor the VM and restart it if it crashes.
The built-in "heart" child OS process, whose job it is to monitor for an unresponsive Erlang VM and restart it, is also killed when the VM exits. So we have to roll our own "restart the dead VM" solution and deal with cross-platform issues providing something I'm still shocked Erlang can't handle itself.
Code Organization
The only code organization offered is the source file module, there are no classes or namespaces. I'm no OO fanatic (not anymore), but I do see the real value it has: code organization.
Every time time you need to create something resembling a class (like an OTP generic process), you have to create whole Erlang file module, which means a whole new source file with a copyright banner plus the Erlang cruft at the top of each source file, and then it must be added to build system and source control. The extra file creation artificially spreads out the code over the file system, making things harder to follow.
What I wish for is a simple class facility. I don't need inheritance or virtual methods or static checking or monkey patching. I'd just like some encapsulation, the ability to say here is a hunk of data and you can use these methods to taste the tootsie center. That would satisfy about 90% of my unmet project organization needs.
Uneven Libraries and Documentation
Most of core Erlang is well documented and designed, but too many of the included modules are buggy, overly complex, poorly documented or all three.
The Inets httpd server we've found incredibly frustrating to use in CouchDB and are discarding it for a 3rd party Erlang HTTP library. The XML processor (Xmerl) is slow, complicated and under documented. Anything in Erlang using a GUI, like the debugger or process monitor, is hideous on Windows and pretty much unusable on OS X. The OTP build and versioning system is complicated and verbose and I still don't understand why it is like it is.
And crufty. I know Erlang has been evolving for real world use for a long time, but that doesn't make the cruftyness it's accumulated over the years smell any better. The coding standards in the core Erlang libraries can differ widely, with different naming, argument ordering and return value conventions. It's tolerable, but it's still there and you must still deal with it.
Erlang Really Sucks?
Yes, in all the ways I just described and more that I didn't.
But also no. Erlang is amazing in ways it would take a whole book to describe properly. It's not a toy built to satisfy the urges of academics, it's used in successful, real world products. But there is a good chance that Erlang just is not a good match for your uses. This list isn't meant to put down Erlang, but as an honest assessment of it's weaknesses, which I think aren't discussed enough.
Posted March 9, 2008 3:46 PM
Comments
Thanks Damien, it's nice to read thoughts about what's not cool about Erlang.
Lawrence Oluyede, March 9, 2008 5:37 PM
Hi Damien,
Agree completely with all of your points. Just a note though -- in many cases where you are doing a bunch of stuff like
f(X) ->
X1 = foo(X),
X2 = fab(X1),
X3 = bar(X2),
baz(X3).
it would be better to use a fold on a list, that way you end up not needing to name the intermediate variables. Since you can always use the module, function, args way to invoke an erlang function, you could do a fold on the list [foo, fab, bar, baz] which would make it easy to add more functions.
Vijay Chakravarthy, March 9, 2008 5:48 PM
I think the problem with Erlang is that it's more than 20 years old and for most of this time haven't been exposed enough to developer community at large. It's like raising a child in a cellar for all its childhood and don't let it interact and learn from his/her peers. Erlang definitely needs something like Python's PEPs.
Meanwhile I am looking forward to see what comes out of efforts like LFE.
Kamyar Navidan, March 9, 2008 6:40 PM
Have you tried daemontools for restarting the Erlang process? http://cr.yp.to/daemontools.html
I wonder if you could maintain your Erlang code as a set of S-expressions. Then use a few Lisp functions to write out the final destination syntax to a file.
CS, March 9, 2008 6:50 PM
For refactoring have you tried distel? I found it made life a fair bit easier to use it when hacking Erlang. For lisp programmers it is a bit like slime for erlang.
cheers.
msingh, March 9, 2008 7:32 PM
Prolog is still better than Java. At least it had the intention to do something useful, while not focussing on machine code optimization. Java alas, does nothing good, it's harder than C++ to program, and the machine code just plain sucks, indeed it doesn't even do machine code, but just some useless virtual machine interpreter code, even BASIC can do that better!
Mika Heinonen, March 9, 2008 7:52 PM
You raise some very good points. Erlang does need to address some of these warts to gain wider acceptance.
However, I'm curious why you think the expression oriented syntax (a la Lisp is a bad thing.
I don't know how much you've worked with Lisp, but to me, the issue is trivially supported by the code editor. Emacs and its various cousins have solved this for Lisp s-expressions to the extent that editing any other language looks clunky (Just contemplate transpose-sexps in any other syntax).
There's a lot of merit to Erlang adopting a s-expression syntax like the LFE work which has appeared recently. Your other points would still need to be addressed though.
ram k., March 9, 2008 7:59 PM
* Basic syntax. Different terminators are not C-ish but I don't think that matters much. Except maybe when you're switching between languages very often during the day.
* As you pointed out: the if syntax only evaluates guard sequences. In the mildly complex code I am looking at right now (about 10 modules) there is none present - I don't think I've ever used one during about one year of Erlang development.
* Single-bind variables: as Erlang's default stacktraces force you to write many small function I never found that a problem.
* Records: preprocessor structures with a nasty syntax where most introspections also need macros. I agree.
* Heart dying: should not IIRC but I've never operated an Erlang application.
* Namespace: see the package module. It requires you to import everything which is in the default namespace (.) though. Or use the prefix convention.
* Module quality: I personally never used xmerl or anything GUI-ish (the examples I looked at kind of spoke for themselves) but you're right the quality of the modules varies. OTP version handling is not really concise documented, that's all (IMHO).
IMHO Erlang is a special purpose language (which is why I do not really understand the hype around it now). For everything beneath it's scope it get more and more difficult to produce concise solutions.
yawn, March 9, 2008 9:34 PM
int f(int x) {
x = foo(x);
x = fab(x);
x = bar(x);
return baz(x);
}
isn't good code to begin with.
int f(int x) {
return baz(bar(foo(x))
}
is better in either language, and lets you make your change easily. Why give names to the intermediate states? It only creates more conceptual entities to track later. This is presuming you've named the functions well of course.
Greg M, March 9, 2008 10:31 PM
"return baz(bar(foo(x))" increases line noise and possibly hinders comprehension, so use with care.
"* Heart dying: should not". Well, it does :-)
"tried daemontools": for custom deployment they are nice, but we don't want to bundle them.
Jan, March 9, 2008 11:35 PM
Of the above comments I can only see two which are major actual problems
The rescrition on Guards and if conditions is definatly cumbersom. There should be some way to declare user defined functions as safe to use in guards. Honestly you should be able to do this, and if you lie well then you've brought trouble on your own head.
Some milage may be gained by using macros in place of user defined functions here (I could be wrong I'm still very new to Erlang and havn't tried this).
The trouble with records struck me as a flaw immediately (on the first reading of the manual) and does smell like it was bolted on.
There is a few other bolted on features you didn't mention such as in process error handling with catch and throw.
Konrad, March 10, 2008 1:22 AM
I basically agree with all your points. Yes they are warts - sigh.
The complaint that heart doesn't restart the VM when it runs out of memory I'd consider just a regular bug. Report it! Of course the VM should be restarted by heart if it runs out of memory and calls exit(1)
On a sidenote - I firmly believe that exit(1) is the right thing to do when malloc() returns NULL
The alternatives are hideous and leads nowhere.
klacke, March 10, 2008 5:06 AM
Re: klacke saying that exit(1) is the right thing...
It really might be, distasteful as that is. Since you aren't explicitly allocating or deallocating memory, how would you handle an OutOfMemory exception, anyway?
We (Opera) run on small devices and every single allocation is prepared for failure, and we do our best to handle the situation with grace.
But I cannot imagine how we would do this in Erlang. Without having a heap/stack distinction, with all of the intermediate objects, with all of the recursion... running out of memory is really a nasty problem. :)
Maybe some kind of compile-time guarantees that certain parts of the code would run in constant space... is that even possible? Even without recursion? Or certain "big objects" that might live in a less-safe place... no, these are really hard problems, Damien. :)
Otherwise, I totally agree with everything you said. And it kills me because most of this stuff is so silly, so surface! It would still (to me at least) feel like Erlang. I often think about writing a front-end that spits out Erlang, but... never quite get around to it. :)
Chris Pine, March 10, 2008 5:37 AM
Nice post!
If I hadn't been using Erlang before and wanted to check it out. Then I would get rid of those silly ideas here and now!
Tobbe, March 10, 2008 7:35 AM
Chris Pine wrote:
>We (Opera) run on small devices and every single >allocation is prepared for failure, and we do our >best to handle the situation with grace.
grace? what is that then. Writing a log entry? That'll also probably fail since it probably will have to allocate to format the print buffers or something. Pre allocated buffers to use in case of memory exhaustion. Uhhh.
The Linux OOM killer - is that a good idea. I think not.
>But I cannot imagine how we would do this in >Erlang. Without having a heap/stack distinction, >with all of the intermediate objects,
It would actually be perfectly doable to choose one or several processes based on some heuristics such as number of reductions, heap size, stack size, unprocessed messages in the mbox etc and then just kill that/those processes and thereby releasing the memory held by just those - possibly - offending processes. Very similar to the OOM killer.
So in Erlang this would be doable as opposed to say a C or a Java program. But I still think it's a bad idea. Better then to exit(1) and let heart restart the daemon.
klacke, March 10, 2008 7:38 AM
Yawn, yawn. You have pet peeves like most programmers. How very interesting(irony intended).
A non, March 10, 2008 9:02 AM
klacke asked:
> grace? what is that then. Writing a log entry?
In Opera's case, or in some hypothetical Erlang?
In Opera's case, we stop and look for memory we can free up (ecmascript runtime gc, cached images, etc).
But my point was that only the app can know how to gracefully handle OOM situations, and can only do something about it if the app is in charge of the memory.
Though you bring up the idea of killing possibly-offending processes... what if the app could declare certain processes as OOM-expendable? Then in an OOM situation, Erlang could start killing off those, and only exit(1) if there are none of those left.
Since the app was able to choose (ahead of time, of course) *which* process(es) to kill, wouldn't that be better than just exiting? Seems like it would not be too much trouble to implement, either, and since it's opt-in only it would play well with existing programs.
Chris Pine, March 10, 2008 10:54 AM
Wrt heart: this is really surprising to me since we run heart and we see it restart Erlang all the time. kill -9, erlang:halt (1), etc. all cause restarts. One question: have you set the HEART_COMMAND environment variable?
Paul Mineiro, March 10, 2008 1:07 PM
FYI, Erlang does have hierarchical namespaces that correspond with source file locations in subirectories (e.g. hammer.json:serialize() that corresponds with what you wrote in "hammer/json.erl").
partdavid, March 10, 2008 6:18 PM
On "if"--would there be an issue if "if" was named "guard" (since that's what it does, evaluates guard clauses, not any conditional expression); and if you could "if ... is" as a synonym for "case ... of"?
partdavid, March 10, 2008 6:49 PM
" heart and we see it restart Erlang all the time"
Jan, March 10, 2008 7:17 PM
(Now the last commet got butched)
" heart and we see it restart Erlang all the time"
we use heart, but in case of a failing memory allocation the erlang vm and all of its processgroup get wiped out. including heart. "just" killing erlang works like a charm.
Anonymous, March 10, 2008 7:20 PM
vim? emacs? or ???
linux? windows? or ??
:-)
zj, March 10, 2008 9:59 PM
"Yawn, yawn. You have pet peeves like most programmers. How very interesting (irony intended)."
I think you mean sarcasm. What kind of person takes the time to read an article and then bother to leave a comment like this. Very strange.
Noah Slater, March 10, 2008 10:19 PM
As far as what to do when an allocation fails, fetchmail waits a bit for some less important process to free up some memory, and tries again. Perhaps not the right choice for a soft real time environment.
Ronald Pottol, March 10, 2008 11:03 PM
I'm curious as to why you think having to add files for modularity is a problem:
"Every time time you need to create something resembling a class (like an OTP generic process), you have to create whole Erlang file module, which means a whole new source file with a copyright banner plus the Erlang cruft at the top of each source file, and then it must be added to build system and source control."
I've been following the one class, one file rule (in C++ development) for several years, and find that it actually helps me to navigate the codebase when file names correspond to single classes. It also keeps the file size small, and class interfaces fit in just one modern screenful (at least most of the time).
Jaakko Haapasalo, March 13, 2008 2:58 AM
11. Thou shall not doubt open source software.
Banador, March 13, 2008 6:24 PM
The discussions on the erlang-questions list pointed out that a simple alternative to 'if' for your debugging example is:
DebugOn andalso log(Something),
provided that log/1 returns 'true' it can be used in any construct where andalso makes sense.
Jay Nelson, March 14, 2008 12:42 PM
The side swipe against prolog mar what is an otherwise interesting and thoughtful article.
Erlang definitely got a leg-up from prolog and declarative programming languages in general.
No doubt when the author(s) of Erlang wanted to build a new language they chose to do it in a language that easily supports building new languages.
I wonder if the author has tried using the definite clause grammar (DCG) feature in prolog? It is a very elegant way of building parsers for domain specific languages.
Another debt owed to prolog is the pattern matching algorithm (unification). The use of unification is one of the reasons that Erlang manages to be so terse and expressive.
Most of the problems the author describes with Erlang syntax were introduced in Erlang in an attempt to shoe-horn imperative features into a syntax that was designed for a declarative language.
There are plenty of things that make prolog completely impractical for serious software development (no arguments on that point) but the lack of a regular syntax is not one of them.
Erlang is a well chosen compromise between expressiveness and terseness of logic programming languages and the practicalities of building distributed imperative software for current networks/hardware.
Declarative by nature, April 5, 2008 11:31 PM
Post a comment