Hacker News new | past | comments | ask | show | jobs | submit login
Erlang beauty (ikura.co)
170 points by _nato_ on Nov 17, 2015 | hide | past | favorite | 88 comments

I started off as a designer before learning to program and I have a minor unhealthy attachment to the design and composition of functions. I've been learning Erlang recently and I have to say Erlang's pattern matching and list comprehension make it one of my favourite languages in terms of design (I should note I'm also a fan of Scheme/Clojure style which I know many people aren't).

It certainly didn't appear that way when I first came across Erlang, as it's not always readable to the uninitiated (and nearly turned me off the language) but brilliant for those that are familiar with it.

Elixir has also done a good job of translating it for people who like Ruby-style syntax, I prefer the flexibility of the more verbose Erlang.

> Elixir has also done a good job of translating it for people who like Ruby-style syntax, I prefer the flexibility of the more verbose Erlang.

The really big deal about Elixir is the ability to do LISP-style runtime code execution and generation. Lots of the Elixir language is implemented as macros, and the beauty and power of it is evident when studying the source of Elixir itself and Phoenix. A good example of where this comes into play is implementing a gen_server: in Erlang, this is always a good bit of boilerplate. In Elixir it can be done with a couple of lines, with automatic default implementations provided.

The Elixir syntax is a nice bonus, but the runtime code-generation Elixir brings to the table is a game-changer that takes it to the next level.

While I like this idea, I think the parent's point is that the utter simplicity of the language is worth living without these things.

I personally prefer Erlang to Elixir aesthetically. The big wins I see from Elixir are protocols and standardized tooling (though I have high hopes for rebar3). Parse transforms are overkill for most things but that's about how often I need things like macros in the world of Erlang.

Even still, today's Erlang ecosystem is about the BEAM and it's great to have so many options. Erlang, Elixir, LFE, Lua, Joxa, &c.

You can now do this in a sanctioned way in Erlang as of v18. They're including merl as part of the standard distribution now.

This is a very big deal. Thank you for telling me about it! As is so often the case, there's more value in the HN comments than the articles.

Elixir macros are compile time.

Yes, sorry, I meant compile time!

Are macros an obstacle to having code completion for Elixir?

Since you mentioned Clojure and Erlang's pattern matching, I thought it nessicary to reference this Clojure library, [1] defun.

Here's a quick snippet:

    (defun fib
      ([0] 0)
      ([1] 1)
      ([n] (+ (fib (- n 1)) (fib (- n 2)))))

    (fib 10)
    ;;=> 55
Cool isn't it!

[1] https://github.com/killme2008/defun

That is indeed cool! Seems extremely handy and intuitive.

I've seen more than one control structure library for Clojure that would make certain algorithms more smooth and "easier to read" in code - I just wonder if it's actually confusing for the readers to start pulling in all of these to your project as you see fit.

This! Erlang's syntax is so small and simple, it probably fits on a postcard.

Although I appreciate the excellent tools and libraries built by the Elixir community, I have a hard time understanding how one can prefer Elixir over Erlang.

It is simple. I think it was one of the design goals.

The other interesting thing is over the years creators have added features but have tried to also remove features to keep it is simple. That is very hard to do but they fought for it, and I think it paid off after many years.

Yup, got bitten by this design goal once, when support for parameterized modules was removed (though I do agree it was the right move in retrospect).

Shame about the record syntax, though. That was the only part I explicitly disliked when working with Erlang.

Agreed, I have found myself wishing other languages have Erlang's pattern matching functionality.

I have to admit my first attempt Erlang was filled with nested cases and I knew something was wrong... Now, I avoid case statements in favor of pattern matching.

Erlang the language is nice, but where it really comes together is the runtime environment.

Although I was a programmer before I became a designer (and still am), I've also found that graphic design (typography in particular), has positively affected how I compose functions and methods. It first started with the how I laid out my statements, then progressed eventually to size and how they were arranged.

