Mar 20, 2020

Tech Book Face Off: Programming Elixir ≥ 1.6 Vs. Metaprogramming Elixir

Since I wasn't quite satisfied with the first Elixir book I read, and I wanted to learn more about this rich, complex programming language, I selected a couple more books to help me explore the more advanced aspects of Elixir. The first selection, Programming Elixir ≥ 1.6 by Dave Thomas, promises to cover all of the major parts of Elixir with a clean, well-written book from the coauthor of the excellent The Pragmatic Programmer. The second selection, Metaprogramming Elixir by Chris McCord, focuses on the ways that a programmer can write code to write code in Elixir, always a fascinating endeavor. Both of these books are again by The Pragmatic Programmers publishing company, since I've been mostly pleased with the books they put out. I might just have another of their books waiting in the wings for a review later this year, but let's take a look at how these two Elixir books stack up.

Programming Elixir ≥ 1.6 front coverVS.Metaprogramming Elixir front cover

Programming Elixir ≥ 1.6

This book is pretty much exactly what I expected it to be. Dave Thomas is an excellent writer who is able to explain difficult programming language concepts with an ease and fluidity that is a joy to read. His writing speaks in a way that feels entirely natural, and he gives the distinct impression of a father explaining how things work to his kids. That's a compliment; I don't mean that he lectures in a patronizing way. I mean that it's comfortable and completely understandable in the same way that your dad showing how to change the tires on a car or how to throw a baseball would be. He's good at it, and his writing flows off the page and into my head almost effortlessly.

At the same time that this book is easy to understand, the explanations are concise because Thomas has a lot to cover while keeping his promise of not making the book as long as his Programming Ruby book. He jumps right in with chapters on pattern matching and immutability, two of the main features of Elixir that will be used constantly when programming in it. Then he runs through the basics of the base types and operators in Elixir, as well as anonymous and named functions.

Next, lists and recursion are introduced together since they are inseparable in a functional language, followed by the rest of the compound data types: maps, keyword lists, sets, structs, strings, and binaries. Thomas really brought out the mystique of using lists and recursion here:
At this point, part of your brain is telling you to go read today's XKCD—this list stuff can't be useful. Ignore that small voice, just for a second. We're about to do something magical.
He goes on to show how easily values can be pulled out of lists using pattern matching, and things progress from there. The main way to process these compound data types with the Enum and Stream modules is covered at this point in the book, and other control flow structures were put off until after the more important declarative programming methods were covered. While if, cond, and case structures are still used in Elixir, they're just not as important as pattern matching and multi-headed functions.

With most of the syntax and basic features of Elixir out of the way, we're ready to tackle a non-trivial example project, so Thomas takes us through building a little application that accesses GitHub and builds a table of code repository issues for a given URL. It's a nice project to show off most of what we've learned so far before heading into the more advanced Elixir features. The more advanced features being concurrent programming with multiple processes, OTA, tasks, and agents. This was the stuff that was missing from Learn Functional Programming with Elixir, and it was covered well here. Normally, processes are a heavy-handed solution to the concurrent programming problem, but Thomas explains why Elixir is different:
[T]he cool thing about Elixir is that you write your code using lots and lots of processes, and each process has its own heap. The data in your application is divvied up between these processes, so each individual heap is much, much smaller than would have been the case if all the data had been in a single heap. As a result, garbage collection runs faster. If a process terminates before its heap becomes full, all its data is discarded—no garbage collection is required.
There's an incredible amount of power in the concurrent programming features of Elixir built on the solid foundation of the Erlang VM, and Thomas does a great job of explaining how each of them work and why you would choose to use OTA or agents or tasks in different situations.

The last few chapters of the book go into metaprogramming with macros, behaviors, protocols, and writing your own sigils. The one thing I thought suffered a little in this part was the examples. Throughout the book most of the examples were short and sweet, simply to show the syntax and how working code would be written with the newly introduced features, but with metaprogramming it's hard to understand exactly why you would want to use it if the examples are too simple and useless. To get a good understanding of when and why you would use metaprogramming requires a motivated example that shows how some tedious, verbose, ugly code can be transformed into a succinct, dynamic, beautiful piece of code that writes code. That type of example was missing from the metaprogramming section.

