I did enough explorations the past few days and it’s time I emptied my cup and proceeded as a total newcomer to the language. Not a Python programmer trying to translate Python idioms into Elixir nor a Clojure programmer checking the language out, but starting with a clean slate and learning from zero. It is not an easy thing to do. Most of the time I would fight the urge to skip a line of the documentation with the assumption that “I already know”. I don’t. My exposure to Elixir is that of a pre-noob’s and I should act like it.
Although it’s entitled “Day 1”, I had spent the past 2/3 days reading through the documentation and browsing through some source codes and snippets, I needed to get a feel for the language before I started documenting my experiences with it. While from a distance, Elixir seemed to have a familiar face but in reality, the way it makes you think and the way your code becomes art through it is quite different. The same is true for Python, or Golang, or Clojure. Every language boasts their own personality and it is important that their coders respect that. Writing Fortran in any language never did any good.
So, assuming zero knowledge of the language, I proceed with the rest of my days with Elixir.
Running a File
How do I run a file? I know iex is the magical shell that let’s me insta-code, but what if I want to write a program, a script of sorts with Elixir? Easy, I write a .exs file and do an elixir <my_file_name>.exs.
|
|
There you go, my first super sophisticated Elixir program. A quick elixir hello_world.exs printed the legendary “Hello World” to my console. I could go to the iex and type c("hello_world.exs") and enable it for a HelloWorld.run too if I want.
What if I am in the shell, compiled the code and then I change it and want to see the results? Just make the c call again.
A little on Modules
Modules in Elixir contain functions. There are anonymous functions which are of the fn(<arg1>,<arg2>...) -> <body> end syntax and can be bound with a var, but for the named functions to exist, they have to reside under a module.
|
|
Default Arguments
We can add more functions to the module, for instance, the ability to compute the area of a rectangle, given height and width:
|
|
What were those \\ 0 about? That’s how Elixir shows default arguments. height \\ 0 and width \\ 0 meant that if any of those parameters were missing during the function call, they’d be replaced with 0s. I think I will typo // instead of \\ a lot.
Match Operator
The = in Elixir is called the match operator. It doesn’t seem like other languages’ =-s in a sense that it doesn’t assign anything to a variable, it matches, and binds the right hand side expression with the left hand side one.
|
|
Error Handling
I always make sure I familiar myself with error messages of a programming language. 2 = x yielded no error but 3 = x did spit up a MatchError and naturally, so should 3 = z, as z doesn’t exist to be matched in the first place. However, this will yield a completely different error- the CompileError. It’s because the compiler expect the right hand side to be an expression which the left hand side will match with. x is an expression, it didn’t match with 3. z isn’t, so it took it for a function (it looks like a no-args function since parentheses are optional in Elixir), so the error message is, undefined function, z/0.
try … rescue and after
This brings us to exception handling. I know it’s too early to be handling exceptions but well, I had dived in there early for all but my first and second languages. Let’s start with everybody’s favorite example:
|
|
Question What does the
<>operator do?Answer That’s Elixir string concatenation operator. (I felt kinda weird to me at first)
When we either run the script or call BadMath.play from iex we get the error message and the finally after message. So I guess we can tell that after puts a piece of code that gets executed regardless whether we made an error or not.
How do you throw raise an exception? The raise macro, of course. Just put raise with the ErrorName as the first argument and message as the second. Call it from within try-s scope and it’ll catch rescue it.
throw and catch
Speaking of striking out things, looks like Elixir has throw and catch as well (See what I mean by starting with No knowlege at all?). throw is not like raise. It just does it’s namesake, takes a value, and throws it into the catchers pitch, anything that is thrown gets handled. Like the following:
|
|
Question
#{v}? Does it do what I think it does?Answer Yes. The braces take Elixir expression, evaluates, stringifies, and replaces the
#{}with it. It’s called String Interpolation.
Question What’s with the
->and thewhen?Answer Bear with me, I’ll get there
tomorrow?right after this. It’s readable though, right?
Rescuing a Function
One thing we notice is, a lot of functions demand to be wrapped inside a try. And there’s sugar for it too:
|
|
So a named function can be an implicit try too.
Summary
Here’s the takeaway:
trycreates somewhat of a protected zone around your code. AnyExceptionerrors occuring orraised becomes a candidate forrescue.- The
rescueclause takes the error in question and matches it with a list of patterns (We’re getting there soon). Whenever a pattern is matched, the associated expression is run. - The
catchclause deals with stuff thattrythrew at it via thethrowfunction. It too, has it’s own set of pattern against which the thrown value is matched with, it is better for the patterns to be exhaustive here. - The difference between
raiseandcatchis thatraiseis activated when an error occures, it knows theerrorand itsmessage.catchon the other hand is activated when something isthrown at it. It can be any value the author chooses it to be. And the value will be matched against a series of pattern, just like therescueclause. - Named functions can act as an implicit try. If the whole body of the function is to be probed for error, then the try wouldn’t be required.
aftertakes place regardless of whether error was madeelsetakes place when no errors were raised or values were thrown. It also matches the value with a series of patterns just likeraiseandcatch, it just takes on the good guys. (I had forgotten about it earlier and am too lazy to be showing an example now :()
Patterns and Guards
Now, let’s focus on the patterns we were talking about earlier. Patterns are one of the coolest things I liked about Elixir. It is actually quite simple, just take two parts, the left one will contain variables to be bound, wrapped in its data structure while the right side will be an expression. Now, mentally superimpose the right part on top of the left. If they make a complete one-to-one match (including the data structure), then you got your variables bound.
Let’s take an example here, [1, 2, 3] is a list, right? And what of [x, y, z]? A list too. But whether or not you will be slapped with a CompilerError or not is totally upto the context and declaration conditions of the variables. Now, what happens if you place [1, 2, 3] on top of [x, y, z]? You see, 1 will sit on top of x, 2 on y, and 3 on z. Bring the match operator in the mix and you have [x, y, z] = [1, 2, 3] binding x, y, and z, to 1, 2, and 3. However, if we place {x, y, z} = [1, 2, 3] they won’t really match, { will reject the [ and we get our favorite MatchError, and neither would [1, x] = [2, 6] because 1 ain’t 2. The pattern has to match completely.
Question What on earth is a
{1, 2, 3}?Answer They’re
tuples. They are likeLists, but with different agenda and performance profile. Use them when you have a fixed number of elements. I’m sure I’ll talk about them in a day or two.
So, let’s do some pattern matchings based on whatever we know:
|
|
As I’ve mentioned in Day-0, %{x: 0, y: 0} refers to a Map. A special case of it too because in here, keys are atoms. If no keys were atoms, we’d do a %{"x" => 0, "y" => 0} instead.
Lists, Maps, Tuples
I have briefly mentioned List, Tuple and Map without talking about it much, I know I will some day but here are some quickies:
Atoms are like constants where their names and values are the same.Lists can be written in the form[1, 2, 3]. However, they are recursively constructed. For example,[0 | []]is[0,1],[0 | [1 | []]]is[0, 1]and so on. This construction is of the form[h|t]wherehrefers to the first element andtrefers to the rest. This can be used as a pattern too.Maps are written like%{"a" => 10, "b" => 20}but if all of its keys are atoms, then it can be of the type%{a: 10, b: 20}. Normally, maps are accessed via the indexing operator but in case of keyword maps,.operator can be used.m = %{x: 0, y: 0}can be accessed like eitherm[:a]orm.a. For keyword maps only.- A
Listwhose elements are all tuples of two elements and the first of whom are atoms, then they are called keywords and have a special sugar as well.[{:a, 2}, {:b, 3}]can also be written as[a: 2, b: 3]and queried likelst[:a].
Functions and Patterns
Back to patterns. And here’s something interesting, function arguments are pattern-ready. Which means if we define a function definition like def f(0, 1, x) and put 0, 1, 2 as the actual parameters, then the function will be activated and x will be bound with 2. This eliminates a lot of conditions and logics and makes the programs look declarative. Modules match all definitions of the functions from top to bottom and activates the first match. Here’s an example:
|
|
In the snippet above, when we call BadMath.factorial(4), then it first matches with factorial(0) signature, it doesn’t find it, so it matches the second one. This stops when n is finally 0 due to the decrements and first one (non-recursive) is matched.
Here’s one with the head rest pattern.
|
|
Question ++ what’s that?
Answer You concatenate two lists with the ++ function. Or the
++/2
This is fun… let’s write some more of these.
|
|
And Guards
Then there are guards. Guards are basically the when clauses that were mentioned in the Exception zone.
Guards guard their patterns, a guard starts with when and is followed by a condition expression. Boolean functions can be called too but predicates that guards allow are very few in number and user-custom functions cannot be used.
|
|
This brings us to the case macro. The case macro takes an expression and matches it with a set of patterns. The grade above could be cased like:
|
|
See when we call the function above, the :wrong never appears. This is because when we assign an impossible value like -10 or 110, then they get matched with the top two clauses, hence the last clause is never met. We should either put the final clause on top, or add a high/low value checking bound in the when clauses.
Similar is cond macro. Instead of taking a value and matching a set of patterns, it dictates a set of conditions and expression that is associated with them.
|
|
That’s with the guards. There are a few things though:
Firstly, Guards know limited functions. Not all functions can be used in guards, no matter how boolean they are.
Guards don’t throw exceptions. I had understood it the hard way. The following piece of code, for example:
|
|
When I call WeirdMath.division_even(10, 2) it sends me a FunctionClauseError, saying, there’s no function clause that matches it. Then when I separately do a rem(10, 2) then I get an ArithmeticError that tells me I have bad argument in my expression. This didn’t get handled in the guard.
Now, if we go back to the error handling section, we would have an easier time understanding it and be more creative while handling errors.
PHEW
That was a long post. And a fun one too. I will be writing on data structures tomorrow. It’s holiday season here so I’ll be able to write more.
Question What’s this
macroyou talk about?Answer Pure awesomeness. I can’t wait to know Elixir’s version of it.