I argue with my colleague a lot about whitespace (he likes to get rid of all of it); sometimes I wish I'd done a Ph. D. on the effect on typography on programming just to win the argument.

I relate a lot with you. I would even see there a pattern, once that design oriented people normally have a preference for low boilerplate and beautiful designed languages as ruby or haskell or even lisp.

> Someone smart, somewhere, at some time, did a study which concluded that the human brain can easily hold six or seven items in short-term memory with little trouble, but beyond that, it becomes taxing. This applies nicely to lines of code in a function. Amazingly, after all the above guidelines have been followed, bringing a function’s lines-of-code count down to a maximum of seven is surprisingly easy to do.


This has got to place as one of the most misapplied research findings in history. If you have an aesthetic preference for 7 as a limit to the number of lines of code in a function, then, sure, do that, but don't pretend there is science behind it (or at least not this particular science.)

She may be wrong about that particular number, but it is really no secret that reducing the scope is a key ingredient to solving problems and avoiding bugs. Another key ingredient is to stay far away from side-effects and Erlang does a fairly good job of riding that fine line between ideal theory and pragmatism.

edit: gender of author corrected, thanks Nathancahill


When in doubt, 'they' is a safe bet too.

To be fair, in our circles, "he" will be right about 95% of the time.


shim also works

Pretty sure nobody did a study with a specific number of LoC for best understanding, because it probably depends on a lot of things. But quick understanding is a function of consistent style and a small number of things to keep in mind required to understand each part.

Things are easier to understand when the deciphering part is done quickly and also when you only have to keep as few things in mind as possible.

An example of a Python statement that is short, but difficult to understand:

    '\n'.join([map(some_func, a) + 'somth' for i, j in c.items() for a, b in i.items()])

Lets play devils advocate. Does pretty == readable?

I consider this:

foo(X) ->

    case X of 

        bar   -> void;

        _Else -> undefined

More readable than:

foo(X) -> foo1(X).

foo1(bar) -> void;

foo1(_Else) -> undefined.

First function is parsable in its entirety immediately. It conveys an idea in a single fragment, which is not too large and it is not too small. Foo executes depending on value of X.

Second function requires parsing two fragments of code. It requires a not-so-pretty naming scheme of appending 1 to the function. And you can not grasp the entirety of the matter in a single glance. To parse this function you must follow a train of thought: foo -> foo1 -> execute depending on X.

Second function style of code results in a million functions in a single module which I very much disagree results in more readable code.

Moving on to next example:

foo(X) -> final_function(maybe_function(X)).

foo(X) -> Maybe = maybe_function(X), final_function(Maybe).

I fail to see how second foo is more clear. In Erlang maybe value can be absolutely anything. It is unnecessary verbose. The explanation on what is wrong with first function contains way to many "shoulds" and not enough "becauses".

> Second function requires parsing two fragments of code.

foo1 is used to illustrate a point not that you'd name functions like that. The whole thing would be probably:

   foo(bar) -> void;
   foo(_) -> undefined.
I don't know, I see that better than the case statement.

Also, adding an "catch all" is usually not recommended in Erlang (even though it's good for some things), so most of the time you would do just: foo(bar) -> void. And then let it crash if the value is something else than expected.

Ah! Very good point. Yes I agree.

Appending 1 to functions is quite common in Erlang for cases such as that. You cant find a good naming scheme for small fragments of ideas.

I don't know, I find I do it with variables more than functions. With functions I seprate them by arity, so there could be a foo/0 and foo/1 maybe. But usually I haven't seen much fun1 fun2 fun3 ... pattern.

And if you don't see a way, don't split it. I mean, make your code look good and easy to understand for yourself and others, that's the point.

Appending 1 to functions might be used for utility/internal functions with a slightly different (and less convenient) signature than the base[0], usually because the original signature does not allow for tail-recursion. That's not the case here.

It makes a difference when it's colorized and may be takes some time to get used to but see this for example http://imgur.com/6UYUvye

Hello there, fellow Kazoo developer!

Haha, I follow your commits these days :)

Don't know erlang, but why can't the first example be

