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
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #ai
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #elixirconf-eu
- #metaprogramming
- #hex










Showing Posts 17 to 8- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
tomekowal
I am not sure
?operator exactly fits the current feature set.There is multiple semantics at play here:
For maps when the key is missing we get a runtime error KeyError.
For structs when the key is missing we get a compile-time error (great for preventing typos)
Elixir can track even nested structs but we need the information about them in the source code. That means it is impossible to create a macro that allows accessing nested struct and preventing typos at the same time. E.g. nothing stops you from populating
person.citywith a string instead of city struct.We could resign from “typo safety” and do the checks at runtime but then we have next choice:
nil?I’d say KeyError is better because we are talking about structs and they populate all keys by default with nils.
Accessing nested values is very much Maybe monad. So if we want to have an infix operator for nested access I’d use
>>>that stands for bind in GitHub - witchcrafters/witchcraft: Monads and other dark magic for Elixir · GitHuband a full example of all the things I talked about:
To sum up: you can solve the problem with a two-line function defining an infix operator. Just be careful to think about the semantics of missing keys. For structs missing a key is definitely a typo. For maps, you could add a case like:
because missing keys might be a legitimate thing.
And yet another question is: what if the city is suddenly a string instead of a struct? Do you return that string or throw an error? I think it is best to leave that decision to people implementing actual business logic and leave it out of the language
aglassman
I really wish this were the case. Kotlin provides this out of the box, and it’s super convenient.
person?.city?.nameKotlin uses this
?syntax for a lot of other things concerning nulls, which don’t necessarily apply to Elixir, but I think if it worked for structs, it would be a really nice convenience.7rans
Well, I for one just came into need of this today. Given how clever a language Elixir is, I’m actually surprised there isn’t a feature like this already
Syntax wise I wonder if
?(in place of.) could work.That reads pretty well to me.
[Update]
I ended doing this, e.g.
(person.city || %{name: ""}).name. Which is okay for one level, but still rather ugly for more, e.g.((person || %{city: nil}).city || %{name: ""}).name.brady131313
I ran into a similar issue where I had lots of structs that were basically wrappers for maps, where I just wanted the compile time guarantee of the structs. I wrote a small using macro that I can add to the top of a struct and then use it with the Access module
silverdr
To be frank I don’t know. Now it doesn’t work. So I must have done something differently and can no longer reproduce it
UPDATE:
The thing that works for my case is something like:
Yet I am surely not happy with how it looks. OTOH
get_in(row, [:column])works nicely when row is nil but fails when it is not.get_in(row, [Access.key(:column)])does the opposite. It works when row is not nil, but fails when it is. And I didn’t come up with a more elegant one-liner to cover both cases. And this doesn’t even touch nested structures/associations.This makes me wonder.. is there really no need for something like an equivalent of Rails’
tryin Elixir/Phoenix? Sure I could probably implement something like that somewhere in every project, but that kind of “safe traversal” does look like a very common case to me. At least coming from the Rails (and a few other languages/frameworks) world.NobbZ
Hmmm…
For me I can not use
get_inon an arbitrary struct…What have you done?
silverdr
What I actually need is a “safe navigation” through Phoenix “models”, exactly the case brought on the very beginning of this thread. A quick test shows the
get_into work so it looks to me that either things changed or there are some gotchas I am not aware ofNobbZ
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)silverdr
Is this still true as of today?
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 · GitHub