Project

General

Profile

Actions

Feature #17330

open

Object#non

Added by zverok (Victor Shepelev) 11 months ago. Updated 9 months ago.

Status:
Open
Priority:
Normal
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}") }
    

Possible extensions of the idea

It is quite tempting to define the symmetric method named -- as we already have Object#then -- Object#when:

some.long.calculation.when { |val| val < 10 } # returns nil if value >= 10
# or even... with support for ===
some.long.calculation.when(..10)&.then { continue to do something }

...but I am afraid I've overstayed my welcome :)


Related issues

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

Updated by nobu (Nobuyoshi Nakada) 11 months ago

Actions #3

Updated by nobu (Nobuyoshi Nakada) 11 months ago

  • Description updated (diff)
Actions #4

Updated by nobu (Nobuyoshi Nakada) 11 months ago

  • Description updated (diff)

Updated by sawa (Tsuyoshi Sawada) 10 months ago

I think the proposed feature would be useful, but I feel that your focus on use cases with a negative predicate is artificial. Positive predicates should have as many corresponding use cases. And since negation is always one step more complex than its positive counterpart, you should first (or simultaneously) propose the positive version, say Object#oui:

calculate.some.limit.oui(&:nonzero?) || DEFAULT_LIMIT

params[:name]&.oui{ _1.empty?.! }

payload.dig('action', 'type').oui{ PROHIBITED_ACTIONS.include?(_1).! }

I think such feature has actually been proposed in the past.

Furthermore, I suggest you may also propose the method(s) to take a variable numbers of arguments to match against:

payload.dig('action', 'type').non(*PROHIBITED_ACTIONS)

And by "match", I think that using the predicate === would be more useful than ==:

"foo".non(/\AError: /, /\AOops, /) # => "foo"
"Oops, something went wrong.".non(/\AError: /, /\AOops, /) # => nil

Updated by akr (Akira Tanaka) 9 months ago

I prefer "not" method than this "non" method.

x.empty?.not

Although x.empty?.! is possible now, ! method is not very readable.

Updated by matz (Yukihiro Matsumoto) 9 months ago

I don't see the non method make code more readable by glancing at the examples.
Can you elaborate on the benefit of the proposal?

Matz.

Updated by zverok (Victor Shepelev) 9 months ago

matz (Yukihiro Matsumoto) Thinking a bit more about it, what I frequently lack is a "singular reject": "drop this value if it doesn't match expectations".

# HTTP-get something, don't return result unless successful:
response = Faraday.get(url)
return response unless response.code >= 400
# or worse:
unless response.code >= 400
  return response.body
end

# With non
return Faraday.get(url).non { |r| r.code >= 400 }
# Note how little changes necessary to say "successful response's body"
return Faraday.get(url).non { |r| r.code >= 400 }&.body

Though, it seems for me when explaining that one needs a symmetric pair of "singular select"/"singular reject", because particular Faraday examples maybe better written as

return Faraday.get(url).when(&:successful?)
# Note how little changes necessary to say "successful response's body"
return Faraday.get(url).when(&:successful?)&.body

Updated by nobu (Nobuyoshi Nakada) 9 months ago

zverok (Victor Shepelev) wrote in #note-8:

return Faraday.get(url).when(&:successful?)
# Note how little changes necessary to say "successful response's body"
return Faraday.get(url).when(&:successful?)&.body

This seems readable enough and more flexible.

return Faraday.get(url).then {_1.body if _1.successful?}

Or, make successful? to return self or nil, then

return Faraday.get(url).successful?&.body

Updated by zverok (Victor Shepelev) 9 months ago

nobu (Nobuyoshi Nakada)

This seems readable enough and more flexible.

return Faraday.get(url).then {_1.body if _1.successful?}

I argue that the idiom (return something/continue with something, conditionally) is so frequent, that it should be more atomic.

Or, make successful? to return self or nil

That's what I started this ticket with!

  • This is not natural for Ruby: nonzero? is frequently mocked and generally considered a "design error"; Rails' presence has kind of the same vibe
  • This requires me to change Faraday (which, to be clear, I have no relation to), and then go and change every library which causes the same idiom :)
  • This is solving very particular case (just like nonzero? and presence), I am talking about generic.

Here's one more example:

Faraday.get(url).body.then { JSON.parse(_1, symbolize_names: true) }.non { _1.key?('error') }&.dig('user', 'status')

Updated by Dan0042 (Daniel DeLorme) 9 months ago

FWIW, +1 from me

At first I thought the only uses were non(&:zero?) and non(&:empty?) which, while I find very elegant, are not enough to justify adding a method to Kernel. But I think zverok has shown enough other uses to make a convincing case.

There might be something to say about how this provides an automatic complement to any predicate method:

if user.subscription.non(&:expired?)     # is more grammatically correct (English-wise)
if !user.subscription.expired?           # than the traditional negation
if user.subscription.expired?.!          # or akr style

if user.subscription&.non(&:expired?)    # particularly useful in chain
if s = user.subscription and !s.expired?           # the alternatives
if user.subscription&.then{ |s| s if !s.expired? } # are not very
if user.subscription&.then{ _1 if !_1.expired? }   # attractive (IMO)

Overall this "singular reject/select" goes in the same direction as tap and then, so if those two are considered improvements to ruby, I think non and when would be the same kind of improvement.

Can we also consider non(:empty?) in the same vein as inject, or is that an anti-pattern?

Actions

Also available in: Atom PDF