Skip to content

Map.map() allows for struct, now deprecated, suggests Map.new() which doesn't support struct #11912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
krainboltgreene opened this issue Jun 8, 2022 · 8 comments

Comments

@krainboltgreene
Copy link
Contributor

Elixir and Erlang/OTP versions

Erlang/OTP 24 [erts-12.2.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit]

Elixir 1.13.1 (compiled with Erlang/OTP 24)

Operating system

Linux DESKTOP-968KC2R 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

Current behavior

%Person{a: 1} |> Map.map(fn {key, value} -> {key, value} end)
# => %Person{a: {:a, 1}}
%Person{a: 1} |> Map.new(fn {key, value} -> {key, value} end)
# => protocol Enumerable not implemented for %Person{a: 1}

Expected behavior

If Elixir is going to tell me to use Map.new/2 instead of Map.map/1 then I expect Map.new/2 to work with my value like it does with Map.map/1.

@josevalim
Copy link
Member

Can you please provide an example of a struct where calling Map.map makes sense? Because it may actually be an argument for keeping Map.map around. Thanks.

@krainboltgreene
Copy link
Contributor Author

krainboltgreene commented Jun 8, 2022

Sure so currently in my use case I have a Struct that I need to rewrite some values on, the normal use case for Map.new/2 but I need to retain the fact that it's a Struct. In code:

record
|> Map.map(&transform_special_values/1)
|> Map.take(&allowed_fields/1)

Other Map functions seem to be able to do this, like Map.take().

Something I'm not sure of that maybe you can answer: If I take a Struct, turn it into a Map, then turn it back into a struct with say struct(), is it (for all intents) equal? Will APIs that expect say an Ecto struct be fooled?

If yes, then I guess in my code I can do:

%X{}
|> Map.from_struct()
|> Map.new(&a/1)
|> fn attributes -> struct(X, attributes) end.()

even though that seems kinda cringe for just one function out of many.

(Next proposal: struct/2 but the first arg is a map() and the second arg is a struct() so I can pipe.)

@josevalim
Copy link
Member

Something I'm not sure of that maybe you can answer: If I take a Struct, turn it into a Map, then turn it back into a struct with say struct(), is it (for all intents) equal?

Yes, because all it matters are the keys and values. There is no underlying identity or objects. So what you see is what you get.

My concern with your approach is that you are traversing all struct keys only to modify potentially a few of them. It feels very inefficient. The whole traversal for structs feels a bit backwards. Wouldn't it be better to pattern match on the desired struct fields and the update them?

%{a: a, b: b} = struct
...
%{struct | a: new_a, b: new_b}

@krainboltgreene
Copy link
Contributor Author

krainboltgreene commented Jun 8, 2022

In my particular case I don't know the keys, thus the need for traversing. For the code in question I am looking to replace special values with "real" values in a broad list of structs which have different keys.

The exact use case is a random data generator where I need to differentiate between empty values and values that are empty. I could explain the business logic further, but for the difference is %Person{friends: []} and %Person{friends: :empty_list} where the business logic sees the former and transforms it into a list of random friends and sees the latter and turns it into an empty list. Except this is also the process for Pantheon, Deity, Location, ...etc.

@josevalim
Copy link
Member

Thank you! In this case, my suggestion might perhaps be something like this:

fields = Map.from_struct(struct)

Enum.reduce(fields, struct, fn {k, v}, acc ->
  # maybe or maybe not update acc
end)

I think it makes the idea more explicit that you are simply updating the struct compared to something like struct |> Map.from_struct |> Map.new |> then(struct(...)).

@krainboltgreene
Copy link
Contributor Author

Okay, so do I make a PR to add the detail to the deprecation notice that for version 1.13.0 and 1.13.1 Map.map(struct) worked but in 1.13.3+ it doesn't? The notice implies that Map.new/2 is a suitable change, when it's not.

@wojtekmach
Copy link
Member

wojtekmach commented Jun 8, 2022

The updated deprecation message already covers it: 9bb1202#diff-17d8120ee6e35ed2a465a5e10985eeb91b64f54b38347daf429cc20d1c85df85R1109.

@krainboltgreene
Copy link
Contributor Author

Ah-ha! Thanks I didn't notice the commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants