Project

General

Profile

Actions

Feature #22392

open

Allow refinements to define && and ||

Feature #22392: Allow refinements to define && and ||

Added by shugo (Shugo Maeda) 18 minutes ago.

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

Description

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:

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:

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.

No data to display

Actions

Also available in: PDF Atom