danielberkompas
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?
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #ai
- #phoenix_html
- #elixirconf-us
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
josevalim
Remember you can use pattern matching:
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 likeanything when is_map(anything).Is there a certain reason why pattern matching on
%{}and[]is treatened differently?josevalim
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).
NobbZ
This makes absolutely sense, thank you for your answer.
jamonholmgren
Pattern matching does work, but is quite verbose compared to what @danielberkompas is suggesting.
Qqwy
I agree with @jamonholmgren. @danielberkompas 's solution seems like a good way to concisely read data.
Of course, the ruby
try()construct can sometimes be considered a code smell, as we’re actually breaking the Law of Demeter here, and a better solution (But often not worth the hassle/extra abstraction) would be to create a proper null-object.But… I’m getting sidetracked.
I think that
Maybe.maybe(some.field.lookup)is a very reasonable way to solve this problem. When using it, a developer explicitly acknowledges that the answer might be nil at any depth of the field lookup.Oliver
I see applications for this, definitely. Especially if not only considering structs.
But… doesn’t this actually do what you want, @danielberkompas ?
Note the use of the empty map as default. The Map.get/3 works on any struct or map as far as I can tell and returns nil or default if the value is not present. Returning by default the empty map makes it save to string these together, and the last one can simply return nil (the default anyway).
danielberkompas
@Oliver, yes,
Map.getdoes do what I want. I actually use it here to provide the functional API forMaybe:You can call it like so:
maybe(map, [:first, :second, :third]). I think it’s a little more convenient than doing a pipeline.The
maybemacro just converts calls like this:maybe(map.first.second.third)into functional calls formaybe(may, [:first, :second, :third]).Edit: Since the
Maybemodule is so small, I decided not to release a hex package for it. If you’re interested, you can find it here: Add Maybe by danielberkompas · Pull Request #14 · infinitered/phoenix_base · GitHubsilverdr
Is this still true as of today?
NobbZ
If a structs module implements the
Accessbehaviour, then you can use it with the lense like accessors.Though a 1:1 mapping to struct keys is often not really what you want for structs. Eg. for a set you might have a couple of struct keys required for the implementation, but each on its own doesn’t have any value. Instead you use
Accessbehaviour to make a lookup in the set and either returnnilfor inexisting items or the item itself if it is in the set. (This is just an example)