[Proposal] Add Map.put_if/4

288 views
Skip to first unread message

Riccardo Binetti

unread,
Jun 5, 2020, 6:36:54 AM6/5/20
to elixir-lang-core
Hi everybody,

this is my first proposal so I hope I get the format right, I've already checked in the mailing list and I haven't found similar proposals.

Problem

When manually constructing a map using external options or the result of a pattern match on a struct, I often find myself in the situation where I have to add a value only if it is non-nil.

The possible approaches available today are these

Using if


def some_fun(s) do
  MyStruct{
    foo: foo,
    bar: bar,
    baz: baz
  } = s

  map
= %{a: "hello", b: 42}

  map
=
   
if foo do
     
Map.put(map, :foo, foo)
   
else
      map
   
end

  map =
   
if bar do
     
Map.put(map, :bar, bar)
   
else
      map
   
end

  if bar do
   
Map.put(map, :baz, baz)
 
else
    map
 
end
end


This is the obvious solution, but it gets very verbose very quickly


Populating the map and then filtering

def some_fun(s) do
  MyStruct{
    foo: foo,
    bar: bar,
    baz: baz
  } = s

 
%{a: "hello", b: 42}
  |>
Map.put(:foo, foo)
  |> Map.put(:bar, bar)
  |> Map.put(:baz, baz)   
  |> Enum.filter(fn {_k, v} -> v end)
  |> Enum.into
(%{})
end

This is more concise, but it creates intermediate maps that are immediately thrown away. Moreover, if the initial map is very large, the filter operation is expensive since it traverses the whole map.


Implementing a maybe_put helper function

def some_fun(s) do
  MyStruct{
    foo: foo,
    bar: bar,
    baz: baz
  } = s

 
%{a: "hello", b: 42}
  |>
maybe_put(:foo, foo)
  |> maybe_put(:bar, bar)
  |> maybe_put(:baz, baz)   
end

defp maybe_put(map, _key, nil), do: map

defp maybe_put(map, key, value), do: Map.put(map, key, value)

This is the solution I end implementing almost always. It is present in multiple modules across the codebase I work on (and/or put in a generic Utils module), and talking with my colleagues I found out that is a utility function that they also have to implement often and it has turned up at least a couple times[1][2] also in the Elixir Forum.


This brings me to my proposal, which is a more generic version of maybe_put.

Proposal

Add Map.put_if/4 to the Map module. This is more generic (and imho clearer) than maybe_put and can be used to solve the same problem without creating intermediate results and allowing the code to be clear and concise.


def some_fun(s) do
  MyStruct{
    foo: foo,
    bar: bar,
    baz: baz
  } = s

 
%{a: "hello", b: 42}
  |> Map.put_if
(:foo, foo, foo != nil)
  |>
Map.put_if(:bar, bar, bar != nil)
  |>
Map.put_if(:baz, baz, baz != nil)   
end

Let me know if you need any other information.
--
Riccardo

José Valim

unread,
Jun 5, 2020, 9:27:51 AM6/5/20
to elixir-l...@googlegroups.com
Hi Riccardo,

This is an interesting proposal! Unfortunately if the value you want to compute is expensive, then you need to fallback to the usual `if` approach. This is also specific to maps. It may be worth considering a more general purpose mechanism, for example:

    map
    |> update_if(foo != nil, &Map.put(&1, :foo, foo))
    |> update_if(bar != nil, &Map.put(&1, :bar, normalize_bar(bar)))

Being in Kernel also allows us to write it as a macro, which can be further optimized. Not sure if update_if is the best name though. Suggestions and comparisons to other languages are welcome.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/f513b901-fd93-4f28-88a2-f496a3c84a21o%40googlegroups.com.

Piotr Szmielew

unread,
Jun 5, 2020, 9:42:04 AM6/5/20
to elixir-l...@googlegroups.com
maybe it should be called run_if? so, something like this:

defmacro run_if(piped_value, condition, fun) do
  quote do
    if unquote(condition) do
      unquote(fun).(unquote(piped_value))
   else
     unquote(piped_value)
    end
  end
