Replacing singular value of map within a list of maps based on key, value conditions

How would I be able to update the “value” of the map that has “key” => “mathematics”? I’m a little confused as to where to begin, especially since the map is so nested. I tried playing around with some Map and Enum functions to do it and

current_tasks = 
%{
	“resource” => “4234234”
	 “name” =>  “To Do”
	 “definitions” => [
		 %{
     			"description" => “school”,
      			"enabled" => false,
      			"key" => “mathematics”,
      			"value" => “calculus”
   		 },
		 %{
     			"description" => “school”,
      			"enabled" => true,
      			"key" => “english”,
      			"value" => “intro to poetry”
   		 }, 
		 %{
     			"description" => “school”,
      			"enabled" => false,
      			"key" => “foreign language”,
      			"value" => “spanish”
   		 },
	]
}

I tried to do something like this but kept running into some issues with how to move forward/condense it. Note that this code below does throw a functionclauseerror due to the 3rd arg in Map.update!, but that’s not what confuses me.

    new_env =
      Enum.each(current_env["definitions"], fn map ->
        case map["key"] do
          "mathematics" -> Map.update!(map, "value", "math")
          _ -> IO.inspect "not it"
        end
      end)

Any thoughts on how I can condense solution above and make it a lot better would be nice, as I am new to learning elixir, this has me stumped and I don’t want to brute force anything when I know there are others who can help me make it more elegant.

1 Like

When you have this nested maps or even lists, there are some Kernel functions like update_in that come really handy. If you combine that with the Access module you can achieve something like the following to tackle your problem:

update_in(current, ["definitions", Access.all(), "key"], fn e ->
    if e == "mathematics", do: "math", else: e
end)

update_in/3 will traverse the given structure following the given keys [definitions, Access.all(), "key"]

The Access module has many other functions similar to Access.all/0 like Access.filter/1 which you might want to take a look at in case you want to tweak the snippet above.

5 Likes

Using Elixir for years now and had no idea about Access.all(). Thanks for pointing it out!

2 Likes

Great solution. I love combining that with pattern matching within the anonymous function:

update_in(current, ["definitions", Access.all(), "key"], fn
  "mathematics" -> "math"
  e -> e
end)

It makes the mapping really clear

4 Likes

Wow, I’ve never seen pattern matching used like this before.

> update_in(current, ["definitions", Access.all(), "key"], fn
   "mathematics" -> "math" 
    e -> e 
end) 

I don’t understand how this works. How can you do this with an anonymous function.

1 Like

Such a nice feature! I’m playing around with Access.filter/1 right now because it seems to be best suited for my issue, since I’d like to change the value of “value” if “key”==“mathematics”. It is however returning FunctionClauseError for Kernel.update_in.

new = Kernel.update_in(current, ["definitions", Access.filter(&(&1["key"] == "mathematics")), "value"], fn -> "math101" end)

My assumption (may be faulty) was that Access.filter(&(&1["key"] == "mathematics")) would only return elements of the list of maps matching “key”== “mathematics”, and "value" would return “calculus”, allowing me to update to “math101” in this instance. Is this assumption incorrect or am I missing the issue completely?

Ohhh, I forget anonymous functions can have multiple clauses

I love pattern matching. Instead of using case or if see if you can solve the problem with a simple pattern match.

1 Like

There probably is a better way …

defmodule Demo do
  defp make_put({key_name, key}, {value_name, value}) do
    fn
      %{^key_name => ^key} = definition, rest ->
        [Map.put(definition, value_name, value) | rest]

      definition, rest ->
        [definition | rest]
    end
  end

  def make_definitions_updater(key, new_value) do
    fn list ->
      List.foldl(list, [], make_put(key, new_value))
    end
  end
end

current_tasks = %{
  "resource" => "4234234",
  "name" => "To Do",
  "definitions" => [
    %{
      "description" => "school",
      "enabled" => false,
      "key" => "mathematics",
      "value" => "calculus"
    },
    %{
      "description" => "school",
      "enabled" => true,
      "key" => "english",
      "value" => "intro to poetry"
    },
    %{
      "description" => "school",
      "enabled" => false,
      "key" => "foreign language",
      "value" => "spanish"
    }
  ]
}

updater = Demo.make_definitions_updater({"key", "mathematics"}, {"value", "math101"})
new_current = update_in(current_tasks, ["definitions"], updater)
IO.puts("#{inspect(new_current)}")
$ elixir demo.exs
%{"definitions" => [%{"description" => "school", "enabled" => false, "key" => "foreign language", "value" => "spanish"}, %{"description" => "school", "enabled" => true, "key" => "english", "value" => "intro to poetry"}, %{"description" => "school", "enabled" => false, "key" => "mathematics", "value" => "math101"}], "name" => "To Do", "resource" => "4234234"}
$

update_in(
  current_tasks,
  ["definitions", Access.filter(&(Map.get(&1, "key") == "mathematics"))],
  &Map.put(&1, "value", "math101")
)

You had one small error:

  update_in(
    current_tasks,
    ["definitions", Access.filter(&(&1["key"] == "mathematics")),"value"],
    fn _ -> "math101" end
  )

You specified a function of arity 0 fn -> "math101" end when you needed arity 1
fn _ -> "math101" end

2 Likes

@ericgray Ah, ok I see what I did wrong haha. Thanks so much! I got it to work using pattern matching.

1 Like

@peerreynders So elegant! I never would have thought to solve it the initial way you’ve got posted. I ended up solving using pattern matching but am considering your split solution for readability. :slight_smile: