Feature #20498
openNegated method calls
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 aunless
) - 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
.
Updated by duerst (Martin Dürst) 6 months 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.)
Updated by nobu (Nobuyoshi Nakada) 6 months ago
- Related to Feature #12075: some container#nonempty? added
Updated by nobu (Nobuyoshi Nakada) 6 months 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) 6 months ago
We can use foo.!
.
must_create_user = User.where(somelong: :condition, even_more: "thing").exists?.!
Updated by ufuk (Ufuk Kayserilioglu) 6 months 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 -) 6 months 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) 6 months 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) 6 months 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.
Updated by MaxLap (Maxime Lapointe) 6 months 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 😂