end

and then used as:

%{a: 1, b: 2}
  |> X.run_if(1 == 2, &Map.put(&1, :foo, "foo"))
  |> X.run_if(1 == 1, &Map.put(&1, :foo, "bar"))
# => %{a: 1, b: 2, foo: "bar"}

Adam Lancaster

unread,
Jun 5, 2020, 9:48:56 AM6/5/20
to elixir-l...@googlegroups.com
We have something similar in our code base we called the Maybe.Pipe: ~>


Leandro Cesquini Pereira

unread,
Jun 5, 2020, 10:10:41 AM6/5/20
to elixir-lang-core
`maybe` is a common prefix for functions that may or may not do something, eg:

https://github.com/elixir-lang/elixir/blob/779ccdb7564d587dfdadc1285a4dc57eb24768a1/lib/logger/lib/logger.ex#L1074

https://github.com/elixir-lang/elixir/blob/779ccdb7564d587dfdadc1285a4dc57eb24768a1/lib/elixir/lib/file.ex#L1796

And so on...

So I'd propose to call it `maybe_run`, just `maybe`, or something with `maybe`.


On Friday, June 5, 2020 at 9:48:56 AM UTC-4, Adam Lancaster wrote:
We have something similar in our code base we called the Maybe.Pipe: ~>

On 5 Jun 2020, at 14:41, Piotr Szmielew <p.sz...@ava.waw.pl> wrote:

maybe it should be called run_if? so, something like this:

defmacro run_if(piped_value, condition, fun) do
  quote do
    if unquote(condition) do
      unquote(fun).(unquote(piped_value))
   else
     unquote(piped_value)
    end
  end
end

and then used as:

%{a: 1, b: 2}
  |> X.run_if(1 == 2, &Map.put(&1, :foo, "foo"))
  |> X.run_if(1 == 1, &Map.put(&1, :foo, "bar"))
# => %{a: 1, b: 2, foo: "bar"}

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Adam Lancaster

unread,
Jun 5, 2020, 10:20:44 AM6/5/20
to elixir-l...@googlegroups.com
Thinking about it, can you solve the original problem with reduce?

```elixir
new_values = %{
foo: 10,
    bar: 20
}

Enum.reduce(map, %{},  fn
   {key, nil}, acc -> Map.put(acc, key, Map.get(new_values, key))
   {key, value}, acc -> Map.put(acc, key, value)
end)
```

Best

Adam

To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/45a9ec20-f997-4fdf-a4c0-ea88388a8fabo%40googlegroups.com.

Alexei Sholik

unread,
Jun 5, 2020, 10:26:36 AM6/5/20
to elixir-lang-core
Enum.reduce() does not compose as well when you have conditional Map.put spread over multiple private functions.



--
Best regards
Alexei Sholik

Adam Lancaster

unread,
Jun 5, 2020, 10:39:31 AM6/5/20
to elixir-lang-core
Forgive me because I’m not 100% sure what you mean, but you can do this:

def update_nil_values(map, new_values, into \\ %{}) do
   
Enum.reduce(map, into,  fn
     
{key, nil}, acc -> Map.put(acc, key, Map.get(new_values, key))

     
{key, value}, acc -> Map.put(acc, key, value)
 
end)
end





%MyStruct{a: nil, b: 10, c: nil}
|> update_nil_values(%{a: 20}, %MyStruct{})
|> other_stuff()
|> update_nil_values(%{c: 50}, %MyStruct{})



On Friday, 5 June 2020 15:26:36 UTC+1, alco wrote:
Enum.reduce() does not compose as well when you have conditional Map.put spread over multiple private functions.

On Fri, Jun 5, 2020 at 5:20 PM Adam Lancaster <ad...@channelviewestates.co.uk> wrote:
Thinking about it, can you solve the original problem with reduce?

```elixir
new_values = %{
foo: 10,
    bar: 20
}

Enum.reduce(map, %{},  fn
   {key, nil}, acc -> Map.put(acc, key, Map.get(new_values, key))
   {key, value}, acc -> Map.put(acc, key, value)
end)
```

Best

