Elixir way to conditionally update a map

I’ve written a helper function for this sort of thing before

def maybe_put(map, _key, nil), do: map
def maybe_put(map, key, value), do: Map.put(map, key, value)

Then you can use for multiple fields and pipe easily, for example:

data = %{
  requireInteraction: true,
  title: title,
  icon: icon,
  click_action: click_action
}
|> maybe_put(:body, body)
|> maybe_put(:other_field, other_value)
26 Likes

Dude thats amazing… thank you

Either you can either conditionally put value or reject it. If there’s only field like body, I’ll lean towards putting conditionally or it there’s more than one nil value then reject it.

Actually it is much easier:

maps:filter(fun (K,V) -> Pred(K, V) end, Map)

or in elixir:

:maps.filter(fn(k, v) -> pred.(k, v) end, map)

2 Likes

I should have been clearer in my message. I was showing the implementation of maps:filter/2

1 Like

OK, gotcha!

This would be my preferred method as well: it conveys the meaning clearly, requires zero mental effort to parse and understand the intent. For me the other methods here trade character count for clearness, like code golfing does

1 Like

For future reference, there is Map.reject/2 and Map.filter/2, now (since 1.13.0).

2 Likes

As it was asked above, perhaps a potential case statement could be…

       data =
        body
        |> case do                                                             
          nil -> data                                                                                              
          body -> Map.put(data, :body, body)                                                           
        end

It doesn’t look quite so “using CASE statements over IF statements for the sake of being Elixir-ish” :slight_smile: if there is some work to calculate body. For example, if we need to do something like Keyword.get(opts, :body) to calculate body

       data =
        Keyword.get(opts, :body)
        |> case do                                                             
          nil -> data                                                                                              
          body -> Map.put(data, :body, body)                                                           
        end

This gives us some pattern matching potential as compared with the IF statement (eg. if this section has to be extended in the future) without adding extra functions.

It’s worth noting that it’s important to not start the pipeline directly (ie. on the same line) after the = as the precedence of data = Keyword.get(opts, :body) is higher than the rest of the pipeline (I learned this the hard way!)…

       data = Keyword.get(opts, :body)
        |> case do #<--- Won't get here                                                            
          nil -> data                                                                                              
          body -> Map.put(data, :body, body)                                                           
        end

You can workaround the precedence issue by putting |> case do on the first line too. But “mix format” will rewrite this to the multi-line version I have at the start of my reply anyway. Here it is nonetheless…

       data = Keyword.get(opts, :body) |> case do                                                             
          nil -> data                                                                                              
          body -> Map.put(data, :body, body)                                                           
        end

I’d be interested to here if the approach I suggested (the one at start of my reply) is considered an acceptably idiomatic solution in Elixir - as compared with the suggestions mentioned by others, such as factoring out extra functions, etc.

Map.merge/2 is another solution:

Map.merge(
  %{
    requireInteraction: true,
    title: title,
    icon: icon,
    click_action: click_action
  },
  if(body, do: %{body: body}, else: %{})
)