From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.2 (2018-09-13) on dcvr.yhbt.net X-Spam-Level: X-Spam-ASN: AS4713 221.184.0.0/13 X-Spam-Status: No, score=-2.6 required=3.0 tests=AWL,BAYES_00, DKIM_ADSP_CUSTOM_MED,FORGED_GMAIL_RCVD,FREEMAIL_FORGED_FROMDOMAIN, FREEMAIL_FROM,HEADER_FROM_DIFFERENT_DOMAINS,MAILING_LIST_MULTI, RCVD_IN_DNSWL_MED,SPF_HELO_NONE,SPF_PASS,UNPARSEABLE_RELAY shortcircuit=no autolearn=no autolearn_force=no version=3.4.2 Received: from neon.ruby-lang.org (neon.ruby-lang.org [221.186.184.75]) by dcvr.yhbt.net (Postfix) with ESMTP id 60F7A1F8C6 for ; Sun, 4 Jul 2021 07:11:02 +0000 (UTC) Received: from neon.ruby-lang.org (localhost [IPv6:::1]) by neon.ruby-lang.org (Postfix) with ESMTP id ADA5B120995; Sun, 4 Jul 2021 16:09:43 +0900 (JST) Received: from o1678948x4.outbound-mail.sendgrid.net (o1678948x4.outbound-mail.sendgrid.net [167.89.48.4]) by neon.ruby-lang.org (Postfix) with ESMTPS id 7795C1209B7 for ; Sun, 4 Jul 2021 16:09:41 +0900 (JST) Received: by filterdrecv-canary-6956fc8f6-ljk29 with SMTP id filterdrecv-canary-6956fc8f6-ljk29-1-60E15EF6-12 2021-07-04 07:10:46.498556352 +0000 UTC m=+296976.430348490 Received: from herokuapp.com (unknown) by ismtpd0149p1mdw1.sendgrid.net (SG) with ESMTP id Ae8WFZEBRSqQKVsTXn64cg for ; Sun, 04 Jul 2021 07:10:46.438 +0000 (UTC) Date: Sun, 04 Jul 2021 07:10:46 +0000 (UTC) From: josh.cheek@gmail.com Message-ID: References: Mime-Version: 1.0 X-Redmine-Project: ruby-master X-Redmine-Issue-Tracker: Bug X-Redmine-Issue-Id: 18021 X-Redmine-Issue-Author: josh.cheek X-Redmine-Sender: josh.cheek X-Mailer: Redmine X-Redmine-Host: bugs.ruby-lang.org X-Redmine-Site: Ruby Issue Tracking System X-Auto-Response-Suppress: All Auto-Submitted: auto-generated X-Redmine-MailingListIntegration-Message-Ids: 80614 X-SG-EID: =?us-ascii?Q?G9ydTtk2SJoSGpJrwmaKWlNzSJSRPYgrzFotqb=2FkjI2BVEG+XI+RgbYJEMoZkw?= =?us-ascii?Q?OoEsGb68S2ZVa57Rg6dRD8z5EEWV8FE0T8b68K8?= =?us-ascii?Q?2yE06AUg5aelp=2FruxZ5YucKhvXGdHSv4uKpiq+D?= =?us-ascii?Q?=2FqMXmg73avOwlh2odUIxtwv7NkFKCzWQU+X5L=2FS?= =?us-ascii?Q?KLiBUYvbe2hxp2LN+t83iqjbzX5znpr4KdNW=2Fn6?= =?us-ascii?Q?F4tMbptKgTIrlsxbI=3D?= To: ruby-core@ruby-lang.org X-Entity-ID: b/2+PoftWZ6GuOu3b0IycA== X-ML-Name: ruby-core X-Mail-Count: 104490 Subject: [ruby-core:104490] [Ruby master Bug#18021] Mixins in Refinements: possibly multiple bugs, workarounds are awkward X-BeenThere: ruby-core@ruby-lang.org X-Mailman-Version: 2.1.15 Precedence: list Reply-To: Ruby developers List-Id: Ruby developers List-Unsubscribe: , List-Post: List-Help: List-Subscribe: , Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: ruby-core-bounces@ruby-lang.org Sender: "ruby-core" Issue #18021 has been reported by josh.cheek (Josh Cheek). ---------------------------------------- Bug #18021: Mixins in Refinements: possibly multiple bugs, workarounds are awkward https://bugs.ruby-lang.org/issues/18021 * Author: josh.cheek (Josh Cheek) * Status: Open * Priority: Normal * ruby -v: ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [arm64-darwin20] * Backport: 2.6: UNKNOWN, 2.7: UNKNOWN, 3.0: UNKNOWN ---------------------------------------- ## Maybe bug 1 Refinements must be created after methods are defined. ```ruby # This one seems buggy module M1 refine(Object) { include M1 } def m() = M1 using M1 m rescue $! # => # end # It works if you refine it after the method is defined module M2 def m() = M2 refine(Object) { include M2 } using M2 m # => M2 end # If it wasn't in a refinement, it would work module M3 Object.class_eval { include M3 } def m() = M3 m # => M3 end ``` ## Maybe bug 2 Here, a module can't delegate to another method it defines, while using a refinement. Presumably this is because the refinement is lexically scoped and where the method is defined, it is not within that scope. ```ruby module M def a() = 1 def b() = a # ~> NameError: undefined local variable or method `a' for main:Object refine(Object) { include M } end using M a # => 1 b # => ``` ## Maybe bug 3 Refinements declared after methods are defined are not available to those methods, even when they use the refinement. We try to use the module to address #2, but because of #1, the `refine` must go after the definitions. ```ruby module M using M # <-- this line is added to the example from #2 def a() = 1 def b() = a # ~> NameError: undefined local variable or method `a' for main:Object refine(Object) { include M } end using M a # => 1 b # => ``` ## Awkward workarounds: So, `refine` depends on `def`, which depends on `using`, which depends on `refine`, which is where we started. Here are several ways I found to break the cycle of dependance: #### 1. "declare" methods by predefining them ```ruby module M # Predefine method stubs def a() = nil def b() = nil # Now the refinement can see them, define the refinement refine(Object) { include M } # Now that the refinement exists, `using` will make it available using M # Now method definitions can see each other, # so overwrite the stubs with the real implementations def a() = 1 def b() = a end # And now things work as expected using M a # => 1 b # => 1 ``` #### 2. Put the module body in a loop First iteration defines them without visibility to each other, because the refinement hasn't been made yet. On the second iteration, the refinement has been made, so the second time they're defined, they can see each other. ```ruby module M 2.times do using M # this can go either here or after `refine` below def a() = 1 def b() = a refine(Object) { include M } end end # And now things work as expected using M a # => 1 b # => 1 ``` #### 3. Define the methods in a block and call it before and after refining ```ruby module M methods = lambda do using M def a() 1 end def b() a end end methods.call refine(Object) { include M } methods.call end using M b # => 1 ``` #### 4. (this does not work) Include the refinement into the module Including this one, b/c it feels like maybe some sort of combination refinement/mixin might be what is needed. Like generally, methods defined in a refinement can see other methods, even across other blocks, if they're all on the same module. The problem is there isn't an obvious way to get them back out so that they're visible in the module outside of the refinement. ```ruby module M include refine(M) { # ~> ArgumentError: refinement module is not allowed def a() = 1 def b() = a } refine(Object) { include M } end ``` ## The problem with the obvious workaround There is an obvious workaround: don't put them in a module, just refine directly: ```ruby module M refine Object do def a() = 1 def b() = a end end using M b # => 1 ``` However, then that behaviour isn't available for mixing into other classes and objects. So if I want both (which I often find I do), then there don't seem to be any good options available. For example, can anyone come up with a better way to write the example below? (without moving `to_type` to `Object`, as that can't be expected to work for the next helper method). ```ruby module Types 2.times do using Types private def to_type(o) = (Symbol === o ? RespondTo.new(o) : o) def |(type) = Or.new(to_type(self), to_type(type)) refine(Module) { include Types } refine(Symbol) { include Types } end RespondTo = Struct.new(:name) { def ===(o) o.respond_to? name end def inspect() = ".respond_to?(#{name.inspect})" }.include(Types) Or = Struct.new(:left, :right) { def ===(o) left === o || right === o end def inspect() = "(#{left.inspect} | #{right.inspect})" }.include(Types) end using Types Integer | String | (:to_int | :to_str) # => ((Integer | String) | (.respond_to?(:to_int) | .respond_to?(:to_str))) ``` -- https://bugs.ruby-lang.org/