Adam

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Davide Bettio

unread,
Jun 5, 2020, 11:55:26 AM6/5/20
to elixir-lang-core
Hello,

I work with Riccardo so I frequently experience the same issue.
I really like José and Piotr solutions, however I'd like to point out that they might introduce some clutter and in real world code they might cause long lines that are broken from the formatter, which can be ugly when using pipes.
For this reason I still think that Map.update_if might be a good idea, anyway run_if already fixes the copy&paste helper function issue, which is already better than nothing.

Regards,
Davide Bettio.

Bruce Tate

unread,
Jun 7, 2020, 8:57:32 PM6/7/20
to elixir-l...@googlegroups.com
I like the idea as well. And I like the name maybe_update. 

-bt

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.


--

Regards,
Bruce Tate
CEO

Riccardo Binetti

unread,
Jun 8, 2020, 5:20:32 AM6/8/20
to elixir-lang-core
If we also want to have a function in the Map module I think that the verb should be put, not update (since the operation described in my first post is effectively a put operation), so eitherx put_if or maybe_put.

Regarding the proposed Kernel macro, it seems to me that it can be used in a more general sense and not only for updating collections (the first argument can effectively be anything), so I like the run verb. It could also be apply_if.

In both cases I personally I find the _if suffix more expressive than the maybe_ prefix, in my opinion this

|> run_if(x == 1, some_fun)

reads more naturally and better conveys the role of the arguments than

|> maybe_run(x == 1, some_fun)

--
Riccardo

Bruce Tate

unread,
Jun 8, 2020, 7:29:25 AM6/8/20
to elixir-l...@googlegroups.com
Map.maybe_put then... that works well for me. 

I love the way maybe_ style functions compose. It makes a tremendous difference to the end product. 

-bt

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

José Valim

unread,
Jun 8, 2020, 7:52:14 AM6/8/20
to elixir-l...@googlegroups.com
I would like to avoid the maybe_ prefixes. In my mind, I would use "maybe_ " for returning {:ok, ...} | {:error, _} or similar.

Furthermore, although we may use maybe_ internally, we don't use the maybe_ prefix in any of the public functions. So I would be very careful before setting a new naming convention, as establishing the wrong convention can be harmful in the long term. The other suggestions, run_if/update_if sound  more plausible in my opinion.

Marcus

unread,
Jun 8, 2020, 10:13:18 AM6/8/20
to elixir-lang-core
I like the Map.put_if from the first post. I am not sure if run_if/update_if is a good idea because it adds to much "magic", and it could lead to the writing of unreadable code.
If we go with run_if, I think we should have a function for each branch (true/false). Something like:

@spec run_if(data, cond, do_fun, else_fun \\ &identity/1) 
  when data: term(), 
       cond: boolean() | (term() -> boolean()), 
       do_fun: (term() -> term()), 
       else_fun: (term() -> term())
def run_if ...

x = nil
y = 11
z = nil

map 
|> run_if(x != nil, fn m -> Map.put(m, :x, x) end)
|> run_if(y != nil, fn m -> Map.put(m, :y, y) end)
|> run_if(fn m -> Map.has_key(m, :a) end, fn m -> Map.put(m, :b) end)
|> run_if(
     z != nil, 
     fn m -> Map.put(m, :z, z) end, 
     fn m -> Map.update(m, :missing, :z, fn x -> [:z|x] end)
   )

José Valim

unread,
Jun 8, 2020, 10:19:44 AM6/8/20
to elixir-l...@googlegroups.com
Unfortunately a run_if that has both success and failure branches offers no benefit to a "if" - so I would suggest to use that instead.

Furthermore, I wouldn't expect a function called "run_if" to receive (or return) the first argument if the condition evaluates to false. For something named run or apply, I would simply expect to return "nil".

That's why I proposed something along the lines of "update_if", because it should be clear that the intent is to update/modify the first argument somehow.

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Artur Dębski

unread,
Jun 8, 2020, 10:30:20 AM6/8/20
to elixir-lang-core
In our codebase, we have a helper called `pipe_if` which evaluates the block if the condition is met, so you can do `params |> pipe_if(foo == "bar", &Map.put(&1, :key, value))` otherwise it just passes through. 


