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.

Where Next?

Popular in Questions Top

_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
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
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lists...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: <h1>Create Post</h1> <%= ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

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
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
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
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
New
vegabook
I’m brand new to Phoenix and I have stripped one of the demo applications to the bone. I just want to get an svg up on the screen. Here i...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
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

We're in Beta

About us Mission Statement