Specializing functions in Elixir - reminiscence from the past?

As I treat my crusade to remaining 'polyglot' very serious, I'm putting significant focus on The BEAM (Erlang VM) as a platform & Elixir in particular. Why Elixir over Erlang? It's quite a long story that deserves a separate post, so let me skip this consideration for now.

Anyway, back to Elixir - as you would expect from an Erlang descendant, it is a functional language and it supports advanced pattern matching (let's call it PM for the sake of brevity). Not a big deal, I've been using Scala for some time & PM has proved itself very useful if you want readable, concise code. What is all Scala's apply/unapply duo is making PM very flexible (extractor object). But there's one VERY interesing feature of PM in Elixir that was not present in Scala...

Function can have multiple bodies

Ok, that may sound ridiculous, but let's get through an example (anonymous func, to make it even simplier):

do_something = fn  
  { param1_A, param2_A, param3_A } -> # do stuff A here
  { param1_B, param2_B, param3_B } -> # do stuff B here
end  

The part of the left in each line (before the arrow sequence: ->) is the parameter list. So I've just declared two bodies that correspond to two parameters lists with the same length. And as you can see, types are not specified directly. WTH? Well, it's because the parameter lists are actually patterns of parameter lists: they have to have the same length & they are processed in order (1st possible match wins). Let's see a more precise example:

do_something = fn  
  { "Lassie", blurp, slurp } -> # do stuff A here
  { :error, _, _ } -> # do stuff B here
end  

Syntax-wise:

  • "Lassie" is an actual (expected) value of string
  • blurp & slurp are just variables (so anything could do in matching to them)
  • :error is an atom literal (sort of enum value in Elixir)
  • _ stands for anything without much care (value won't be accessible in the right side of the line)
do_something("Lassie", "will", "return") # will do stuff A  
do_something(:error, 1, xyz) # will do stuff B  
do_something(666, "is devil's", "work") # fail in pattern match  
do_something("Lassie", "gone ...") # qty of params differs, so it's a different function  

So, you can get various processing variants that depend not just on variable types (typical overloading, still avail in Elixir by using so-called guards), but also on variable values ("Lassie", :error) -> doesn't it remind you anything from the past?

Once upon a time

"Between the time when the oceans drank Atlantis,
and the rise of the sons of Aryas,
there was an age undreamed of."

The Wizard ("Conan the Barbarian", 1982)

The age of C++, my first true love (as a dev ;D). If I had to name just one thing I loved most in C++, it'd be templates - a perfect example of the mechanism that has capabilities that have by far surpassed the original intent of its creators. You could use it in exactly the same way people do with generics in C# or Java, but it was possible (though it required far more knowledge & skill) to do advanced meta-programming using templates & their unique features: explicit & partial specialization, non-type params, etc. Examples? Plenty of them here, here or here.

And these (specialization for types & non-type values) are exactly the features I find similar in ancient C++ & modern Elixir. The fact that we're speaking about the specialization on completely different level doesn't mean much, because the actual intent is to improve composing abilities of constructs available in those two languages (that are totally different in general).

To be honest you could achieve the same output effect using simpler means, but following this way of thinking -> you can do everything in assembly language, so why don't you? They key point is these both cases the specialization isn't just shorter, more flexible & readable than nested switch/if hierarchies (in imperative languages), but also it's very convenient to extend (by making a change in one, single piece of code) once you need to add more processing paths.

Having a generic algorithm while being able to adjust it to some future scenarios at minimal cost without potentially breaking changes ('contract' AKA public function signature doesn't change at all) in futures? Count me in!