SW engineering, engineering management and the business of software

subscribe for more
stuff like this:

The Sublime Developer Efficiency of Elixir, Phoenix and LiveView, Part 1

Editor’s note: This is part one of a short series. You can find part two here.

I’ve spent the last month and change working on a little side project.

The interesting thing about this one is that it’s written completely in Elixir. This is a post about my experience doing so.

Elixir is a language that came out of the heyday of Ruby on Rails about a decade ago. It was originally billed as “Ruby on the Erlang VM”, trying to blend the best of both worlds. Spoiler, I believe it has largely succeeded.

It’s still got quite a bit of Ruby influence, but overall, it’s much more functional. You will see maps, filters and reduce’s all over your code base.

From the Erlang side of the family, you’ll see a lot. There’s no mutability. Variables cannot be changed, you can only create new ones from combining or transforming existing. Pattern Matching is everywhere and it’s still quite powerful. You can trivially call down to Erlang primitives whenever you need to.

The Ruby influence is more in style and syntactic sugar. You’ll see your def’s and end’s and ->’s. You can skip parentheses around function arguments if it doesn’t introduce ambiguity. There’s no early return, you always return the last expression. Map and list syntax will be fairly familiar. The @ prefix denotes attributes / decorators. Atons.

It’s got some quirks. The native list primitive is a classic linked list, not an array. This means that prepend is cheap, but count, insert, append is expensive. It also means you can do head/tail car/cdr operations quite easily. Keyword lists as a weird list/map hybrid.

Digging into the pipe operator

The |> operation is a particularly good example. To most newcomers, it’s a weird, inscrutable symbol whose operation isn’t easily intuited.1 Yet once you get used to it, there’s quite a bit of elegance to it, and even historical reference to the pipe operation in the shell.

For those new to the language it works like this: take the input of the pipe operation (x below) and pass it as the first argument of the target function of the pipe operator.

x
|> function(y, z)

is equivalent to

function(x, y, z)

The elegance comes from chaining these together like so;

x
|> function(y, z)
|> other_function(a, b)
|> yet_another_function(c)

If this is the last expression in a function you return the results of the final yet_another_function

You can also bind the final output to a variable like so:

final_output =
  x
  |> function(y, z)
  |> other_function(a, b)
  |> yet_another_function(c)

The pipe metaphor breaks down as you start with x, do a ton of transformation on it and your eyes have to scan way back to the beginning to see what variable you are assigning the final results to.

It kind of micro-example of the entire macro-experience of using Elixir:

  • It’s quirky
  • There’s a learning curve
  • Taking the time to climb that curve can result in elegant solutions
  • The endgame is high developer efficiency

An actual example

To see an actual code snippet with pipes in action:

collected_errors =
  Date.range(start_date, yesterday)
  |> Enum.to_list()
  |> Enum.filter(fn _ ->
    random_number = :rand.uniform(100)
    random_number < 75
  end)
  |> Enum.map(fn date ->
    make_map_of_attrs(date, user, some_bool, some_int)
  end)
  |> Enum.reduce([], fn entity_attr, all_errors ->
    case create_entity_with_attr(entity_attr) do
       {:ok, _entity} ->
         all_errors
       {:error, err_msg} ->
         [err_msg | all_errors]
    end
  end)

If the above looks like moon runes, it’s probably just because the syntax is new to your eyes. After a week or so with the language, it reads very clearly. Walking thru that snippet, we start with the second line:

  1. Create a date range between two dates (a range is essentially an entity with a start and end.)
  2. Take that range and convert it into a linked list, filling in all the dates inbetween
  3. Throw away about 25% of the dates, randomly
  4. For each date, create a struct (map) which contains kv pairs of data, including the date.
    • The output of this is a list of maps which gets passed on
  5. Take that list of maps and create defined structs. Typically a create_ prefix means you are creating and object and often storing it in some persistence layer.
    • If no error occurred during the create statement, just pass along the error accumulator unmodified.
    • Any errors that do occure get prepended into the all_errors accumulator during the reduce operation.
  6. Finally we put the result of the reduce operation (any accumulated errors) and bind that to the collected_errors variable.

The endgame is high developer efficiency

I’m very, very fond of go, but a typical implementation of equivalent functionality would typically be at least 5x longer and certainly more irritating to write.

So, back to that macro experience of using Elixir:

  • It’s quirky
  • There’s a learning curve
  • Taking the time to climb that curve can result in elegant solutions
  • The endgame is high developer efficiency

That last bullet point, in my relatively short experience with Elixir, I’m fairly certain the overall developer productivity is quite high. Likely in the top quartile or better of all development environments (possibly even top 10%). There are others who think so as well.

The combination of the last two bullets is what I refer to as sublime efficiency in the title of this post. This is a rare combination. I believe go to be a great language for “IDE to prod” developer productivity, but I don’t believe it is a particularly elegant language. Lisp and it’s variants can be quite elegant but you will run into feature velocity problems at some point (deployment, ecosystem, hiring…).

Developing in a modern functional language, with great safeguards (pattern matching, guards, type annotations, strong testing libraries, etc.) somehow allows developers to be efficient, write elegant code and also achieve high feature velocity.

It’s clearly not a perfect language or ecosystem. The deployment story is not as good as go.2

But it is one that I could see a lot of companies using as a competitive advantage with respect to time to market of getting product out.

All you have to do is get over the learning curve.


Author’s Note:

This post is the first of a two part series. You can find the second part here.

Lastly, I do live code streaming about Elixir, Phoenix, and LiveView on twitch.tv. You should follow me there.


Footnotes:

  1. OCaml and F# have a similar operator and predate Elixir. The creator of Elixir himself thinks it came from F#.
  2. To be fair, no language ecosystem has a deployment story as good as go. I would put elixir above any other dynamic language like python, ruby. Being dependent on the erlang VM, it’s more like deploying a Java app (if you squint). The tooling is there and actively getting better.


in lieu of comments, you should follow me on bluesky at @amattn.com and on twitch.tv at twitch.tv/amattn. I'm happy to chat about content here anytime.


the fine print:
aboutarchivemastodonblueskytwitchconsulting or speaking inquiries
© matt nunogawa 2010 - 2023 / all rights reserved