Taking Control of Map Sort Order in Elixir

Yellow bird reading up on some helpful tips.
Image by Annie Ruygt

This post is about customizing IEx to sort map keys by default after a change in OTP 26. If you are looking for place to launch your latest Elixir project, check out how to get started on Fly.io! You could be up and running in minutes.

If you’ve recently upgraded to Elixir 1.14.4 and OTP 26, you may have noticed that map keys are no longer showing in a predictable order. Fear not! Understanding why it’s happening and taking the time to sort the keys by default can save you some headaches. In addition, we can explore a few other neat tricks with customizing IEx.

Problem

You recently upgraded your OTP to 26 or a later version. To your surprise, the maps you inspect now display with their keys in seemingly random order. In IEx, it can be frustrating to find things when map key order has no rhyme or reason. For example:

data = %{a: 1, b: 2, c: 3, d: 4}
%{c: 3, a: 1, d: 4, b: 2}

Did you notice how the keys in the output version are arranged?

How can we predictably sort the map keys when working in IEx?

Solution

Why this is happening

The recent internal changes in OTP 26 is the culprit. As stated in the Erlang OTP Release Notes:

Some map operations have been optimized by changing the internal sort order of atom keys. This changes the (undocumented) order of how atom keys in small maps are printed and returned. The new order is unpredictable and may change between different invocations of the Erlang VM.

The Elixir 1.14.4 release notes also calls this out:

When migrating to Erlang/OTP 26, keep it mind it changes how maps are stored internally and they will be printed and traversed in a different order (note maps never provided a guarantee of their order). To aid migration, this release adds :sort_maps to inspect custom options, in case you want to sort them before inspection.

Let’s see what that :sort_maps option is and how it’s used.

iex> data = %{a: 1, b: 2, c: 3, d: 4}
iex> IO.inspect(data, custom_options: [sort_maps: true])
%{a: 1, b: 2, c: 3, d: 4}

When we call inspect explicitly, there is a new custom_options of sort_maps: true. That can be helpful when logging a map in production, but it’s a lot to type when we’re developing locally.

Let’s see if we can make the local experience better.

Sorting by Default

When working in IEx locally, we want it sorted by default. This can be achieved by configuring the .iex.exs file. It can be located in your home directory or with your project. If you don’t have one yet, create it. Add the following code:

IEx.configure(
  inspect: [
    custom_options: [sort_maps: true]
  ]
)

Test it out in a new IEx session:

iex> data = %{a: 1, b: 2, c: 3, d: 4}
%{a: 1, b: 2, c: 3, d: 4}

Nice! Sorted by default!

What else can we do in our .iex.exs file?

Over time I’ve picked up some IEx customizations from others. Here’s my current .iex.exs file:

Application.put_env(:elixir, :ansi_enabled, true)

timestamp = fn ->
  {_date, {hour, minute, _second}} = :calendar.local_time
  [hour, minute]
  |> Enum.map(&(String.pad_leading(Integer.to_string(&1), 2, "0")))
  |> Enum.join(":")
end

IEx.configure(
  colors: [
    syntax_colors: [
      number: :light_yellow,
      atom: :light_cyan,
      string: :light_black,
      boolean: :red,
      nil: [:magenta, :bright],
    ],
    ls_directory: :cyan,
    ls_device: :yellow,
    doc_code: :green,
    doc_inline_code: :magenta,
    doc_headings: [:cyan, :underline],
    doc_title: [:cyan, :bright, :underline],
  ],
  default_prompt:
    "#{IO.ANSI.green}%prefix#{IO.ANSI.reset} " <>
    "[#{IO.ANSI.magenta}#{timestamp.()}#{IO.ANSI.reset} " <>
    ":: #{IO.ANSI.cyan}%counter#{IO.ANSI.reset}] >",
  alive_prompt:
    "#{IO.ANSI.green}%prefix#{IO.ANSI.reset} " <>
    "(#{IO.ANSI.yellow}%node#{IO.ANSI.reset}) " <>
    "[#{IO.ANSI.magenta}#{timestamp.()}#{IO.ANSI.reset} " <>
    ":: #{IO.ANSI.cyan}%counter#{IO.ANSI.reset}] >",
  history_size: 50,
  inspect: [
    pretty: true,
    limit: :infinity,
    width: 80,
    custom_options: [sort_maps: true]
  ],
  width: 80
)

Here’s how a default IEx looks: Screenshot of a default IEx shell with coloring and the prompt

Here’s how my customized one looks. It features improved syntax colors and a customized prompt. Screenshot of a customized IEx shell with coloring and the prompt

There are other customizations in there. Feel free to adapt or adopt only what you care about.

Discussion

We’ve learned why the key order is no longer predictable, how to sort keys before inspection, and how to configure IEx to sort keys by default. We’ve also seen that there are more customization possibilities for our IEx shell. (Beware the rabbit hole!)

If this tip was helpful for you, be sure to share it with your team!

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!