Issue #22213 has been reported by shugo (Shugo Maeda). ---------------------------------------- Feature #22213: Allow no-argument and chained calls of Proc#refined https://bugs.ruby-lang.org/issues/22213 * Author: shugo (Shugo Maeda) * Status: Open ---------------------------------------- ## Abstract Currently, `Proc#refined` rejects no-argument and chained calls, because I wasn't sure what the desired behavior should be. How about returning the receiver for no-argument calls: ```ruby original = -> {} refined = original.refined p refined.equal?(original) #=> true ``` and activating all the given modules for chained calls? ```ruby module M refine String do def shout = upcase + "!" end end module M2 refine Integer do def doubled = self * 2 end end original = ->(s, i) { [s.shout, i.doubled] } refined = original.refined(M).refined(M2) # the same result as original.refined(M, M2) refined.call("hi", 3) #=> ["HI!", 6] ``` ## Motivation This change is necessary to make `Proc#refined` composable. A no-argument call of `Proc#refined` is a no-op, but it's useful when the arguments are calculated at runtime: ```ruby def foo(prc, *modules) ... refined = prc.refined(*modules) ... end ``` If an empty module list were rejected, a guard like `unless modules.empty?` would be needed. A chained call of `Proc#refined` is useful to extend existing DSLs: ```ruby require "packrat_parser" require "mathn" class MathParser < PackratParser def define(sym, &block) # PackratParser#define also calls Proc#refined super(sym, &block.refined(Math::N)) end end parser = MathParser.new { ... define :multitive do [:primary, "*", :multitive].map { |x, _, y| x * y } | [:primary, "/", :multitive].map { |x, _, y| x / y } | :primary end ... } parser.parse("1 / 2") #=> (1/2) ``` ## Formal semantics With this proposal, `Proc#refined` can be formalized as a monoid action on Procs created from a block, where the acting monoid is the sequences of modules under concatenation, with the empty sequence as its unit. Writing ≃ for behavioral equivalence, the action satisfies the unit and associativity laws: ``` prc.refined ≃ prc # unit law prc.refined(*ms).refined(*ns) ≃ prc.refined(*ms, *ns) # associativity law ``` Note that the unit law holds for any Proc, since `refined` with no modules returns the receiver itself. I've written a formalization in the Rocq Prover: https://github.com/shugo/ruby-refinements-semantics It abstracts away the details irrelevant to refinements. ## Design decisions ### The receiver or a copy for no-argument calls Returning a copy was also considered, but I chose to return the receiver, following precedents such as `Data#with` and `Enumerator::Lazy#lazy`. Mutable containers tend to return a copy even for a no-op, but `Proc#refined` is closer to the former group. ### Extend or replace for chained calls If a chained call replaced the receiver's refinements instead of extending them, `prc.refined(M).refined(M2)` would activate only `M2`. That breaks the associativity law above, and makes chaining useless for the DSL case in Motivation. Extending also matches nested `using`, where refinements activated later take precedence. ## Implementation Pull request: https://github.com/ruby/ruby/pull/18052 `Proc#refined` on an already refined Proc is not memoized: the cache would be keyed on short-lived copies, and entries are retained for the VM's lifetime, so it would grow without bound. A performance warning is emitted under `Warning[:performance] = true`, and passing all the modules in a single call is more efficient. -- https://bugs.ruby-lang.org/