Issue #22392 has been reported by shugo (Shugo Maeda). ---------------------------------------- Feature #22392: Allow refinements to define && and || https://bugs.ruby-lang.org/issues/22392 * Author: shugo (Shugo Maeda) * Status: Open * Target version: 4.1 ---------------------------------------- ## Abstract I propose to allow refinements to define `&&` and `||`, so that DSLs can build expressions such as `name == "x" && age > 20`. The right-hand side is evaluated before the method is called, so short-circuiting is not customizable. ## Motivation Query builders and similar DSLs want to capture boolean expressions written in a block: ```ruby User.where { name == "x" && age > 20 } ``` Today such DSLs have to use `&` and `|` instead, which bind tighter than `==` and `>`, so every operand needs parentheses: `(name == "x") & (age > 20)`. ## Proposal `&&` and `||` can be defined as methods in refinements: ```ruby module QueryDSL refine Condition do def &&(other) = And.new(self, other) def ||(other) = Or.new(self, other) end end def User.where(&block) = build(block.refined(QueryDSL).call) User.where { name == "x" && age > 20 } # => And(Eq(name, "x"), Gt(age, 20)) ``` Semantics of `a && b` (`a || b` is symmetric): * If the class of `a` has `&&` defined by a refinement active in the current scope, `b` is evaluated and `a.&&(b)` is the result. * Otherwise, the behavior is unchanged. With a hook, the result is always `a.&&(b)`, which never short-circuits. Details: * `&&` and `||` can be defined only in refinements. Defining them elsewhere raises NameError. A hook changes when the right-hand side is evaluated, so its effect should be limited to code that opted in by `using` or `Proc#refined`, rather than reaching every method that receives such an object. There are no default methods such as `Object#&&`, so `super` is not available in a hook. * The hooks also apply in conditions such as `if a && b`, so that `x = a && b; if x` and `if a && b` behave the same. * `and` and `or` behave the same as `&&` and `||`. * `&&=` and `||=` do not use the hooks, to keep the memoization idiom. * `def &&`, `def ||`, `:&&` and `:||` become valid syntax. ## Prior art Scala and Swift treat `&&` and `||` as ordinary methods, so DSLs such as Slick define them on their own types, and the compiler resolves them statically. C# lets a type define `&`, `|`, `true` and `false`, and expands `x && y` to `T.false(x) ? x : x & y`. C++ allows overloading `&&` globally, which is discouraged because short-circuiting is lost for every user of the type. Python, Kotlin, Rust and Julia don't allow it; their DSLs use `&` and `|` with the same precedence problem as in Ruby, and PEP 335, which proposed overloadable `and`/`or`, was rejected for the cost it added to every `and`/`or` and for its complexity. This proposal follows Scala and Swift in dispatching on the receiver, at run time since Ruby is dynamically typed, and limits the definitions to refinements so that, unlike in C++, code outside the DSL keeps short-circuiting. Code that doesn't use the hooks pays nothing, unlike in PEP 335. The right-hand side is evaluated eagerly rather than passed lazily: a hook that builds an expression needs its value in any case, so laziness would add complexity without a use. -- https://bugs.ruby-lang.org/