[Proposal] Change in Signature of Map and Keyword *_lazy functions to better facilitate piping

52 views
Skip to first unread message

Christian Trosclair

unread,
May 19, 2023, 7:30:48 AM5/19/23
to elixir-lang-core

It is sometimes if not often desirable to build up some Map or Keyword list and add or update values in such a way that takes into account the current state of the data, yet some of the current functions do not allow this to be done from a pipe.


A trivial example for instance:

```

map = %{filename: “thing.txt”}

map = Map.put_new(map, :base_path, "/")

map = Map.put_new(map, :file_path, Path.join([map.base_path, map.filename]))


```

This is begging to be piped, but Map.put_new_lazy/3’s function argument does not receive the current map.


For reference Map.put_new_lazy/3

https://github.com/elixir-lang/elixir/blob/a64d42f5d3cb6c32752af9d3312897e8cd5bb7ec/lib/elixir/lib/map.ex#L381


It is true we could simply use Kernel.then/2, but I think we can do better and make code easier to scan.


An example using pipes and Kernel.then/2

```

%{filename: “thing.txt”}

|> Map.put_new(:base_path, "/")

|> then(&(Map.put_new(&1, :file_path, Path.join([&1.base_path, &1.filename]))))


```

Kernel.then/2 is breaking up what could be a quick scan of the code.


We can smooth this out.


Here is an implementation on Map.put_new_lazy/3


```

​​  @spec put_new_lazy(map, key, ((map) -> value)) :: map

  def put_new_lazy(map, key, fun) when is_function(fun, 1) do

    case map do

      %{^key => _value} ->

        map


      %{} ->

        put(map, key, fun.(map))


      other ->

        :erlang.error({:badmap, other})

    end

  end


```

Then we could rewrite the original example like:


```

%{filename: “thing.txt”}

|> Map.put_new(:base_path, "/")

|> Map.put_new_lazy(:file_path, &(Path.join([&1.base_path, &1.filename]))))


```

If we wanted to go even further, we could even accept an arity 2 function and pass in both the map and the key.


Thoughts?



Wojtek Mach

unread,
May 19, 2023, 7:54:46 AM5/19/23
to elixir-l...@googlegroups.com
I believe an apples-to-apples comparison would be:

    # before
    x
    |> then(&Map.put_new_lazy(&1, :file_path, fn -> Path.join([&1.base_path, &1.filename]) end))

    # after
    x
    |> Map.put_new_lazy(:file_path, &Path.join([&1.base_path, &1.filename]))

And I think it does read better.

That being said, personally I don’t think I have commonly needed something like this though and I’d just break out of a pipe and move on. :)
If we add this precedent, that the function receives the input as one of the arguments, should we update other functions for consistency too? Do we have an Map.update accepting a 2-arity function? (Is the map the first or the second argument?!)

--
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/bc728f96-ff11-40c1-b90f-b412e241fbd9n%40googlegroups.com.

Christian Trosclair

unread,
May 19, 2023, 9:46:05 AM5/19/23
to elixir-lang-core

For me, I run into this often enough, that I wish this was a thing. Clearly, YMMV.

Strictly speaking, yes. But I think just updating the *_lazy versions is sufficient for the level of consistency already present. I mean, there's no Map.put_lazy when there clearly could be and I don't think not changing update makes things worse. (sorry for the double negatives there)
The documentation for a revised Map.update would need to be careful. There' would be a lot going on to explain.

Kurtis Rainbolt-Greene

unread,
May 19, 2023, 9:58:22 PM5/19/23
to elixir-l...@googlegroups.com
I want to second this, the value of pipe-ready functions is an obvious one to me, it has so many downstream bonuses. If we improve this by one step or many I don't care, as I think naturally this will be kept in mind anyways for future functions.



--
Kurtis Rainbolt-Greene,
Software Developer & Founder of Difference Engineers

José Valim

unread,
May 20, 2023, 4:04:52 AM5/20/23
to elixir-l...@googlegroups.com
I agree this is a good proposal but Wojtek unfortunately touched on a point that I don't know how to address.

There are currently 4 *_lazy functions and, if we add this functionality to one of them, it would soon be expected that all of them support it. The problem is that the functions that already receive an argument, such as replace_lazy, cannot be easily modified to accommodate this feature.

Since replace_lazy already accepts the current value as argument, in order to accept the map and potentially the key we would have the following inconsistencies:

1. If a function of single arity is given, then it must receive the key (which is inconsistent with the others which receive the map as first argument)
2. If a function of arity 2 is given, then the map should either be:
  a. the first argument, which means the "key" argument is moving around, which is confusing
  b. the second argument, which means it does not mirror the order of arguments given to Map.replace_lazy itself, which is also confusing

Also note that historically we have not added features, such as new functions, for pipe-ability reasons. In any case, I think this would have been fine but given it will introduce inconsistencies into stdlib, then I don't believe it is worth it.

Reply all
Reply to author
Forward
0 new messages