Project

General

Profile

Actions

Feature #17330

open

Object#non

Added by zverok (Victor Shepelev) over 3 years ago. Updated over 1 year ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:100897]

Description

(As always "with core" method proposals, I don't expect quick success, but hope for a fruitful discussion)

Reasons:

Ruby always tried to be very chainability-friendly. Recently, with introduction of .then and =>, even more so. But one pattern that frequently emerges and doesn't have good idiomatic expression: calculate something, and if it is not a "good" value, return nil (or provide default value with ||). There are currently two partial solutions:

  1. nonzero? in Ruby core (frequently mocked for "inadequate" behavior, as it is looking like predicate method, but instead of true/false returns an original value or nil)
  2. ActiveSupport Object#presence, which also returns an original value or nil if it is not "present" (e.g. nil or empty? in AS-speak)

Both of them prove themselves quite useful in some domains, but they are targeting only those particular domains, look unlike each other, and can be confusing.

Proposal:

Method Object#non (or Kernel#non), which receives a block, calls it with receiver and returns nil (if block matched) or receiver otherwise.

Prototype implementation:
class Object
  def non
    self unless yield(self)
  end
end
Usage examples:
  1. With number:

    limit = calculate.some.limit
    limit.zero? ? DEFAULT_LIMIT : limit
    # or, with nonzero?
    calculate.some.limit.nonzero? || DEFAULT_LIMIT
    # with non:
    calculate.some.limit.non(&:zero?) || DEFAULT_LIMIT
    # ^ Note here, how, unlike `nonzero?`, we see predicate-y ?, but it is INSIDE the `non()` and less confusing
    
  2. With string:

    name = params[:name] if params[:name] && !params[:name].empty?
    # or, with ActiveSupport:
    name = params[:name].presence
    # with non:
    name = params[:name]&.non(&:empty?)
    
  3. More complicated example

    action = payload.dig('action', 'type')
    return if PROHIBITED_ACTIONS.include?(action)
    send("do_#{action}")
    # with non & then:
    payload.dig('action', 'type')
      .non { |action| PROHIBITED_ACTIONS.include?(action) }
      &.then { |action| send("do_#{action}") }
    

Basically, the proposal is a "chainable guard clause" that allows to "chain"ify and DRYify code like:

value = fetch_something
return value unless value.with_problems?
# which turns into
fetch_something.non(&:with_problems?)

# or
value = fetch_something
value = reasonable_default if value.with_problems?
# turns into
value = fetch_something.non(&:with_problems?) || reasonable_default

I believe that this idiom is frequent enough, in combinations like (assorted examples) "read config file but return nil if it is empty/wrong version", "fetch latest invoice, but ignore if it has an unpayable flag", "fetch a list of last user's searches, but if it is empty, provide default search hints" etc.

I believe there is un unreflected need for idiom like this, the need that is demonstrated by the existence of nonzero? and presence.


Related issues 1 (0 open1 closed)

Related to Ruby master - Feature #12075: some container#nonempty?Feedbackmatz (Yukihiro Matsumoto)Actions
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0