My OCaml-flavoured Elixir style

by Paweł Świątkowski
23 Sep 2025

Recently I’m finding myself leaning towards writing some Elixir code in a bit different way than the community standard. I call it, perhaps unjustly and a bit tongue-in-cheek, “OCaml-flavoured Elixir”. Now, I don’t really write OCaml well (or: at all), but I spent last 3 years working with a frontend written in ReScript. And I think in recent months it started to affect how I think about the Elixir code.

But to start the conversation, let me show you what I actually mean:

def close_ticket(ticket_id, actor_id) do
  fetch_ticket = fn ->
    Repo.get(Ticket, ticket_id)
    |> Result.wrap_not_nil(:ticket_not_found)
  end

  fetch_user = fn ->
    Repo.get(User, actor_id)
    |> Result.wrap_not_nil(:user_not_found)
  end

  with {:ok, ticket} <- fetch_ticket.(),
    {:ok, user} <- fetch_user.(),
    :ok <- TicketPolicy.can_close?(ticket, user) do
      Tickets.close(ticket)
      |> Result.map(&Map.from_struct/1)
      |> Result.map(&Map.put(&1, :closed_by, user))
  end
end

I see few eyebrows raised, perhaps even some pitchforks picked up, but allow me explain why I find this style of code easier to follow.

1. Self-containing

Traditional rules of writing Elixir code, stemming from Ruby, stemming probably from Java (?), would urge us to create private functions fetch_ticket and fetch_users, wrapping database calls in a result tuples ({:ok, user}). This is an understandable coding pattern, however it leads to “jumping around” the codebase. The private function will usually be defined below the public function using it, perhaps even at the end of a file. This leads to very “choppy” experience when reading the code.

Another problem is creating non-reusable private function, which serve only to encapsulate some code logic. They often receive a lot of arguments, same as the private function calling them. These, in my opinion, are signals of suboptimal design.

Defining anonymous functions inside the public function changes the reading dynamic a bit. When you read a function, you read it top-down. This means that upon getting to the with piipeline, you have at least skimmed over the fetch_ticket and fetch_user. If you want to go back to read what they do, they are here, at hand, not somewhere in the module.

Of course, this might get out of hand with multiple complex anonymous functions defined. I try to keep 3 of them at most, otherwise it obfuscates where the actual function “meat” begins.

2. Follows prepare-execute-format flow

Majority of code actions, especially the ones modeling business actions, follow a certain flow. First you prepare data, then you execute an action, then - optionally - you format the result. Here defining a functions serve as a preparation. The with pipeline is execution and we have some lightweight formatting as a map too. I could just take this code, print it, and then draw the line where the “preparation” step finishes and execution begins.

With private functions instead, I would have the preparation conflated with execution. It’s not bad, but it’s less explicit.

3. Using OCaml-like helpers

If you are wondering what Result.map is, it comes from my Fey library. I created and published it some time ago, but never really announce it. It contains multiple functions to work with result tuples (and introduces option tuples), inspired by OCaml/ReScript.

In the case above, it allows to simplify code that would often be a case statement:

case Tickets.close(ticket) do
  :ok ->
    ticket
    |> Map.from_struct()
    |> Map.put(:closed_by, user)
    |> then(&{:ok, &1})

  {:error, error} ->
    {:error, error}
end

My codebase is full of snippets like this. If the result is {:ok, something} then do some small transformation of something and wrap it back with {:ok, _}. If not, just pass the result through.

Fey.Result.map wraps it with a convenient helper, just as OCaml does. This is much more terse and concentrating on what’s important - modifying the result value.

Similarily, the Fey.Result.wrap_not_nil helps avoid repetitive boilerplate of

case Repo.get(Ticket, id) do
  nil -> {:error, :ticket_not_found}
  ticket -> {:ok, ticket}
end

That’s it

I’d love to hear where this breaks down for other people. I truly find this code easier to follow and maintain, but at the price of deviating from a community standard. Ultimately, we always need to decide what’s more important.

And by the way, I’m sure that there is a performance penalty of using anonymous functions over private functions. I haven’t measured it (yet). But for non-critical paths this should not matter that much.

end of the article

Tags: elixir

This article was written by me – Paweł Świątkowski – on 23 Sep 2025. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.