Erlang beauty
blog.ikura.co
blog.ikura.co
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.
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.
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.
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.
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.
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.
Here's a quick snippet:
(defun fib
([0] 0)
([1] 1)
([n] (+ (fib (- n 1)) (fib (- n 2)))))
Then: (fib 10)
;;=> 55
Cool isn't it!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.
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.
https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus...
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.)
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()])edit: gender of author corrected, thanks Nathancahill
When in doubt, 'they' is a safe bet too.
I consider this:
foo(X) ->
case X of
bar -> void;
_Else -> undefined
end.
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".
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.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.
foo(bar) -> void; foo(_Else) -> undefined;
foo(bar) -> void;
foo(X) -> undefined.
without the need for the proxy function: foo(X) -> foo1(X).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.
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.
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. final_function(maybe_function(X))
vs final_function(maybe_function)(x)
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
final_function(fun() -> maybe_function(X) end)
or
final_function(maybe_function, X)
if final_function does:
final_function(Function, X) -> Function(X).
When in doubt, 'they' is a safe bet too.
Sorry I don't see how your comment fits with the gp's comment.
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?
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.
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. :)
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.
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. :)
Breaking things down in smaller easier to understand pieces if you can applies to any language probably. 7 lines is just a heuristic.
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.
foo(X) -> foo1(X).
foo1(bar) -> void; foo1(_Else) -> undefined.
rather than just
foo(bar) -> void; foo (_Else) -> undefined.
?
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.
Also the _Else part I would just make it into foo(_) -> undefined.
foo(X) ->
case X of
bar -> void;
_Else -> undefined
end.
> 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.
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.
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.
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):-
t3(T1)
,T2
...
But that's a story for another time :PErlang 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. ;)
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.