darwin67

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

mudasobwa

Creator of Cure

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

mudasobwa

Creator of Cure

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

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.

al2o3cr

al2o3cr

+1 to @fuelen’s point about JSON. There are a LOT of terms that won’t cleanly round-trip through that process, so you’d be better off explicitly documenting what will - or switching to a serialization format that’s higher-fidelity.

Adzz

Adzz

If the user implements the anonymous function passed to run, are they not in charge of what is returned? In which case they will know whether the function returns a map with string keys or atom keys because they implemented the function to do that? Or am I misunderstanding?

Where Next?

Popular in Questions Top

vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
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
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
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New

We're in Beta

About us Mission Statement