Hi, I’m Erika Rowland (a.k.a. erikareads). Hi, I’m Erika. I’m an Ops-shaped Software Engineer, Toolmaker, and Resilience Engineering fan. I like Elixir and Gleam, Reading, and Design. She/Her. Constellation Webring Published on Modified on

Just Add Water

Or, Using Partial Application In Elixir for Better Errors.

Today, I was adapting the generator for a recipe website written by Hundred Rabbits into Elixir. While adapting their idiomatic C code into Elixir, I needed a way to pass a predefined list of ingredients to the builder API for Recipes.

To do this, I defined a function that returned a keyword listThe cheese: cheese syntax looks a bit funny, but it’s Elixir syntax sugar for {:cheese, cheese} where the second cheese is the bound variable.:

def ingredients do
  cheese = create_ingredient(...)

  salt = create_ingredient(...)

  [cheese: cheese, salt: salt]
end

And then in my builder API, I called that function inside the builder scope:

def recipes do
  ingredients = Ingredients.ingredients()

  ...
end

In order to add an ingredient to a recipe, I used a function add_ingredient_quantity/3, used something like this:

def recipes do 
  ingredients = Ingredients.ingredients()

  create_recipe("salted cheese")
  |> add_ingredient_quantity(ingredient[:cheese], "2 oz")
  |> add_ingredient_quantity(ingredient[:salt], "1 teaspoon")
end

This worked great for ingredients that I had already defined in my keyword list, but the AccessRead more about Access here. behaviour that powers Elixir’s [] syntax returns nil if a key isn’t found in the accessed object.

I wanted a way to detect that I needed to define a new ingredient in a recipe, so that every ingredient gets its own page, like Grim Grains. My naive solution was to put a guard clause on add_ingredient_quantity that checked if the ingredient was nil:

def add_ingredient_quantity(recipe, ingredient, quantity) when not is_nil(ingredient) do
  ...
end

This would certainly complain, because of a pattern match error. However, the Access had already returned a nil, so the missing ingredient’s atom wasn’t available for an error message.

This became evident when I tried to add waterYou would want to drink some water if you were eating that much salted cheese, after all. to my recipe:

def recipes do 
  ingredients = Ingredients.ingredients()

  create_recipe("salted cheese")
  |> add_ingredient_quantity(ingredient[:cheese], "2 oz")
  |> add_ingredient_quantity(ingredient[:salt], "1 teaspoon")
  |> add_ingredient_quantity(ingredient[:water], "1 quart") 
end

This resulted in a pattern match error, but the error only knew that it was passed a nil with no context about where that nil came from.

Partial Application in Elixir

To solve this, I replaced the ingredient[:water] callI first thought about overriding the Access behaviour to return something other than nil, but I decided against it. with a partial application.

In Elixir I can use an anonymous function to partially apply a named Elixir function. Here, I wanted to get a value out of a specific Keyword List, so I chose Keyword.fetch. Keyword.fetch takes two arguments: a keyword list and a key to getRead more about Keyword.fetch/2 here.. By partially applying Keyword.fetch with ingredients and wrapping it in a with statement I can raise a helpful error:

def recipes do
  ingredients = Ingredients.ingredients()
  get_ingredient = fn key -> 
    with {:ok, value} <- Keyword.fetch(ingredients, key) do
      value
    else
      :error -> raise "#{key} not found in ingredients"
    end
  end

  create_recipe("salted cheese")
  |> add_ingredient_quantity(get_ingredient.(:cheese), "2 oz")
  |> add_ingredient_quantity(get_ingredient.(:salt), "1 teaspoon")
  |> add_ingredient_quantity(get_ingredient.(:water), "1 quart") 
end

Note that we’re using an anonymous function, so get_ingredient needs to be applied with the . operatorI initially tried to use the capture operator on Keyword.get/3, passing raise as the default as shown here. This successfully raised a helpful error on a missing key, but it eagerly raised on every key!.

Now, when I’m missing an ingredient I’ll get a helpful error message that reminds me to just add it.


Constellation Webring