Kernel.put_in on nil values

I’m trying to use Kernel.put_in to insert data on nested maps. This works fine, unless I want to put data on a nil-value.

Example:

m = %{}
Kernel.put_int(m, [:foo, :bar], “baz”)

raises:

** (ArgumentError) could not put/update key :bar on a nil value
(elixir) lib/access.ex:309: Access.get_and_update/3
(elixir) lib/map.ex:694: Map.get_and_update/3
(elixir) lib/kernel.ex:1831: Kernel.put_in/3

since there is no element with the key :foo in the map. How would you implement a function that will create an empty map with the key :foo if it does not exist yet?

Expected result:

m = %{}
my_put_in(m, [:foo, :bar], “baz”)
%{foo: %{bar: “baz”}}

3 Likes

If you plan on working on a map with a regular structure if might be a good idea to use a struct. This would give you a default structure that you can work on. If you do you’ll have to change your call to be

put_in(m.foo.bar, "baz")

as structs don’t use the Access behaviour.

Otherwise you could create a function and pattern match against it to make sure it’s populated, such as:

def validate(%{foo: %{bar: _}} = data), do: data
def validate(%{} = m), do: Map.put(m, :foo, %{bar: ""})
1 Like

Hey Alejandro,

unfortunately both structs and pattern matching are probably not viable since I’m working on data coming in from json objects, where the structure is not (or only loosely) known before.

I will dig into the Access behaviour and have a look if I can find some clues there.

1 Like

Maybe you can do something like this?

defmodule Test do
  def my_put_new(_map, [key | []], value), do: %{key => value}
  def my_put_new(map, [h | t], value) do
    Map.put_new(map, h, my_put_new(map, t, value))
  end
end

iex(5)> Test.my_put_new %{}, [:foo, :baz, :bar], "hi"
%{foo: %{baz: %{bar: "hi"}}}

It doesn’t work in any other case though :wink:

iex(11)> Test.my_put_new %{bar: "haha"}, [:a, :b, :c], "hi"
%{a: %{b: %{c: "hi"}, bar: "haha"}, bar: "haha"}

Or you can look at how put_in function works.

1 Like

I was able to figure it out by having a look at Kernel.put_in and Access.get_and_update. The key was to override the Access.get_and_update(nil, key, fun) function (which basically meant, re-implementing the whole Kernel- and Access.Access.get_and_update functions, unfortunately. I’m also not sure if there are still bugs there, but for other people wondering, here is the gist: https://gist.github.com/ifoo/334d11f4b7cb5491447d39539ebf85bb

Which produces:

iex(2)> ExEvent.put_in(%{bar: “haha”}, [:a, :b, :c], “hi”)
%{a: %{b: %{c: “hi”}}, bar: “haha”}

2 Likes
iex(8)> ExEvent.put_in(%{bar: "haha"}, [:bar, :c], "hi")      
** (FunctionClauseError) no function clause matching in ExEvent.access_get_and_update/3

I think that what you want to do, you should do something like this:

iex(8)> Map.merge(%{bar: "haha"}, %{bar: %{b: %{c: 1}}})
%{bar: %{b: %{c: 1}}}

Or it should be added to https://gist.github.com/ifoo/334d11f4b7cb5491447d39539ebf85bb:

defp access_get_and_update(map, key, fun) do
    Map.get_and_update(%{}, key, fun)
  end

Try this one

%{}
|> put_in( [Access.key(:foo, %{}), :bar], “baz”)
4 Likes