foo(bar) -> void; foo(_Else) -> undefined;

It's an overly simplistic example.

Seems reasonable to me... except I don't see the point of having foo/1 and foo1/1. The author could just have easily done:

  foo(bar) -> void;
  foo(X) -> undefined.
without the need for the proxy function:

  foo(X) -> foo1(X).

I think it was for demonstration. The example certainly is a bit contrived but the general idea is that pattern matching in function heads is preferred over pattern matching in case expressions by most experienced Erlang developers I know and certainly by the author.

It's not a hard rule but there are definitely times when giving a name to a set of patters is better than the anonymous case. Some other languages use things like let-in and with to achieve similar ideas but Erlang is a rather simple language so it just reuses function heads instead of nesting definitions.

Yeah, it makes for some very interesting uses cases for code-generation.

I've converted entire databases of mostly static data into nothing but a bunch of exact-match function head signatures and let one of the fastest paths in the VM be my "query planner".

It's insanely fast (at the expense of compile time) and the generated code is really easy to read, trace, and debug.

Intersting. Do you have an example somewhere?

The point is to show an example of how a mechanical translation could work from one form to another. It is like an intermediate step if you wish. You'd instead do that code snippet you wrote first of course. But yeah looking back at comments here I think they've confused others with that code as well it seems ;-)

The beauty of Erlang became apparent to me when looking at a the structure of Google's Certificate Transparency structures.

There are C implementations with loops, gotos and memcpy()'s. In Erlang I could do this:

    <<Version:8, LeafType:8, Timestamp:64, LogType:16,
        ASNLen:24, Cert/binary>> = PemBin.

Nice guidelines. Having finally put a few hundred lines of erlang under my belt, it's clear to my that it's a wildly under appreciated language.

The pipe operator is extremely elegant and helpful in F# and (I presume) Elixir. I don't understand why the author considers it "malpractice".

Well, the author says it helps 'mollify this malpractice', which I would take to mean it helps improve the readability of nesting functions. Mind you, the author says "incredibly", too, and I'm not sure how to take that.

Since Erlang doesn't have a pipe operator it's hard to compare but I'd imagine using nested calls where a pipe might work is also considered inelegant in F#?

It's the "incredibly" that is tripping people up on that statement, I suspect. I personally love the pipe operator in F#, and use the Clojure threading macros in much the same way - it removes nesting, and it makes logic flow more obvious

Excellent heuristics for any functional language, not just Erlang.

Not being able to pass functions as values is a show stopper - it's arguably the single most important tenet of functional languages.

i believe she was talking specifically about `foo(bar(baz(X)))` and not passing functions themselves as arguments

Ahh - that makes more sense. Still, this practice wouldn't make much sense for the ML family of languages.


When in doubt, 'they' is a safe bet too.

I understand her point to be about passing called functions. Surely passing functions as values (partials, etc) is fine?


Disclaimer, not familiar with Erlang syntax, so the above example is psuedo-code.

"If you need more than 3 levels of indentation, you're screwed anyway, and should fix your program." --Linus

Right, you can pass a function:

final_function(fun() -> maybe_function(X) end)


final_function(maybe_function, X)

if final_function does:

final_function(Function, X) -> Function(X).

Small thing, but `final_function` here needs to be passed `fun maybe_function/1`, not the atom `maybe_function` alone.

Oops, yup. Meant to make it explicit since you can also do: ?MODULE:Function(X)

You can pass functions. The article was saying not to nest function calls.

You can't, where? You mean in general in some functional languages?

Sorry I don't see how your comment fits with the gp's comment.

How does everyone feel about the last item, the "seven lines per function rule"?

I can definitely appreciate the readability of this assuming the functions and intermediate variables are well named, but am I the only one that fears negative PR feedback about adding cruft?

"but am I the only one that fears negative PR feedback about adding cruft?"

Go for it, and you might be surprised as how your team responds. The biggest improvements to our codebase have come from my colleagues making controversial changes. Sometimes they don't get merged, but sometimes they end up shaping how we write code going forward.

