Feature #17330
openObject#non
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:
-
nonzero?
in Ruby core (frequently mocked for "inadequate" behavior, as it is looking like predicate method, but instead oftrue
/false
returns an original value ornil
) - ActiveSupport
Object#presence
, which also returns an original value ornil
if it is not "present" (e.g.nil
orempty?
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:¶
-
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
-
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?)
-
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
.