danielberkompas

danielberkompas

Maybe: nil protection for nested structs

I have a problem. It’s very common to have nested structs when you deal with Ecto and associations. For example, a Person can have a City, which has a name.

The problem is, city can sometimes be nil. In that case, this code will raise an error. (“nil doesn’t respond to :name”) I want something like Ruby try here.

person.try(:city).try(:name)

Elixir has a built-in thing like this, called get_in:

get_in map, [:nonexistent, :keys]
# => nil

However, it only works with maps, not structs. The documentation says it’s specifically ​_not_​ supposed to work with structs.

So, I have a proposal. We make a module with a functional API like this:

maybe(person, [:city, :name])

And a macro to pretty things up a bit:

maybe(person.city.name)

If any element in the chain returns nil, the expression returns nil.

Here’s my sample implementation:

defmodule Maybe do
  defmacro maybe(ast) do
    [variable|keys] = extract_keys(ast)
​
    quote do
      maybe(var!(unquote(variable)), unquote(keys))
    end
  end
​
  def maybe(nil, _keys), do: nil
  def maybe(val, []), do: val
  def maybe(map, [h|t]) do
    maybe(Map.get(map, h), t)
  end
​
  defp extract_keys(ast, keys \\ [])
  defp extract_keys([], keys), do: keys
  defp extract_keys({{:., _, args}, _, _}, keys) do
    extract_keys(args, keys)
  end
  defp extract_keys([{{:., _, args}, _, _}|t], keys) do
    keys = keys ++ extract_keys(args)
    extract_keys(t, keys)
  end
  defp extract_keys([{:., _, args}|t], keys) do
    keys = keys ++ extract_keys(args)
    extract_keys(t, keys)
  end
  defp extract_keys([key|t], keys) do
    keys = keys ++ [key]
    extract_keys(t, keys)
  end
end

And example usage:

import Maybe

defmodule Person do
  defstruct city: nil
end
	
defmodule City do
  defstruct name: nil
end
	
person = %Person{city: %City{name: "Portland"}}
maybe(person.city.name) # => "Portland"
maybe(person.nonexistent.name) # => nil

Is this a good idea? Should it be named something else? Is there something in the standard library that I missed?

Most Liked

josevalim

josevalim

Creator of Elixir

Is there a certain reason why pattern matching on %{} and is treatened differently?

They are different structures. Maps were designed to always map on a subset, therefore the empty map ends up matching all maps. Otherwise, imagine how useless pattern matching on maps would be if every time I wanted to take a key from the map, I had to match on all keys. The cases where I would want to do a full matching are rather rare (and can always be achieved with map_size).

12
Post #4
danielberkompas

danielberkompas

@Oliver, yes, Map.get does do what I want. I actually use it here to provide the functional API for Maybe:

def maybe(nil, _keys), do: nil
def maybe(val, []), do: val
def maybe(map, [h|t]) do
  maybe(Map.get(map, h), t)
end

You can call it like so: maybe(map, [:first, :second, :third]). I think it’s a little more convenient than doing a pipeline.

The maybe macro just converts calls like this: maybe(map.first.second.third) into functional calls for maybe(may, [:first, :second, :third]).

Edit: Since the Maybe module is so small, I decided not to release a hex package for it. If you’re interested, you can find it here: https://github.com/infinitered/phoenix_base/pull/14

josevalim

josevalim

Creator of Elixir

Remember you can use pattern matching:

case person do
  %{city: %{name: name}} -> name
  %{} -> default
end
NobbZ

NobbZ

This makes absolutely sense, thank you for your answer.

NobbZ

NobbZ

I’m always confused when matching on %{} (or #{} in erlang)… My intuition does tell me we are matching on an empty map (as we were when matching on []), but in reality it is like anything when is_map(anything).

Is there a certain reason why pattern matching on %{} and [] is treatened differently?

Where Next?

Popular in Questions Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New

Other popular topics Top

lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
lk-geimfari
What is most correct way to open, read and parse JSON file with poison? For example if we have example.json file in root of some projec...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
magnetic
Hey :wave:t3: Elixir community, I’ve been learning Elixir, and working on some side projects. My editor of choice is VSCode, and althoug...
New

We're in Beta

About us Mission Statement