Feature #16446
closedEnumerable#take_*, Enumerable#drop_* counterparts with positive conditions
Description
#16441 led me to think about the issue more generally. When we want to split a series of iterations by the first element that satisfies (or dissatisfies) a condition, we have three factors to consider.
(1) Whether we want the condition to work negatively or positively
(2) Whether we want the first element to satisfy (or dissatisfy) the condition to be included in the left side or the right side of the split
(3) Whether we want the left side or the right side in the returned output
This leads us to eight possible combinations to consider.
enum = [1, 1, 0, 3, 3, 0, 5, 5].to_enum
(1) | (2) | (3) | method | example | |
---|---|---|---|---|---|
1 | negatively | left | left | take_while |
enum.foo1(&:nonzero?) # => [1, 1] |
2 | negatively | left | right | drop_while |
enum.foo2(&:nonzero?) # => [0, 3, 3, 0, 5, 5] |
3 | negatively | right | left | enum.foo3(&:nonzero?) # => [1, 1, 0] |
|
4 | negatively | right | right | enum.foo4(&:nonzero?) # => [3, 3, 0, 5, 5] |
|
5 | positively | left | left | enum.foo5(&:zero?) # => [1, 1] |
|
6 | positively | left | right | enum.foo6(&:zero?) # => [0, 3, 3, 0, 5, 5] |
|
7 | positively | right | left | enum.foo7(&:zero?) # => [1, 1, 0] |
|
8 | positively | right | right | enum.foo8(&:zero?) # => [3, 3, 0, 5, 5] |
Proposal #16441 asks for a method that corresponds to case 3 in the table above, but I think that would make the paradigm messy unless case 4 is also implemented. Either cases 3 and 4 should both be implemented, or both not. Actually, the current proposal is not about cases 3 and 4. I would leave that to #16641.
In many use cases (including the first example in #16641), we want to detect the "marker element" by which we split the iterations. In the cases above, that can be the element 0
. In such use cases, it is more natural to describe the condition in positive terms (i.e., zero?
) rather than negative terms (i.e., nonzero?
). (And in other use cases, it might be the other way around.) So I would like to propose methods that correspond to cases 5, 6, 7, 8 above.
Naming of the methods should be done systematically. As a candidate, I came up with the following:
method | |
---|---|
5 | take_before |
6 | drop_before |
7 | take_upto |
8 | drop_upto |
Updated by shevegen (Robert A. Heiler) almost 5 years ago
I have not thought about the proposal here quite much so far, sorry (and admittedly I may need
to do so on a fresh morning as I am a bit tired).
Just two brief comments:
-
Some of the examples remind me a bit of .select/.reject/.filter. Personally I try
to favour .select whenever possible, largely because I like positive selection/filtering. -
To your statement "In such use cases, it is more natural to describe the condition
in positive terms (i.e., zero?) rather than negative terms (i.e., nonzero?)".
Yup, completely agree with you there. We may have to include cases that do not have a
clear decisive difference, though, such as .odd? and .even? for numbers, and perhaps
other elements similar to this.
Updated by wishdev (John Higgins) almost 5 years ago
The answer here is simplicity (declarative) as opposed to wordsmithing.
We need to add an drop_until and take_until to handle 5-8 and drop_while and take_while handle 1-4
All 4 methods get a new parameter "bucket" which determines where the element that triggers the split gets placed :left or :right bucket
Your table looks something like this to me - I apologize but I prefer simpler examples
ary = [-1, 0, 1]
ary.take_while{ |x| x.nonzero? } # => [-1]
ary.take_while(:right){ |x| x.nonzero? } # => [-1]
ary.take_while(:left){ |x| x.nonzero? } # => [-1, 0]
ary.drop_while{ |x| x.nonzero? } # => [0, 1]
ary.drop_while(:right){ |x| x.nonzero? } # => [0, 1]
ary.drop_while(:left){ |x| x.nonzero? } # => [1]
ary.take_until{ |x| x.zero? } # => [-1]
ary.take_until(:right){ |x| x.zero? } # => [-1]
ary.take_until(:left){ |x| x.zero? } # => [-1, 0]
ary,drop_until{ |x| x.zero? } # => [0, 1]
ary,drop_until(:right){ |x| x.zero? } # => [0, 1]
ary.drop_until(:left){ |x| x.zero? } # => [1]
#take_* gets the left bucket and #drop_* gets the right bucket - the default would be to place the trigger in the right bucket (take_while and drop_while current behave with :right as their option in this scenario).
John
Updated by sawa (Tsuyoshi Sawada) almost 5 years ago
wishdev (John Higgins) wrote:
I prefer simpler examples
ary = [-1, 0, 1]
My intention of including two 0
s in the example was to show that there remains an asymmmetry; the search is done from the left, not from the right. In principle, we can still extend the paradigm, but I am not asking for that.
Updated by wishdev (John Higgins) almost 5 years ago
sawa (Tsuyoshi Sawada) wrote:
wishdev (John Higgins) wrote:
I prefer simpler examples
ary = [-1, 0, 1]My intention of including two
0
s in the example was to show that there remains an asymmmetry; the search is done from the left, not from the right. In principle, we can still extend the paradigm, but I am not asking for that.
Nothing wrong with that at all - but I think adding a :reverse options to the methods would accomplish a right to left search feature.
As I said - declaring what you want to do as opposed to trying to come up with some group of 2 words descriptions seems the wiser path. Especially looking at concepts like reversing the search direction.........
Updated by Dan0042 (Daniel DeLorme) almost 5 years ago
I think the positive counterparts of take_while/drop_while
should be take_until/drop_until
. And although take_upto
and drop_upto
are nicely intuitive and descriptive, I can't think of any good names for their negative counterparts (3) and (4). That may be an indication that we'd be better with a parameter as wishdev suggests. However I find that :left
and :right
do not read naturally at all. Maybe a number to indicate how many extra items to take or drop:
enum.take_while(&:nonzero?) # => [1, 1]
enum.drop_while(&:nonzero?) # => [0, 3, 3, 0, 5, 5]
enum.take_while(+1, &:nonzero?) # => [1, 1, 0]
enum.drop_while(+1, &:nonzero?) # => [3, 3, 0, 5, 5]
It's worth noting that there's several other succint ways to accomplish the same thing, so I'm not convinced it's worth adding that many methods. Not everything has to be a one-liner.
i = ary.index(&:zero?) || ary.size
ary[0...i] # => [1, 1]
ary[0..i] # => [1, 1, 0]
ary[i..-1] # => [0, 3, 3, 0, 5, 5]
ary[i+1..-1] # => [3, 3, 0, 5, 5]
n = enum.find_index(&:zero?) || enum.count
enum.take(n) # => [1, 1]
enum.take(n+1) # => [1, 1, 0]
enum.drop(n) # => [0, 3, 3, 0, 5, 5]
enum.drop(n+1) # => [3, 3, 0, 5, 5]
BTW you could also classify the three factors as:
(1) Whether we want to express the items to include (take) or exclude (drop)
(2) Whether we want the left side or the right side in the returned output
(3) Whether we want the items to be inclusive or exclusive of the boundary element
(1) | (2) | (3) | method | example | |
---|---|---|---|---|---|
1 | take | left | exclusive | take_before | enum.take_before(&:zero?) # => [1, 1] |
1 | take | left | inclusive | take_upto | enum.take_upto(&:zero?) # => [1, 1, 0] |
1 | take | right | inclusive | take_from | enum.take_from(&:zero?) # => [0, 3, 3, 0, 5, 5] |
1 | take | right | exclusive | take_after | enum.take_after(&:zero?) # => [3, 3, 0, 5, 5] |
1 | drop | left | exclusive | drop_before | enum.drop_before(&:zero?) # => [0, 3, 3, 0, 5, 5] |
1 | drop | left | inclusive | drop_upto | enum.drop_upto(&:zero?) # => [3, 3, 0, 5, 5] |
1 | drop | right | inclusive | drop_from | enum.drop_from(&:zero?) # => [1, 1] |
1 | drop | right | exclusive | drop_after | enum.drop_after(&:zero?) # => [1, 1, 0] |
This is what I'd call a declarative style, as it describes what the method does, not how. take_while
feels more like an imperative style to me since it describes the how, the control flow.
Ultimately there's so many different ways to think about and express any given operation... there's More Than One Way To Do It but they don't all have to be in the ruby core.
Updated by sawa (Tsuyoshi Sawada) almost 5 years ago
I understand some people are thinking that adding all these methods is doing too much.
I came up with an idea of using just one method: cut
. It works like String#partition
when it does not take an argument (but unfortunately, the method name Enumerable#partition
is already used for a different purpose). I took the terminology from Dedekind cut.
enum.cut(&:zero?) # # => [[1, 1], [0], [3, 3, 0, 5, 5]]
It would take optional keyword argument :take
or :drop
.
enum.cut(take: :left, &:zero?) # => [1, 1]
enum.cut(drop: :left, &:zero?) # => [0, 3, 3, 0, 5, 5]
enum.cut(drop: :right, &:zero?) # => [1, 1, 0]
enum.cut(take: :right, &:zero?) # => [3, 3, 0, 5, 5]
Updated by akr (Akira Tanaka) almost 5 years ago
- Related to Feature #16441: Enumerable#take_while_after added
Updated by matz (Yukihiro Matsumoto) almost 5 years ago
- Status changed from Open to Rejected
I don't think this many methods only introduce confusion. I feel negative. Besides that, I see the cut
method is too difficult to use.
Matz.