-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Add Hash#transform_keys!
#16280
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
base: master
Are you sure you want to change the base?
Add Hash#transform_keys!
#16280
Changes from 1 commit
f79ec79
d61b81d
2382ff3
9c98056
8151b0c
01e502d
44b8f05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1755,6 +1755,24 @@ class Hash(K, V) | |||||||||||
| end | ||||||||||||
| end | ||||||||||||
|
|
||||||||||||
| # Destructively transforms all keys using a block. Same as transform_keys but modifies in place. | ||||||||||||
| # The block cannot change a type of keys. | ||||||||||||
| # The block yields the key and value. | ||||||||||||
| # | ||||||||||||
| # ``` | ||||||||||||
| # hash = {"a" => 1, "b" => 2, "c" => 3} | ||||||||||||
| # hash.transform_keys! { |key| key.upcase } | ||||||||||||
| # hash # => {"A" => 1, "B" => 2, "C" => 3} | ||||||||||||
| # hash.transform_keys! { |key, value| key * value } | ||||||||||||
| # hash # => {"a" => 1, "bb" => 2, "ccc" => 3} | ||||||||||||
| # ``` | ||||||||||||
| def transform_keys!(&block : K, V -> K) : self | ||||||||||||
| transformed_hash = self.transform_keys(&block) | ||||||||||||
|
||||||||||||
| transformed_hash = self.transform_keys(&block) | |
| transformed_hash = Hash(K, V).new(initial_capacity: entries_capacity) | |
| each_with_object(transformed_hash) do |(key, value), memo| | |
| memo[yield(key, value)] = value | |
| end |
And then Set#map! could also use the yielding form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean by "yielding form" this approach:
transformed_hash = self.transform_keys { |k, v| yield k, v }then yes, it doesn't compile:
bin/crystal spec/std/hash_spec.cr
Using compiled compiler at .build/crystal
Showing last frame. Use --error-trace for full trace.
In src/pointer.cr:146:29
146 | (self + offset).value = value
^----
Error: type must be Hash::Entry(Int32 | String, String), not Hash::Entry(Int32, String)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Thank you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should be fine with an explicit cast to K. No need to duplicate the implementation.
transformed_hash = transform_keys{ |k, v| (yield k, v).as(K) }Sorry for the back and forth Andrii, and thanks for staying around with us 🫶
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙂
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It probably makes sense to move the copying logic into a new method #replace, similar to Hash#replace in Ruby.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: It would be nice to add an example where the transformed key type is a subtype of the original type. This should already work correctly because the proc's output type is
K(this forces a cast on captured blocks). But a spec would make sure it stays that way.{"1" => "foo", 2 => "bar"}.transform_keys!(&.to_i).should eq({1 => "foo", 2 => "bar"})There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a particular reason for introducing explicit types for this instead of using the example I provided?
The key type of the hash is
Int32 | String, butto_ireturns onlyInt32.Union types work just fine for demonstration, there should be no need for explicit type inheritance.
This would be a bit more concise because we don't need the type definitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. I haven't noticed that keys in
{"1" => ..., 2 => ...}are of different types. And indeed a union type is a super type of any of its parts.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there also be a negative assertion test, where the transformed key type is not a subtype of the original type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will lead to a compilation error. Is it convenient to have such tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure if we do those kinds of tests to show the intention that it shouldn't work and/or that that's a known case. In this case, it "works" with
transform_keys, but not withtransform_keys!, so it highlights a difference between the two.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah unfortunately, tests for compilation errors are quite inconvenient. Especially the runtime cost is prohibitive. They need to run an extra compiler which is multiple orders of magnitude slower than a simple positive spec.
So we usually don't do that.
It would be pretty awesome to have a more efficient way via some compiler primitive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense @straight-shoota thanks. We do have a doc comment that the type of the keys can't change, so at least that's in place.