Today is the two week mark since I started to rewrite my home services in Elixir.
Currently ad-hoc bash scripts run my home services:
- Continuously build pollen documentation with bash scripts and cron in containers.
- Fetch scanned pdfs from a ftp server and upload to iCloud with a bash script.
- Continuously build my mono-repo with bash scripts in containers.
- Slackbot posts to my slack channel about build status, home status, etc.
I have always wanted something unified, like what Jane Street does to their tech stack with OCaml. I have been searching a language that has excellent tooling, composable local libraries, and self-contained releasing process. The tooling should include at least the following supports:
- Official code formatter: I am my own code reviewer, so I don’t want to see unnecessary changes.
- Awareness of project structures: no hand-made build manifest such as Makefiles.
- Testing being the first citizen: no manual testing from command line; all testing configuration and environment should be self-contained into the project. The cost of running testing is extremely low so that I would run testing as often as possible during development, which saves time for maintaining services at home.
- A unified package manager for share third-party libraries: if HTTP client is not built-in, I should be able to fetch a HTTP library in one command.
- building local libraries: local libraries should be easy to build and reuse.
A few more things are also on the list, but they’re not hard requirement:
- Supports building docs from the source: library docs should be at hand when I want it, even for my own local library.
Excellent supports on cross machine communication. For most language, this just means
- To have third party libraries for networking (such as zeroMQ).
- To have libraries for (de-)serialization.
- ML-style strong, static type system: this is just a wish though, not a show stopper.
Erlang came to my attention last year because of Joe’s thesis. The baked-in cross machine communication grabbed my attention immediately. After reading posts and watching talks about Erlang, Elixir showed up. A few months back in May, in the middle of the pandemic I started to experiment with Elixir.
Finally, two weeks ago I decided to give it a try to convert all my home services to Elixir.
So far, it meets almost all the requirements. The whole journey is quite pleasant. I like many tiny things in the language and in the tooling:
The language is dead simple. Just like scheme, elixir’s semantics are simple. What you get is a module for namespaces, pure functions and behaviors for abstraction (similar to abstract class for C++ without the inheritance shit), process for sending and receiving events, and that’s it. The rest of the castle is built on those simple concepts; there are not many surprises when you use the language.
Pattern matching in function parameters produces simpler logic in function body. My function Git.last_hash should only operates on a cloned git directory. So the function prototype is last_hash(%Git{cloned: true}=git), and I don’t need to care about the false case because at run time that will be an unrecoverable logic error (such as my code forget to call clone). In C++ we usually do DCHECK, in Python we assert, and in Elixir we just need to pattern match in the parameter and let the runtime fail the unmatched cases.
No early `return`, which encourages small functions. There is no return keywords in Elixir. So functions are usually small, composable.
Modern toolings support high productivity. I often need to clone and push git repo in many scripts. Wrapping common git command into a reusable library is simply mix new git in my workspace/common directory. Using the library is straightforward. It takes me only a few seconds to refactor a common library such as shell for running shell command and include the library into the application.
Hot reload during development makes productivity high. Erlang-vm supports two versions of compiled code. In the REPL, I can type recompile to recompile everything including changes in dependencies. Majority of silly typing errors get a quick fix. When working on the function Git.last_hash, I decided to truncate the hash string to only 8 chars. Change the source, type recompile in REPL, and run my previous expression again prints 8 chars. This hot loading magic excites me even more when I have one process running GenServer periodic job, and after typing recompile, the periodic job picks up the new code and start to print out new logs.
{:ok, result} and {:error, reason} convention helps reduce type mismatch and helps error propagation. I have always wondered why in production our code in golang generally handles errors better compared to Python. Because the idiomatic way of handling errors in golang is to return (value, error). Returning error instead of throwing errors forces callers to handle errors, which helps building good quality programs. Similar to golang, in Elixir I feel the same thing. Always returning {:ok, data}
or {:error,
reason}
has helped proper error propagation.
Not to mention the actor programming model. That’s the simplest model to handle concurrent programming.
I know you want to ask: do you feel anything unpleasant? Not really so far.
A notable one for me is the dynamic typing. I feel uneasy when working with a language that’s not statically and strongly typed or a language that allows random null pointers. C, C++, Python, Golang, untyped Racket, you name it. I liked OCaml, not because the safety that type system brought to me, but strong type system with type inference helps me get away with silly errors during refactoring: it’s too easy for me to get frustrated by those errors; I just want to focus on logic errors, not typos, mismatched return values, undefined names, fall through enumerations, etc., even if those are relatively easy to fix during development.
But I do want to give Elixir a try. One reason is because of Joe’s thesis. After years of working on large-scaled, and distributed, systems, I believe the idea behind the paper is more and more relevant. The second reason is the difference of the community. If you go to OCaml’s community you’ll see people talking mostly about languages, type system, concurrency (and how hard it is to bring gRPC into OCaml). whereas to Elixir community, people talk about how to handle reconnection after networking failures, how to build a cluster. I do want to get closer to the infrastructure side.