For educational purposes (and fun), I am currently rewriting the openiban.com IBAN validation and generation service in Elixir. Because most of the validation is simple pattern matching, I expect Elixir to be a great choice for the task.
Openiban currently uses a MySQL database to look up BIC codes and some additional data from the bank code, that is included inside every IBAN. My goal is to get rid of the database and simply generate all the code that will be used to perform the lookups.
To get started with this endeavour, I’ll come up with a macro that can be used to generate a case expression which matches all valid https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 country codes.
This post will show my train of thought and the problems I encountered while developing the macro. I have read Chris McCord’s awesome Metaprogramming in Elixir a while ago, but never used any of it in a real project. What I write here might not be the most idiomatic solution, but maybe someone can get unstuck by reading about the problems and intermediate steps that I went through while coding the macro.
It’s been a while since I read the book, so I decided to get started with what little I already knew about my macro: the way it should be called.
I also knew the code that the
take_prefix/2 macro would have to produce:
An example invocation of this function should return the following result:
This can’t be too hard, right?. Let’s create a module that will house the macro:
My next step was to try and generate the AST representation of the conditions that would be put in the body, or block, of my case condition.
So let’s see what this generates:
Looks great. So let’s try to inject that into a
case ... do expression:
But this doesn’t work as expected:
So what is the problem? I found it very useful to inspect the generated AST with the
Macro.to_string functions. I know it’s a little hard to see at first, but there is a surplus list around every generated AST node:
This is how the AST of two actual case branches looks like:
My first intuition was to simply call
List.flatten/1 on the nested list of AST nodes. At a second thought (on the next morning) I discovered that
quote always returns a list. So I simply had to concatenate (
 ++ ) the lists instead of prepending (
[x | xs]) each case branch to the accumulator.
Let’s try again:
Okay, this looks promising. How about the case when no match is possible?
CaseClauseError is raised, because the ‘no match’ case has not been implemented yet. Here is the full implementation of the macro, which fixes this problem.
The first version of the macro also used
Enum.reverse/1 to correct the order of AST nodes. Otherwise the default condition
"_" -> ... would come first inside the list of
case expressions, and all other conditions would be ignored. The final version always appends to the list of AST nodes, making the
reverse operation unneccessary.
The next part of this series will show how macros can be used to create a simple matcher DSL and deal with non-AST-literal arguments in the
take_prefix/2 macro. This will come in handy, because it will allow the macro to use prefixes that have been e.g. read from a file or placed in a module attribute.