ruby-core@ruby-lang.org archive (unofficial mirror)
 help / color / mirror / Atom feed
From: daniel@dan42.com
To: ruby-core@ruby-lang.org
Subject: [ruby-core:96526] [Ruby master Feature#16446] Enumerable#take_*, Enumerable#drop_* counterparts with positive conditions
Date: Fri, 27 Dec 2019 20:43:01 +0000 (UTC)	[thread overview]
Message-ID: <redmine.journal-83457.20191227204300.26a0993cc333039c@ruby-lang.org> (raw)
In-Reply-To: redmine.issue-16446.20191223082605@ruby-lang.org

Issue #16446 has been updated by Dan0042 (Daniel DeLorme).


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:

```ruby
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.

```ruby
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.

----------------------------------------
Feature #16446: Enumerable#take_*, Enumerable#drop_* counterparts with positive conditions
https://bugs.ruby-lang.org/issues/16446#change-83457

* Author: sawa (Tsuyoshi Sawada)
* Status: Open
* Priority: Normal
* Assignee: 
* Target version: 
----------------------------------------
#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.

```ruby
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`|




-- 
https://bugs.ruby-lang.org/

  parent reply	other threads:[~2019-12-27 20:43 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
     [not found] <redmine.issue-16446.20191223082605@ruby-lang.org>
2019-12-23  8:26 ` [ruby-core:96419] [Ruby master Feature#16446] Enumerable#take_*, Enumerable#drop_* counterparts with positive conditions sawadatsuyoshi
2019-12-23 15:14 ` [ruby-core:96434] " shevegen
2019-12-23 20:29 ` [ruby-core:96440] " wishdev
2019-12-23 21:52 ` [ruby-core:96442] " sawadatsuyoshi
2019-12-23 22:12 ` [ruby-core:96443] " wishdev
2019-12-27 20:43 ` daniel [this message]
2019-12-28 17:59 ` [ruby-core:96558] " sawadatsuyoshi
2020-01-16  6:56 ` [ruby-core:96893] " matz

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-list from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://www.ruby-lang.org/en/community/mailing-lists/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=redmine.journal-83457.20191227204300.26a0993cc333039c@ruby-lang.org \
    --to=ruby-core@ruby-lang.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).