You should never be scared of negative PR feedback -- your reviewers aren't judging you, they're judging your code, and code responds surprisingly well to criticism.

> How does everyone feel about the last item, the "seven lines per function rule"?

As with all constructs that incorporate magic numbers, I really, rather dislike it.

Playing "follow the function call trail" can be just as harmful to code readability as overly-long functions.

I do strive to write functions that are as short as is reasonable, and I do lift single-use code blocks out into their own functions when doing so improves clarity. However, I'm neither an English 101 student, nor a newspaper journalist: I don't stress about whether my work meets some arbitrary length criteria. :)

It's amazing how people seem to miss that sending their readers jumping around all over the place just to get an idea of what is going on can be worse than long functions where the code reads coherently and simply.

I think Clean Code has encouraged people to write a lot of single line functions that get called from a single place (often with weird parameter lists) as a means of replacing commented code blocks. While it has its place, this is not a good pattern in general.

Good function writing is similar to what gets said for objects in OO design. The function should do one cohesive thing that it is sensible to give a name to and consider at least somewhat in isolation from its context. It should take in a relatively small number of parameters (external dependencies for classes), all of which should be the sorts of things you'd expect it to need from its name.

> I think Clean Code has encouraged people to write a lot of single line functions that get called from a single place (often with weird parameter lists) as a means of replacing commented code blocks.

Single line functions, called from a single place? To replace commented code blocks?? In the name of improving readability!? What?! (!!!)

Unrelated to that: If your function name correctly and completely describes the actions taken by that function and those that it calls, then replacing a commented code block with that function isn't unreasonable.

shrug I guess the ages old advice: "Dogma is bad. Rules of thumb are often useful." applies to the topics discussed in this subthread. :)

Short functions is a pretty universal and good heuristic.

Breaking things down in smaller easier to understand pieces if you can applies to any language probably. 7 lines is just a heuristic.

It really isn't. As another user said below, following long function call chains is counter-productive and if you don't have an ultra strict function naming policy you will have functions with duplicate functionalities very quickly.

> following long function call chains is counter-productive

Well, be productive then!

It is a heuristic. I don't think I am in the minority saying that "breaking things down in small manageable chunks is a good thing". That's is not just Erlang that's common sense. That's the point. That doesn't mean having to count lines if you go over 7 start making functions like next1 next2 and so on. That's called being pedantic.

No being pedantic is giving a fixed number. Just say: "try to keep functions small" and be done with it without needing to debate semantics.

I just said this somewhere else, but if you change it to ten lines per function you can rearrange FUCRS and then you'll really be adding CRUFT.

Heh, FUCRS? At first skim, I thought the cheerleader chant spelled "FUNCS", which is cute for Erlang. Is this just a jokey abbreviation of "fuckers", a random set of letters given in a funny way, or am I missing something?

I don't think you are, no. It might have been a backronym, or it might be a legit acronym, but either way it means nothing to me either (well, from a programming, Erlang especially, context)

Perhaps if they used 10 instead of 7 for the magic number, they could have used CRUFT.


foo(X) -> foo1(X).

foo1(bar) -> void; foo1(_Else) -> undefined.

rather than just

foo(bar) -> void; foo (_Else) -> undefined.


It is just a simple example to convey that a portion of foo/1 contains a case statement, and that case statement is being replaced with a function. There could be more lines in foo/1. The point is that both the case statement and the multi-clause function are identical in behavior, but the function prevents nesting at the call site.

To illustrate a point of how the two are equivalent and one could almost write an automatic translator. But yeah it ended up confusing many here.

Also the _Else part I would just make it into foo(_) -> undefined.

foo(_Else). -> undefined. Doesn't seem very erlangy. I'd expect a runtime error, as someone's passed foo() the wrong value.

> Doesn't seem very erlangy. I'd expect a runtime error, as someone's passed foo() the wrong value.

It depends.

Sometimes you want to crash when handed an unexpected value.