Setting aside that one complaint, this book was an excellent overview of Elixir from the basics of the language to the advanced concurrent programming features that make it such a compelling language for modern multi-core processors. If you need to learn Elixir well enough to start writing solid concurrent applications, or even are just curious about an entirely different and powerful way to program, Programming Elixir ≥ 1.6 is definitely worth checking out.

Metaprogramming Elixir

Whereas Programming Elixir ≥ 1.6 was a general tour of Elixir, this book focused on one specific feature of Elixir: metaprogramming. Luckily, this is the feature that was least well described in Programming Elixir ≥ 1.6, so having an entire book on it proves quite helpful. Metaprogramming Elixir is also a relatively short book, clocking in at just over 100 pages of real material, and it was a quick read.

What made the read even quicker was the fact that most of the coding examples were repeated within and between chapters, resulting in a few core examples that were extended multiple times with different metaprogramming features. This method worked great for instruction, since later examples were immediately familiar, even though it tended to pad the page count of the book. If not for the repetition, this book could have been 70 pages or less.

That's not to say it's a bad thing that the book is so short and contains a fair amount of repetition. I found the explanations to be extremely clear and easy to read. The code examples were well thought out and served their purpose in showing how to use all of the metaprogramming features, as good examples should do. Everything fit together nicely, and the chapters had a smooth flow, developing from basic macros into an advanced DSL example.

This development is split into six chapters, starting with an introduction to Elixir macros and the abstract syntax tree (AST). Similar to Lisps, Elixir code is represented as an AST that is accessible at compile time, and it can be easily changed and added to while compiling. With this power comes the tendency to over-engineer, and McCord offers up some clear warnings about overusing it:
It's easy to get caught in our own web of code generation, and many have been bitten by reckless complexity. When taken too far, macros can make programs difficult to debug and reason about. There should always be a clear advantage when we attack problems with metaprogramming. In many cases, standard function definitions are a superior choice if code generation is not required.
These warnings are sprinkled throughout the book for the various metaprogramming features. Each feature gives the programmer more power to change code at will, but at the risk of making the code an opaque, untestable maintenance nightmare. The judicious use of metaprogramming can neatly solve otherwise tedious problems, but it should only be used when necessary.

Chapter 2 gets into how nearly all of Elixir can be changed and extended with metaprogramming. The core of the language is quite small, and most of the language that's used is implemented with macros already, even the basic if expression. The example in this chapter shows how easy it is to create a unit test library using macros, and McCord takes the opportunity to discuss some metaprogramming best practices:
This [example] also highlights an effective approach to macros, where the goal is to generate as little code as possible within the caller's context. By proxying to an outside function, we keep the code generation as straightforward as possible. As you'll see later, this approach is pivotal to writing maintainable macros.
It also makes it much easier to test the macros, since most of the code will be contained in functions that you can call from tests and see what's going on, instead of trying to posit what all of the generated code looks like.

Chapter 3 shows how to use macros to generate code from data, both through reading from a file and from a web API. Not only is this ability slick as hell, it's highly performant because the code is generated only once at compile time and then during runtime it's all internal function calls—no latency-ridden I/O. The benefits of this approach cannot be emphasized enough:
Let sink in for a moment what we just accomplished in 20 lines of code. We hit a remote JSON API over the Internet and embedded the data directly into a module as functions. The API call only happens a single time when the module is compiled. At runtime, we have the GitHub data cached directly within function definitions. While just a fun example, it really shows how Elixir lends itself to extension.
The next few chapters round out the book, with chapter 4 focusing on how to test macros, followed by an extended example of writing a DSL to generate HTML code directly from Elixir syntax instead of parsing a template language, and finishing with a short chapter on some final tips, tricks, and warnings on metaprogramming in Elixir.

Metaprogramming Elixir was at the same time complete and accessible. It was short and sweet, and an excellent companion to Programming Elixir ≥ 1.6. It filled in the only real gap in the latter book, and helps give a real appreciation for one of Elixir's best features. Along with pattern matching, immutable functions, and rock-solid concurrent programming, metaprogramming makes Elixir a fascination language for a whole host of modern day back-end programming. These two books will help you get up to speed with this powerful language, and let you have some fun with that new-found power.