Skip to content
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

Remove delete_if #9878

Merged
merged 2 commits into from Nov 17, 2020
Merged

Remove delete_if #9878

merged 2 commits into from Nov 17, 2020

Conversation

caspiano
Copy link
Contributor

@caspiano caspiano commented Nov 4, 2020

Implementing changes referenced by @asterite in #9856

  • Deprecates Hash#delete_if
  • Removes Deque#delete_if
  • Adds Deque#reject!
  • Adds Deque#select!

@caspiano caspiano changed the title Remove delete if Remove delete_if Nov 4, 2020
@straight-shoota
Copy link
Member

Let's first have a proper discussion on how to proceed with this. Please open an issue and describe your proposal. Only when that's come to a conclusion, it's time to implement it. Proposals tend to change over time. Starting a discussion whit a PR (or rather jumping from one PR to another) has proven to be an inefficient means to communicate about proposed changes.

@caspiano
Copy link
Contributor Author

caspiano commented Nov 4, 2020

Sorry @straight-shoota, I was jumping on work mentioned in the discussion here #9856, specifically this comment

@caspiano caspiano mentioned this pull request Nov 4, 2020
Copy link
Member

@asterite asterite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@jkthorne
Copy link
Contributor

jkthorne commented Nov 4, 2020

Even though we are not 1.0 should there be a depreciation warning for this?

@jzakiya
Copy link

jzakiya commented Nov 4, 2020

Instead of using % to check for even|odd, as below:

a2 = a1.select! { |elem| elem % 2 == 0 }

its faster to do:

a2 = a1.select! { |elem| elem & 1 == 0 }

or

a2 = a1.select! { |elem| (elem & 1).zero? }

I see this kind of unnecessary|expensive use of % throughout the codebase to test for even|odd.
Using this simpler|faster test can increase observable performance, especially if it's within loops, which they typically are.

@Blacksmoke16
Copy link
Member

@jzakiya Why not just a1.select! &.even?.

@jzakiya
Copy link

jzakiya commented Nov 4, 2020

I was pointing out how to improve that code snippet by eliminating using %, with minimal change.

I suspect many (younger) programmers don't really understand even|odd of an integer is merely if the lsb is 1 or 0.

I grew up as a hardware engineer, and this is what you do in hardware, branch on the lsb value.

Yes, you can (should?) use .even? or .odd? (assuming they're implemented optimally) for this type of testing,
but that will have to be promoted (educated) to users for them to understand how to best code in Crystal.

@j8r
Copy link
Contributor

j8r commented Nov 4, 2020

An option can be to have a 0.36.0 before 1.0 in order to have the deprecation warnings. After that, we will have a clean, 0 deprecations for 1.0.

The deprecation warnings are important to make the Crystal upgrade experience a breeze @wontruefree . I agree with you that it doesn't feel right to have warnings for a "definitive" 1.0 API.

@asterite
Copy link
Member

asterite commented Nov 4, 2020

@j8r the next version is 1.0, that's not discussable

@stakach
Copy link
Contributor

stakach commented Nov 4, 2020

@jzakiya LLVM should optimise that operation in release and as elem % 2 == 0 is arguably more readable than elem & 1 == 0 its probably worth leaving it

http://duriansoftware.com/joe/Optimizing-is-multiple-checks-with-modular-arithmetic.html

@caspiano
Copy link
Contributor Author

caspiano commented Nov 5, 2020

@jzakiya I copied the test from Array's test suite, I think the most crystalline solution for this would be using a1.select! &.even? and a1.reject! &.even?. I can update that now.
Perhaps you could open another PR for other occurrances that you've seen across the codebase?

@jzakiya
Copy link

jzakiya commented Nov 5, 2020

Thank you @caspiano, and I agree they are the clearest for someone reading the code.

I am willing (with others) to do a thorough (as possible that I can do) review of Crystal's codebase to identify instances to use &.even? and &.odd? if the devs indicate they are willing to take PRs to change these instances.

Or I (and others) can first identify such instances to log for change for future consideration.
Let me know which is preferable (make a log of instances first, or submit PRs to change first).
Also, what level of code granularity do you want this done on?
Should it be of a file basis, module, class, method by method, etc.

While writing this, I think it would be more manageable to create a list of instances first, so people can review them to insure changing their semantics won't create unexpected edge cases for their particular use cases, just to be careful.

Addition:
I just realized searching the codebase can be automated, which would make the identification|logging task quite easy.

@asterite
Copy link
Member

asterite commented Nov 5, 2020

You won't find such instances except in tests, and there it doesn't matter.

@caspiano
Copy link
Contributor Author

caspiano commented Nov 5, 2020

@bcardiff following from your comment here

From a manual code search on github, I saw around ~80 instances of Hash#delete_if and almost no instances of Deque#delete_if.
IMO, the way forward would be deprecation of Hash#delete_if and an immediate bypass of deprecation for Deque#delete_if.

@jzakiya
Copy link

jzakiya commented Nov 5, 2020

So @asterite for clarity, you don't favor even changing test code?

I think even changing test code instances is worth doing, because you would be reinforcing best practices to people reviewing the codebase, while reducing its size too.

@caspiano
Copy link
Contributor Author

caspiano commented Nov 5, 2020

@jzakiya updated. I doubt anyone's going to stop you improving the tests. However this is best discussed on a seperate PR.

@caspiano
Copy link
Contributor Author

caspiano commented Nov 5, 2020

@j8r @wontruefree @bcardiff

Reverted removal of Hash#delete_if and added a deprecation notice.

@jzakiya
Copy link

jzakiya commented Nov 5, 2020

@jzakiya updated. I doubt anyone's going to stop you improving the tests. However this is best discussed on a seperate PR.

Will do.

# See also: `Deque#reject`.
def reject!
internal_delete { |e| yield e }
self
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make return self or nil as in Hash.

Ref: #9856 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to change Hash#reject! to be non-nilable instead. There's really not much reason to return nil and it breaks method chaining. If you need to know whether an item was removed, you can keep track of whether the block ever returns false as demonstrated in #9879 (comment)
That clearly communicates what's going on and avoids having to do some nasty nil-checks when chaning calls. I doubt there are many use cases for the nilable return type anyway.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing Hash#reject! to return always self would be a silent breaking-change.

Ruby returns self or nil on {Array, Hash}#reject!.

tap can be used to perform chaining if wanted. c.tap(&.reject!(...)).foo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing Hash#reject! to return always self would be a silent breaking-change.

Yes, that's unfortunate. We don't necessarily have to change it, at least not right now. But I would not introduce the same weird return type for new methods.

tap can be used to perform chaining if wanted. c.tap(&.reject!(...)).foo

This clearly shows that it doesn't make any sense to have #reject! return self as an indicator for an item being deleted. Returning Bool would be sufficient if such a return value is desired. Otherwise it should just be self always.
Even nil always is better than self | nil.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we then update Array#reject! to also return self | nil?
I don't see a particularly good reason for the inconsistency between these interfaces besides least astonishment coming from ruby. I think the return type of self is better purely for the benefit of simply chaining methods.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, let's keep reject! : self then. We will change the hash#reject! in another PR.... I hope this breaking-change will not haunt us later...

@bcardiff bcardiff merged commit fe5fff6 into crystal-lang:master Nov 17, 2020
@caspiano caspiano deleted the remove-delete-if branch December 4, 2020 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants