TIL: Using functions as keys with update_in in Elixir

If you aren't already aware, Elixir has some very handy convenience helpers for accessing and updating nested values: get_in, put_in and update_in. For example, you can use get_in to access a value deeply nested in a JSON response by passing in an array of keys:

json = %{
  "data" => %{
    "level_1" => %{
      "level_2" => %{
        "value" => 1
      }
    }
  }
}

get_in(json, ["data", "level_1", "level_2", "value"])
# 1

Or use put_in to set a nested value:

# Same JSON

put_in(json, ["data", "level_1", "level_2", "value"], 2)

# Sets the value and returns a new map
%{
  "data" => %{
    "level_1" => %{
      "level_2" => %{
        "value" => 2
      }
    }
  }
}

Similarly, you can use update_in to update a deeply nested value:

# Same JSON as before

update_in(json, ["data", "level_1", "level_2", "value"], &(&1 + 1)

# Updates the value and returns a new map
%{
  "data" => %{
    "level_1" => %{
      "level_2" => %{
        "value" => 2
      }
    }
  }
}

These helpers are fantastic and gets rid of a lot of boilerplate code. However, there may be a case where you're given an array of items and you want to update a specific one. You can't do that just by passing in the index value of the array you want to update. Luckily, you can use a function as a key.

Let's define our helper function to use as a key. You might have to know ahead of time what operation you're expecting to happen to your structure.

# Helper function to access an item in an array
item_at_index = fn index ->
  fn :get_and_update, list, next_op ->
    item = Enum.at(list, index)

    # Update the item with the expected change
    {_, updated_item} = next_op.(item)
    # Must return a tuple of the item in question and the updated version of the container. In this case, the updated list
    {item, List.replace_at(list, index, updated_item)}
  end
end

Now let's use that function as a key to update a map.

json = %{
  "data" => %{
    "items" => [
      %{
        "value" => "foo"
      },
      %{
        "value" => "bar"
      },
      %{
        "value" => "foobar"   # I want this to be capitalized
      }
    ]
  }
}

# Update the map
update_in(json, ["data", "items", item_at_index.(2), "value"], &String.upcase(&1))

# Our updated map
%{
  "data" => %{
    "items" => [
      %{
        "value" => "foo"
      },
      %{
        "value" => "bar"
      },
      %{
        "value" => "FOOBAR"
      }
    ]
  }
}

Wrap-up

Elixir gives us the tools to write amazing code and there's always plenty to explore. Take a look at at the docs for update_in , get_in, and put_in to learn more.

#elixir   •   #TIL