Groups keyboard shortcuts have been updated
Dismiss
See shortcuts

[Proposal] Add Access.lazy_key/3 to allow for deferred defaults

42 views
Skip to first unread message

Ocean Lewis

unread,
Feb 14, 2025, 4:50:52 PMFeb 14
to elixir-lang-core
I've found a need to write a lazy version of `Access.key/2` when traversing nested structures where providing a default value might be expensive.

```elixir
defmodule Book, do: defstruct([:name, :isbn_13, :author])
defmodule Author, do: defstruct([:name, :related_works])
defmodule RelatedWorks, do: defstruct([:books, :papers, :music])

book = %Book{
  name: "Programming Erlang, Second Edition",
  isbn_13: "978-1-937785-53-6",
  author: %Author{name: "Joe Armstrong", related_works: nil}
}

related_works =
  Kernel.get_in(
    book,
    [
      Access.key(:author, fn -> lookup_author(book) end),
      Access.lazy_key(:related_works, fn -> lookup_related_works(book) end),
      Access.key(:books, [])
    ]
  )
```

For my use-case I also needed to differentiate between `nil` fields on structs vs maps, so I added the third parameter for an anonymous function that takes in the result of `Map.fetch/2` and returns whether or not to substitute the value with a default.

Here's the full implementation:

```elixir
@spec lazy_key(
        key,
        (-> term),
        (map | struct, {:ok, term} | :error -> boolean)
      ) ::
        access_fun(
          data :: struct | map,
          current_value :: term
        )
def lazy_key(
      key,
      default_fn \\ fn -> nil end,
      use_default? \\ fn
        data, {:ok, nil} when is_struct(data) -> :replace
        _, {:ok, value} -> {:keep, value}
        _, :error -> :replace
      end
    ) do
  fn
    :get, data, next ->
      value = fetch_or_default(data, key, default_fn, use_default?)
      next.(value)

    :get_and_update, data, next ->
      value = fetch_or_default(dt ata, key, default_fn, use_default?)

      case {data, next.(value)} do
        {_data, {get, update}} -> {get, Map.put(data, key, update)}
        {data, :pop} when is_struct(data) -> {value, Map.put(data, key, nil)}
        {data, :pop} -> {value, Map.delete(data, key)}
      end
  end
end

defp fetch_or_default(data, key, default_fn, use_default?) do
  with fetch_result <- Map.fetch(data, key),
       {:keep, value} <- use_default?.(data, fetch_result) do
    value
  else
    :replace -> default_fn.()
  end
end
```

I was about to open a PR to add in the implementation but then I saw that I should propose the idea here first (it's my first time submitting a proposal).

I'd love to hear your thoughts on the idea.

– Ocean
Reply all
Reply to author
Forward
0 new messages