Sometimes you expect that you'll be handed unexpected values and need to either fail more gracefully, print a warning and then fail, or warn and keep going, or whatever.

Erlang gives you a bunch of tools, and they all have their place. [0] :)

[0] With the possible exception of the if statement... which is of questionable utility.

The "if" keyword should have been "when" instead. It's useful in some cases, but rarely used.

But in the given example, isn't foo(_Else) = foo1(_Else) = undefined anyways?

  foo(X) ->
      case X of 
          bar   -> void;
          _Else -> undefined
> would become:

  foo(X) -> foo1(X).

  foo1(bar)   -> void;
  foo1(_Else) -> undefined.
Incomprehensible rubbish, made more obfuscated by the second form. Okay, X appears to be an identifier bound as a parameter. So then why doesn't bar behave that way in

   foo1(bar)   -> void
is it because the X symbol is upper case? Or one letter wide? (Yuck, if so.) Or does it have something to do with that semicolon? (Double yuck.)

At a glance, how the heck do we know when a symbol denotes a binding, and when it denotes itself?

"_Else" is beautiful? It reminds one of the "_Bool" retch, vomited up in C99 by a drunken ISO C committee; but at least that is hidden by a #define bool _Bool that everyone halfway sane uses in its place.

In any event, splitting case statements into pattern matching functions isn't "lucid". Unless you're doing OOP, it's largely stupid. The case statement keeps the relevant cases together. It "en-case-apsulates", pardon the pun. The only reason that the individual functions retain clarity is that they are grouped close together, so we, the readers, can reconstruct them as the case handling aggregate that they are.

> Incomprehensible rubbish, made more obfuscated by the second form. Okay, X appears to be an identifier bound as a parameter. So then why doesn't bar behave that way in...

It appears that you don't understand Erlang syntax. Go back and read these two chapters [0][1] of Learn You Some Erlang, and revisit the article.

[0] http://learnyousomeerlang.com/starting-out-for-real

[1] http://learnyousomeerlang.com/syntax-in-functions

From your reference [0]: (note that variables begin with an uppercase letter)

Seriously? Making case not merely distinct but semantically significant is a deal breaker for me.

(Does that at least work for all Unicode characters that support case distinction? If we start an identifier with a Δ, is that a variable name, as opposed to starting with δ?)

> It appears that you don't understand Erlang syntax.

Yet I formed a correct hypothesis: the meaning does hinge on the case of X.

I only formed this hypothesis because I'm jaded.

The designer of a non-clusterfuck syntax would be offended by such a cynical hypothesis. ("How can you even think I made case semantically significant in a symbol?")

I was hoping someone would tell me exactly that and set me straight about how it really works. Like maybe that X and bar are assumed to be declared somehow or whatever.

> Does that at least work for all Unicode characters that support case distinction...

Erlang was designed and first written back in the 1980's, so characters that fall within the latin1 encoding are the only acceptable characters in atom and variable names. However, UTF-8 source files are fully supported, as is Unicode in lists and binaries. There is a proposal to make Unicode characters accepted for atom and variable names, but it hasn't been accepted yet, as the details are (as you allude to) tricky. [0]

> The designer of a non-clusterfuck syntax would be offended by [the conclusion that I came to].

I can't agree with that. Initially, I thought that the case-significance was strange, but -as I used the language- I quickly found that the ability to distinguish at a glance between an atom and non-atom type is very valuable. These are things that you don't understand until you learn about the language, and maybe actually use it some.

Erlang's syntax is strange, but not even within ICBM distance to being a clusterfuck. Frankly, Python's significant whitespace has caused me far more lost hours than any strangeness in Erlang's syntax.

> I only formed this hypothesis because I'm jaded.

Heh. I expect that the fact that you've likely seen many imperative and some functional languages in your day, and that Erlang is -despite its syntax quirks- a language that is rather well designed and makes sense once you look at it for a few minutes, has far more to do with it than your disaffectedness. :) [1]

From your other comment:

> I promise you, I will not write another word anywhere about Erlang.

