darwin67
Pattern match on map keys that can be atoms or strings
Hi folks,
I’m building a library and I have an interesting case when it comes to pattern matching with maps.
Would appreciate any help or pointing me in some directions.
Goal
I’d like to be able to have a map that’s deserialized over the wire from JSON, but can still pattern match with atoms if specified.
Here’s an simplified snippet of the library, where run is the function I exposed from the library, and the anonymous function is provided by the user.
%{foo: value} = run("do something", fn ->
%{foo: "bar"}
end)
As someone reading this, without knowing what run is doing, the anonymous function just returns a map %{foo: "bar"}, which will be the return value of run itself as well, and a user would want to pattern match against it.
Now here’s the interesting part. The reason run is a wrapper in the first place is it do some extra things to make sure this function is idempotent, and it communicates elsewhere to store the state of the returned function.
So from my perspective of the library author, when I get the result of do something again over the wire and deserializes it, it results in %{"foo" => "bar"} instead, and I have no way to really know beforehand what shape of the data the user is expecting it in.
Which means the attempted pattern match will raise an error.
Things I’ve thought of
If the pattern match errored, catch it and use String.to_existing_atom to attempt again.
Might not be a big deal if the map is flat, but if the map is nested, doing that for each key iteration is likely going to slow things down unnecessarily.
Then obviously I don’t want to just do String.to_atom because we all know atoms are not GC’d. Also just preemptively converting all keys in a map to atoms is also likely to be waste of CPU cycles if they’re not utilized.
Technically speaking, this can also apply to the values of maps as well since someone would want to declare a map that have atoms for both key and values for some reason.
Then that’s even worst.
So rephrasing the question again. How would I take a deserialized JSON map, but will be able to pattern match against a user defined map regardless of the type being a String type or :atom type.
Thanks in advance!
Note
I care less about other data types atm since they aren’t like String and :atom, where essentially they’re string literals in different presentations with some different characteristics, but are somehow not fully compatible with each other.
If you’ve used Ruby/Rails before. I basically want something like HashWithIndifferentAccess
require "active_support/hash_with_indifferent_access"
framework = ActiveSupport::HashWithIndifferentAccess.new
framework[:name] = 'Ruby on Rails'
puts framework[:name] # Ruby on Rails
puts framework['name'] # Ruby on Rails
And yes, I know it’s generally frowned upon in Elixir, but as a library author, I don’t have control over the data a user might be putting into it.
Hope this helps with the context.
Most Liked
mudasobwa
Generally speaking, in such a case the approach would be to accept the options in a call to run/3 as for instance jason does.
# def run(name, options \\ [keys: :strings], fun)
%{foo: value} = run("do something", keys: :atoms, fn ->
%{foo: "bar"}
end)
%{"foo" => value} = run("do something", keys: :strings, fn ->
%{foo: "bar"}
end)
# the above is default
%{"foo" => value} = run("do something", fn ->
%{foo: "bar"}
end)
Or simply document run/2 as returning binary keys always.
mudasobwa
I am a bit lost about which part of the code above comes from the library, and which is expected to be written by the user.
If you want run/1 to return something that would happily match to both %{foo: :bar} and %{"foo" => :bar}, it’s impossible (unless you override Kernel.=/2 which I would strongly rule out.)
Could you please clarify?
fuelen
JSON is a very limited and is not 100% compatible with elixir terms.
If you want to use JSON, then you have to expose this internal detail to the end user, since you can’t convert {:ok, %{{:a, :b} => ~D[2021-01-01]}} to JSON and back without additional efforts. There will be some limitations and user should know about them.
So, I’d suggest adding an option to run/2, so the end user can specify which keys he’s interested in. Like this is done in Jason.decode/2 with :keys option.
Probably, the easiest way to receive the same data which user passes is by using :erlang.term_to_binary and :erlang.binary_to_term for serialization, instead of JSON.
Popular in Questions
Other popular 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
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








