How to conditionally update map elegantly?

54 views
Skip to first unread message

胡永浩

unread,
Apr 22, 2019, 4:26:46 AM4/22/19
to elixir-lang-core

I am now doing it like this in a route:

    query = %{ type: params["type"] }
    query = if !!params["language"], do: Map.put(query, :language, params["language"]), else: query
    query = if !!params["id"], do: Map.put(query, :language, params["id"]), else: query

I wonder, could we afford a function to decide whether to put the key?(I do mention this as a proposal if there is no best practice.)


RIP, Joe.



José Valim

unread,
Apr 22, 2019, 4:38:07 AM4/22/19
to elixir-l...@googlegroups.com
Thanks 胡永浩!

I write similar code frequently too, it would be nice if everyone could share how they are writing said/similar constructs. If it it happens too frequently, I create a small function to do it: https://github.com/elixir-ecto/ecto_sql/blob/master/lib/ecto/adapters/postgres.ex#L146-L147


José Valim
Skype: jv.ptec
Founder and Director of R&D


--
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/11ef9a1a-1bbd-4ce3-93ff-3ad16466cb0e%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

胡永浩

unread,
Apr 22, 2019, 4:47:18 AM4/22/19
to elixir-l...@googlegroups.com
Hi, thanks for your reply. 

I am sorry that I just copy the content form my issue in Elixir project and forgot to say hi to community. 

I enjoy(love) Elixir and used it in my job for two years, thanks for Jose and Joe 's creating.

Hope that I can contribute to Elixir! 😀


For more options, visit https://groups.google.com/d/optout.


--
--
Regards,
YongHao Hu


Michał Muskała

unread,
Apr 22, 2019, 5:12:36 AM4/22/19
to elixir-lang-core
In a case like this, I usually prefer to flip the task around and reduce over the params:

Enum.reduce(params, %{type: nil}, fn
  {"language", language}, acc -> Map.put(acc, :language, language)
  {"id", id}, acc -> Map.put(acc, :id, id)
  {"type", type}, acc -> Map.put(acc, :type, type)
  _, acc -> acc
end)

It works nice and doesn't require any conditionals. I saw the double negation - if that's important for the logic, it could be achieved by adding in `when language not in [nil, false]`, which could be further reduced to a nice `when truthy(language)` defguard macro.

Michał.

YongHao Hu

unread,
Apr 22, 2019, 5:54:18 AM4/22/19
to elixir-l...@googlegroups.com
Reduce seems good.

How about add a extra param like bool conditional func  to decide  put key or not?

Like `Map.put(query, :language, params["language"], fn x -> x != nil)`.
Of course it can apply to `Map.merge` too.


For more options, visit https://groups.google.com/d/optout.

w...@resilia.nl

unread,
Apr 23, 2019, 5:58:42 AM4/23/19
to elixir-lang-core
I have come across wanting to do this a couple of times in the past as well.

But before that, I do want to mention that there are two related situations that have more elegant solutions:

1) You want to create a map or structure where fields that were not supplied (e.g. by the user request) fall back to a default. This can be done with patterns like this:
query = %MyRequest{
            language
: params["language"] || "english",
            id
: params["id"] || session.id
       
}

2) You want to pick one of a restricted set of choices; also known as working with a 'sum type':
shape = case params["shape"] do
          "square" ->
            %Square{position: params["position"], width: params["width"], height: params["height"]}
          "circle" ->
             %Circle{position: params["position"], radius: params["radius"]}
           _ ->
            raise ArgumentError, "unknown kind of shape provided"
       
end

If you are able to use either of these approaches in a specific use-case, then you can reduce the number of input checks you need to do later on. (In your original example code, all functions operating on `query` will have to check again if `:language` and/or `:id` exist.)


Having said that, there definitely are cases where you want to normalize a map with string keys to a map with recognized atom keys (to ensure that (a) you dont exhaust memory by creating more and more atoms and (b) reject inputs that you cannot handle).
The pattern I have used in the past was similar to:

def normalize_keys(into \\ %{}, input, accepted_keys) do
  string_keys
= MapSet.new(accepted_keys, &to_string/1)

 
Enum.reduce(input, into, fn {string_key, val}, acc ->
     
if string_key in string_keys do
       put_in
(acc, [:"#{string_key}"], val)
     
else
       acc
     
end
 
end)
end
And use it as:

iex> normalize_keys(%{"language" => "english", "id" => 33, "disregard_me" => 10}, [:id, :language])
%{id: 33, language: "english"}

Do note that this has a slightly different result from your original code, since `string_key in string_keys` checks for the existance of a key, while `params[string_key]` checks for the existance of a key AND the value stored under key being truthy.

Only after writing out this code I realize its similarity to Ecto.Changeset.cast/4 which seems to deal with a variant of the same problem.
A common pattern for sure, interesting.

~Marten / Qqwy

OvermindDL1

unread,
Apr 23, 2019, 2:15:36 PM4/23/19
to elixir-lang-core
I also do this reduce pattern that Michał Muskała put, though I often don't put a catch all and rather just error back invalid arguments as that usually means something was mis-typed and I want it reported early and fast.


On Monday, April 22, 2019 at 3:12:36 AM UTC-6, Michał Muskała wrote:
In a case like this, I usually prefer to flip the task around and reduce over the params:

Enum.reduce(params, %{type: nil}, fn
  {"language", language}, acc -> Map.put(acc, :language, language)
  {"id", id}, acc -> Map.put(acc, :id, id)
  {"type", type}, acc -> Map.put(acc, :type, type)
  _, acc -> acc
end)

It works nice and doesn't require any conditionals. I saw the double negation - if that's important for the logic, it could be achieved by adding in `when language not in [nil, false]`, which could be further reduced to a nice `when truthy(language)` defguard macro.

Michał.
On 22 Apr 2019, 10:26 +0200, 胡永浩 <christo...@gmail.com>, wrote:

I am now doing it like this in a route:

    query = %{ type: params["type"] }
    query = if !!params["language"], do: Map.put(query, :language, params["language"]), else: query
    query = if !!params["id"], do: Map.put(query, :language, params["id"]), else: query

I wonder, could we afford a function to decide whether to put the key?(I do mention this as a proposal if there is no best practice.)


RIP, Joe.



--
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.
Reply all
Reply to author
Forward
0 new messages