That's a pity. Erlang is rather quite good at what it's designed to do, and a real pleasure to use. Many folks report that Elixir is at least as rewarding to work with.

[0] http://www.erlang.org/eeps/eep-0040.html

[1] As an aside, I note that in your opening comment, you mention that the syntax is "incomprehensible rubbish", but yet you correctly determined a pertinent feature of the language syntax in very little time at all. This kinda puts the lie to your claims of incomprehensibility. ;)

Your tone sucks but your question is valid, and I'm personally interested in the history of capitalising variables, so I'll answer your question.

In short, it all goes back to Prolog, the language used to write the (first versions of) Erlang's first compiler. Prolog has bequeathed quite a bit of its syntax to Erlang and also chunks of its semantics, for instance the pattern matching (although arguably that may be a trait inherited from its functional lineage instead, or maybe Prolog borrowed it from LISP, or possibly it was just convergent evolution).

In any case, in Prolog any term that begins with a capital letter or the underscore, '_', is a variable. A single underscore is a "don't-care" variable. There is a very good reason to syntactically distinguish variables from non-variables like this. Note that I said "any term" earlier, rather than "any identifier". In Prolog, variables are never stand-alone identifiers[1], instead they are arguments of predicates. That's because Prolog variables represent logic variables, rather than computer science variables. By "logic" here I mean mathematical logic, as one may find in textbooks. In this sense, logic variables are not meant as references to memory locations (though in Prolog it's very convenient to see them as such, at times). Instead, they represent unbound terms, terms whose value we do not know until a certain point in our calculation, or indeed computation. Once the value of a variable is known, it stays the same until the end of computation (it's immutable) and we speak of the variable as being "ground" or "bound to the value".

Now, first-order logic predicates, and therefore Prolog predicates, are written in clausal form as:

F(t1, t2, ... tn)

Where "F" is a ground term and t1,...,tn any number of terms, each of which is either a ground term or a variable (ie, unbound). So we need a way to tell ground terms from variables. In textbooks, the convention is to denote variables with lowercase letters and ground terms in uppercase. In Prolog it's the other way around, probably because of some concern over overuse of the Shift key in old-style keyboards.

The alternative of course would be some kind of annotation of the varible, with an additional term. For example, we could use the functors "ground" and "variable". This would make for hairy syntax though; look at our example predicate from above:

ground f(variable t1, variable t2, ground t3)

That syntax would just make it harder to see what's going on. Instead:

f(T1, T2, t3)

- makes it perfectly clear that T1, T2 are variables and t3 is a ground term. [Edit: where "predicates", in Prolog, read "functions" for Erlang. In Erlang also there are no stand-along variables declared, or referenced, outside of functions so the same hairy syntax would have to be adopted to distinguish them].

... And that is how Erlang got its uppercased variables.


[1] So in Prolog, as in first-order logic, one cannot declare variables separate from predicates. You don't start a program by laying out a bunch of variables and their types etc. Variables live between parentheses, as parts of terms, never anywhere else.

Well OK, you can also do this:

  f(T1, T2, t3):-
But that's a story for another time :P

Meh, don't feed the trolls, I think it is pretty clear they don't want to learn, rather they want to spew angry rubbish into the conversation.

I'm grateful for the response; it clarified/confirmed things. I promise you, I will not write another word anywhere about Erlang.

Maybe try Elixir if you don't like the syntax of Erlang, because it's really a pleasure to work with Elixir/Erlang.

As others have said, there is a difference in Erlang between `Foo` and `foo`; the first is a binding the latter is an atom.

Many languages encode information in capitalization. Go uses it for functions which are exported. In OCaml and Haskell, constructor names always begins with a Capital letter (to distinguish them from values). The alternatives are usually not that alluring: either you need some symbol like a backtick, or a keyword in front or behind the value. So capitalization it is.

The particular Erlang-case stems from Prolog as already said. And as you note, Unicode provides its own headaches with respect to these rules.

Applications are open for YC Winter 2022

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact