Project

General

Profile

Actions

Feature #20498

open

Negated method calls

Added by MaxLap (Maxime Lapointe) about 1 month ago. Updated about 1 month ago.

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

Description

I want to propose the following syntax: foo.!bar. I know it's already valid syntax, but please read on for details.

When someone write a somewhat long line of code that is negated, the main way I've seen of doing it is:

must_create_user = !User.where(somelong: :condition, even_more: "thing").exists?

I personally highly dislike it, as I must keep the "not" in the back of my mind as I read the line. When quickly reading a line like this, it's super easy to misread and understand the opposite result.

The current ways around this I can think of are:

  • rename the variable (can be annoying)
  • Use unless (only possible when in a condition; some people, like me, have a hard time grapsping a unless)
  • use a .!in the end (foo.exists?.!), I've never seen that and it looks ugly to me (this is subjective).
  • create a new method name with the negated meaning (not always possible)

My proposal would look like this:

must_create_user = User.where(somelong: :condition, even_more: "thing").!exists?

You cannot forget the bang that you saw 15 words ago, it's right there.

It also basically reads as English: "user where ... doesn't exists".

The main argument against this I can think of is that it's technically already a valid syntax. I believe it's frowned upon to override the bang operator and I'm not aware of places where it is overridden to with also having a parameter.

I made a prototype in RubyNext, which you can try here: https://ruby-next.github.io/#gist:0e133bf6f27f2437193dc034d58083dc

Clarification: the prototype is not perfect and does not handle foo&.!empty?. In that case, if foo is nil, the result of the expression would be nil.


Related issues 1 (0 open1 closed)

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

Updated by duerst (Martin Dürst) about 1 month ago

I think defining an explicit method for this purpose (on Object, I guess) would be better, because it wouldn't add complications to the syntax. The name could be not or invert or `negate or some such. Your result would look like:

must_create_user = User.where(somelong: :condition, even_more: "thing").exists.not

BTW, different languages use different places (start, middle, end, several places) for the negation. (Japanese puts it at the end.)

Actions #2

Updated by nobu (Nobuyoshi Nakada) about 1 month ago

Updated by nobu (Nobuyoshi Nakada) about 1 month ago

In your prototype, foo . ! exist? was transpiled to !(foo . exist?).
Your proposal is not a new operator, but a syntax sugar?

Updated by akr (Akira Tanaka) about 1 month ago

We can use foo.!.

must_create_user = User.where(somelong: :condition, even_more: "thing").exists?.!

Updated by ufuk (Ufuk Kayserilioglu) about 1 month ago

I wonder how wild it would be to make ! accept an optional block. That way we could write:

must_create_user = User.where(somelong: :condition, even_more: "thing").!(&:exists?)

which very close to what the original poster wants, and is a much smaller method change instead of a syntax change.

Updated by hmdne (hmdne -) about 1 month ago

I saw such a proposal before and I thought of some syntax and implementation, but I didn't submit that in the previous issue:

class MethodNegator < BasicObject
  def initialize(obj)
    @obj = obj
  end

  def method_missing(method, ...)
    @obj.public_send(method, ...)
  end

  def respond_to_missing?(include_all = false)
    @obj.respond_to?(include_all)
  end
end

module Kernel
  def non(sym=nil)
    if sym
      proc { |*args,**kwargs,&block| !sym.to_proc.call(*args,**kwargs,&block) }
    else
      MethodNegator.new(self)
    end
  end
end

p "".non.empty?
# => true
p ["", "a", "b"].select(&non(:empty?))
# => ["a", "b"]

Updated by MaxLap (Maxime Lapointe) about 1 month ago

Thanks for the feedback. I updated the gist to have an example with arguments in the call:

puts A.new.!exists?(with_friends: true, skip_administrator: true)

This highlights some problems with the alternatives:

  • Calls at the end (such as .! or .not) are very easy to miss if there are arguments given to the method. The longer the args, the most likely to be missed:
puts A.new.exists?(with_friends: true, skip_administrator: true).!
puts A.new.exists?(with_friends: true, skip_administrator: true).not
  • Using a no block (ex: foo.!(&:exists?)) as ufuk (Ufuk Kayserilioglu) suggested is no longer clean looking. The extra syntax ends up almost hiding the !:
foo.!{ _1.exists?(with_friends: true, skip_administrator: true) }

About p "".non.empty?: I personally dislike this pattern because there is a clear performance cost to it (extra method call & creating an object). As a result, whenever I would have to use it, I would wonder if the cleaner looking code is worth it knowing it's wasteful at the same time. (I know it's not much, but it's enough for me to ask myself each time, I dislike Rails' .not.where pattern since it could have simply been not_where or where_not.)

@nobu (Nobuyoshi Nakada): I'd say it's both syntax sugar and an operator, just like &..

Updated by marcandre (Marc-Andre Lafortune) about 1 month ago

MaxLap (Maxime Lapointe) wrote in #note-7:

@nobu (Nobuyoshi Nakada): I'd say it's both syntax sugar and an operator, just like &..

Seems like pure syntax sugar.

That's not to say it's not a good idea. It's difficult to say if the added convenience is worth it versus the added cognitive load required / added difficulty to learn the language.

Actions #9

Updated by MaxLap (Maxime Lapointe) about 1 month ago

  • Description updated (diff)
Actions #10

Updated by MaxLap (Maxime Lapointe) about 1 month ago

  • Description updated (diff)

Updated by MaxLap (Maxime Lapointe) about 1 month ago

For foo&.!empty?, the result would be nil if foo is nil. This is not handled by the prototype.

The alternatives mentionned so far look like this:

foo&.empty?&.!
foo&.non&.empty?
foo&.empty?&.not
foo&.!(&:empty?)

Which I hope no one writes 😂

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0