W dniu poniedziałek, 8 czerwca 2020 16:19:44 UTC+2 użytkownik José Valim napisał:
Unfortunately a run_if that has both success and failure branches offers no benefit to a "if" - so I would suggest to use that instead.

Furthermore, I wouldn't expect a function called "run_if" to receive (or return) the first argument if the condition evaluates to false. For something named run or apply, I would simply expect to return "nil".

That's why I proposed something along the lines of "update_if", because it should be clear that the intent is to update/modify the first argument somehow.

On Mon, Jun 8, 2020 at 4:13 PM Marcus <esurk...@gmail.com> wrote:
I like the Map.put_if from the first post. I am not sure if run_if/update_if is a good idea because it adds to much "magic", and it could lead to the writing of unreadable code.
If we go with run_if, I think we should have a function for each branch (true/false). Something like:

@spec run_if(data, cond, do_fun, else_fun \\ &identity/1) 
  when data: term(), 
       cond: boolean() | (term() -> boolean()), 
       do_fun: (term() -> term()), 
       else_fun: (term() -> term())
def run_if ...

x = nil
y = 11
z = nil

map 
|> run_if(x != nil, fn m -> Map.put(m, :x, x) end)
|> run_if(y != nil, fn m -> Map.put(m, :y, y) end)
|> run_if(fn m -> Map.has_key(m, :a) end, fn m -> Map.put(m, :b) end)
|> run_if(
     z != nil, 
     fn m -> Map.put(m, :z, z) end, 
     fn m -> Map.update(m, :missing, :z, fn x -> [:z|x] end)
   )

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

Leandro Cesquini Pereira

unread,
Jun 8, 2020, 10:33:03 AM6/8/20
to elixir-lang-core
What about `Map.update_if` and `Keyword.update_if`? Benefits:

- It's scoped to the data structure that will in fact receive the update (or not)
- Closer to the original proposal
- Similar to existing functions
- Not misleading to broader concepts
- May be implemented on other modules by demand in the future


On Monday, June 8, 2020 at 10:19:44 AM UTC-4, José Valim wrote:
Unfortunately a run_if that has both success and failure branches offers no benefit to a "if" - so I would suggest to use that instead.

Furthermore, I wouldn't expect a function called "run_if" to receive (or return) the first argument if the condition evaluates to false. For something named run or apply, I would simply expect to return "nil".

That's why I proposed something along the lines of "update_if", because it should be clear that the intent is to update/modify the first argument somehow.

On Mon, Jun 8, 2020 at 4:13 PM Marcus <esurk...@gmail.com> wrote:
I like the Map.put_if from the first post. I am not sure if run_if/update_if is a good idea because it adds to much "magic", and it could lead to the writing of unreadable code.
If we go with run_if, I think we should have a function for each branch (true/false). Something like:

@spec run_if(data, cond, do_fun, else_fun \\ &identity/1) 
  when data: term(), 
       cond: boolean() | (term() -> boolean()), 
       do_fun: (term() -> term()), 
       else_fun: (term() -> term())
def run_if ...

x = nil
y = 11
z = nil

map 
|> run_if(x != nil, fn m -> Map.put(m, :x, x) end)
|> run_if(y != nil, fn m -> Map.put(m, :y, y) end)
|> run_if(fn m -> Map.has_key(m, :a) end, fn m -> Map.put(m, :b) end)
|> run_if(
     z != nil, 
     fn m -> Map.put(m, :z, z) end, 
     fn m -> Map.update(m, :missing, :z, fn x -> [:z|x] end)
   )

--
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-l...@googlegroups.com.

jonar...@gmail.com

unread,
Jun 9, 2020, 1:43:52 PM6/9/20
to elixir-l...@googlegroups.com
I believe it would make sense to follow the general convention from the Kernel Access functions and have a Kernel.put_if to place a new value or override an existing one and a Kernel.update_if to update an existing value with an anonymous function.
run_if to me seems too general. When reading 'put_if' or 'update_if', I can immediately determine the intent. With 'run_if', determining the intent requires further reading.
As far as the naming of maybe_put versus put_if is concerned, the 'maybe' version harkens to Haskell's maybe. The functionality discussed here is not a direct analogue, though similar.

