Deeply update a map with key-value pairs

Let’s say we have:

game = %App.Games.Game{
  id: "owieruoiewurwer",
  players: %{
    "jwuiyeywi_oiwueroiu" => %App.Games.Player{
      admin: true,
      card: "4"
    },
    "oiwueroiquw_ncjaioq" => %App.Games.Player{
      admin: false,
      card: "6"
    }
  }
}

What it the best way to put nil in each player card ?
I used update_in/3 with Access.all/0 but can’t make it work with key-value pairs in :players.

The only solution I found it is to convert my string keys into atoms, but that would force me to iterate through the player “list” two times.

You need to replace values to build the result.

  • At the top level, we’re placing players
  • And then we need to iterate the map (as key-value pair), and for each value we need to put nil to card key.

Quick solution:

v = %{
  id: "owieruoiewurwer",
  players: %{
    "jwuiyeywi_oiwueroiu" => %{
      admin: true,
      card: "4"
    },
    "oiwueroiquw_ncjaioq" => %{
      admin: false,
      card: "6"
    }
  }
}

%{
  v
  | players:
      Map.new(
        v.players,
        fn {k, player} -> {k, %{player | card: nil}} end
      )
}
|> IO.inspect()

Note that it would be better to make functions to capture “domain logic” properly. e.g. if reset_game/1 or empty_card/1.

1 Like

That worked just fine, thanks !

I’ve never used Map.new before. Just to be sure, update_in/3 can’t be used in those circumstances ?

Since update_in needs keys to look at for a value, you cannot use it to transform all values.

If you need to change a value under a known path, you can use that for example.

1 Like

It can be used, but you need to write your own lens (Access.access_fun/2 type):

all_map_values = fn
  :get, data, next ->
    for {_, v} <- data, do: next.(v)

  :get_and_update, data, next ->
    {gets, updates} =
      Enum.reduce(data, {[], []}, fn {k, v}, {gets, updates} ->
        case next.(v) do
          {get, update} -> {[get | gets], [{k, update} | updates]}
          :pop -> {gets, updates}
        end
      end)

    {:lists.reverse(gets), Map.new(updates)}
end

game = %{
  id: "owieruoiewurwer",
  players: %{
    "jwuiyeywi_oiwueroiu" => %{
      admin: true,
      card: "4"
    },
    "oiwueroiquw_ncjaioq" => %{
      admin: false,
      card: "6"
    }
  }
}

IO.inspect(put_in(game, [:players, all_map_values, :card], nil))
4 Likes

I thought the Access module could help me bypass that kind of function definition. But it makes a lot of sense to simply explain how to access the value like this in the end.

Thanks !