Just my two cents.

Riccardo Binetti

unread,
Jul 23, 2020, 9:20:35 AM7/23/20
to elixir-l...@googlegroups.com
Hi everybody,
I would like to bring this proposal back since I just typed `def put_if` again in another module :)
 
Being my first proposal I don't know what is the usual process, but I think the discussion showed that there is some interest in this functionality.
 
I personally like the `update_if` proposal made by José because it's a more general construct that can be useful in various situation. I wouldn't mind also having a `Map.put_if` function in the Map module but I'm fine using `update_if` in case.
--
Riccardo

Zachary Daniel

unread,
Jul 23, 2020, 9:26:59 AM7/23/20
to elixir-lang-core
I would use this function pretty often if it existed :)

I'm not really a fan the `update_if` that Jose proposed because my biggest use case has been "update a key, but I don't want to put a default in if it doesn't exist". I think something like

`Map.update_existing(map, :key, fn value -> value * 2 end)`

You can also simulate the "update if" by putting a conditional in the function like `if value != nil, ...`

Adam Lancaster

unread,
Jul 23, 2020, 9:30:18 AM7/23/20
to elixir-l...@googlegroups.com
What do people think about this suggestion:

```elixir
defmodule Thing do

  def put_if(map, value_to_check, new_values) do
    Enum.reduce(map, %{},  fn
       {key, ^value_to_check}, acc -> Map.put(acc, key, Map.get(new_values, key))
       {key, value}, acc -> Map.put(acc, key, value)
    end)
  end

end

new_values = %{
  foo: 10,
  bar: 20
}

Thing.put_if(%{foo: nil, bar: 10}, nil, new_values)
# => %{bar: 10, foo: 10}
Thing.put_if(%{foo: false, bar: 10}, false, new_values)
# => %{bar: 10, foo: 10}
Thing.put_if(%{foo: nil, bar: 10}, false, new_values)
#=> %{bar: 10, foo: nil}
```

That would give you the ability to replace something if it was nil or false, or whatever you wanted?

Best

Adam

-- 
You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.

Zachary Daniel

unread,
Jul 23, 2020, 9:30:31 AM7/23/20
to elixir-lang-core
I guess, rereading your original proposal, you don't actually need the helper I just described, sorry.

Tallak Tveide

unread,
Jul 25, 2020, 5:49:09 AM7/25/20
to elixir-lang-core
You also have the pretty compact option:

’’’
map = (foo && Map.put(map, :foo, foo)) || map
’’’

You could make it pipeable by wrapping it in an anonymous function:


’’’
map
|> &((foo && Map.put(&1, :foo, foo)) || &1).()
’’’

Tallak Tveide

unread,
Jul 25, 2020, 5:56:07 AM7/25/20
to elixir-lang-core
I think the proper name would be something like

```
transform_if(x, condition, fun)
```

Davide Bettio

unread,
Aug 31, 2020, 12:28:57 PM8/31/20
to elixir-l...@googlegroups.com
Any update on this topic?
I still think this is quite useful and I would love to see some kind
of outcome from this thread.

Regards,
Davide Bettio.

Il giorno sab 25 lug 2020 alle ore 11:56 Tallak Tveide
<tal...@gmail.com> ha scritto:
>
> I think the proper name would be something like
>
> ```
> transform_if(x, condition, fun)
> ```
>
> --
> You received this message because you are subscribed to the Google Groups "elixir-lang-core" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to elixir-lang-co...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/9334d4ba-d96c-466d-80e8-af0c20dc3f47o%40googlegroups.com.

Piotr Szmielew

unread,
Sep 2, 2020, 8:45:09 AM9/2/20
to elixir-l...@googlegroups.com
I have proposed a PR [a little prematurely], but it's there if we want to discuss more about it:

Reply all
Reply to author
Forward
0 new messages