Project

General

Profile

Feature #14344

refine at class level

Added by kddeisz (Kevin Deisz) 9 months ago. Updated 6 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:84778]

Description

I rely on refinements a lot, but don't want to keep writing Module.new in code. I'm proposing Object::refine, which would create an anonymous module behind the scenes with equivalent functionality. So:

class Test
  using Module.new {
    refine String do
      def refined?
        true
      end
    end
  }
end

would become

class Test
  refine String do
    def refined?
      true
    end
  end
end

It's a small change, but reads a lot more clearly. Thoughts?

History

#1 [ruby-core:84779] Updated by shevegen (Robert A. Heiler) 9 months ago

I like the proposed syntax. Syntax is one major reason for
me, oddly enough, to not use refinements. In particular the
"using" clause felt weird, so I like changes (improvements?)
to the syntax.

I think your syntax suggestion is better than the status quo
too.

However had, I am saying all of this without really having
used refinements extensively (other than in test.rb files),
so I am essentially not qualified to comment really. I'd
be inclined to use your suggestion more than the "using"
variant, though. ;)

(There was some presentation at ruby or railsconf about
someone talking about nobody using refinements or so,
a few years ago perhaps. A bit like the old "nobody knows
nobu", which isn't true by now anymore either. Either way
I think people should comment who use refinements a lot
too, since they should be able to consider the impact of
the changed syntax proposal.)

#2 [ruby-core:84780] Updated by Eregon (Benoit Daloze) 9 months ago

The whole purpose of the Module around it is so multiple refinements can be bundled up together in a Module and that Module is passed to #using.

I don't think #refine should enable refinements after the block.
It doesn't seem clear to me and is too confusing with the existing usage of #refine.
Whether #refine is called on a Module or not would have very different semantics.

Also I guess in general refinements are not defined right in the class using them, especially if they are not trivial one-liner methods.

#3 [ruby-core:84781] Updated by kddeisz (Kevin Deisz) 9 months ago

I think we're talking about two different use cases. There is a place for refinements with large batch changes and a module is very appropriate for that. But for smaller refinements, which make life a lot easier, it's really not that far of a stretch. It's not that much different from class Foobar ... end being a shortcut for Foobar = Class.new do ... end

#4 [ruby-core:84782] Updated by Eregon (Benoit Daloze) 9 months ago

There is probably room for a shorter syntax for refinements.
But one thing which is not acceptable in this specific proposition here is that

class Test
  refine String do
    def refined?
      true
    end
  end
  # String refinements apply here
end

and

module Test
  refine String do
    def refined?
      true
    end
  end
  # String refinements do not apply here, it's a normal Module#refine
end

would do two very different things, which is too hard to understand and confusing.

Maybe a block to #using would make things slightly shorter:

class Test
  using do
    refine String do
      def refined?
        true
      end
    end
  end
end

#5 [ruby-core:84783] Updated by zverok (Victor Shepelev) 9 months ago

Also I guess in general refinements are not defined right in the class using them, especially if they are not trivial one-liner methods.

I believe that "one-liner" & "inplace" definition is one of the most important usages of refinements.

Because, speaking philosophically, why do we need them at all?

Instead of doing "refine String + call string.something" you always can StringUtilModule.something(string), but when you do some, say, complicated reporting algorithm, those StringUtilModule.something(string), especially several in a row can become REALLY ugly.

Therefore you just... add one small method to String. Just here and there, as close to its usage and as visible and as easy as possible, and it WILL be one-line method, just calling the same StringUtilModule.something(string) (which is easier to test and maintain and document).

I believe that "module with refinement for the several classes" is, to the opposite, much less frequent case. It is either "all cool shticks for my entire project in one file", or something very domain-specific (like, I don't know, String#as_currency, Numeric#as_money(currency), CSV#ready_money something).

#6 [ruby-core:84814] Updated by kddeisz (Kevin Deisz) 9 months ago

Just to take a real example from my current application, here's a job (from Rails ActiveJob) that I want to refine by moving the logic into the class in which it belongs. It currently looks like this:

class EventEndActionsJob < ApplicationJob
  queue_as :default

  def perform(event)
    return if event.end_actions_completed?
    event.update!(end_actions_completed: true)
    activate_survey_for(event) if event.survey
  end

  private

  def activate_survey_for(event)
    event.survey.update!(active: true)

    event.rsvps.not_declined.each do |rsvp|
      EmailJob.perform_later('PostEventSurvey', rsvp)
    end
  end
end

but I want it to look like this:

class EventEndActionsJob < ApplicationJob
  refine Event do
    def perform_end_actions
      return if end_actions_completed?

      update!(end_actions_completed: true)
      activate_survey if survey
    end

    private

    def activate_survey
      survey.update!(active: true)

      rsvps.not_declined.each do |rsvp|
        EmailJob.perform_later('PostEventSurvey', rsvp)
      end
    end
  end

  queue_as :default

  def perform(event)
    event.perform_end_actions
  end
end

now all of the logic is in the right place (in the Event model) but I don't have to clutter up the class definition with a method that will only be used in this one place. I don't need to refine multiple classes, so I don't want to build a whole module, but instead right now I have to:

class EventEndActionsJob < ApplicationJob
  using(
    Module.new do
      refine Event do
        def perform_end_actions
          return if end_actions_completed?

          update!(end_actions_completed: true)
          activate_survey if survey
        end

        private

        def activate_survey
          survey.update!(active: true)

          rsvps.not_declined.each do |rsvp|
            EmailJob.perform_later('PostEventSurvey', rsvp)
          end
        end
      end
    end
  )

  queue_as :default

  def perform(event)
    event.perform_end_actions
  end
end

#7 [ruby-core:84818] Updated by Eregon (Benoit Daloze) 9 months ago

kddeisz (Kevin Deisz) wrote:

Just to take a real example from my current application, here's a job (from Rails ActiveJob) that I want to refine by moving the logic into the class in which it belongs. It currently looks like this:

That's an interesting example indeed: using refinements to not pollute the model class but still make it convenient to write methods with the model as self.

To clarify my comment above: I'm not against a shorter way to define refinements+use them.
But #refine as proposed is wrong: if EventEndActionsJob was a module it would stop working because then
it would be the refine-just-define-refinements (current semantics) and not the refine-define-and-use-refinements you propose.

Or are you proposing to change the behavior of refine do ... end to always enable refinements after it until the end of the class/module body or the end of the file?
Then there would be a compatibility risk.

#8 [ruby-core:84819] Updated by kddeisz (Kevin Deisz) 9 months ago

I was proposing the former, which would be to have refine be a class method that would effectively be the same as using with an anonymous module. I get what you're saying about it being different between a class and a module but I'm not sure I necessary see that as a problem. Class and Module already don't have perfect parity (allocate, new, superclass) so it doesn't seem like we need to enforce that. I doubt people would be caught off guard by a change in the semantics of the method between Module and Class because it doesn't seem like it would be a common practice to be switching constants back and forth between modules and classes all the time.

#9 [ruby-core:86511] Updated by kddeisz (Kevin Deisz) 6 months ago

I haven't contributed before so I'm not sure how to bump this ticket, but I'd like to keep pushing on this. Could someone from core take a look at this proposal? I'd love to help introduce this syntax.

#10 [ruby-core:86550] Updated by Eregon (Benoit Daloze) 6 months ago

I am a MRI committer. This is just my opinion, but I'm confident it is shared by other committers as well.

I believe changing the semantics of refine (to be using+Module.new+refine) based on whether the receiver is a Class or Module is not acceptable.
So, we need a new name for this feature as refine would be too confusing with the existing semantics.

Changing the semantics for refine for all cases would be consistent, but introduce too large incompatibilities.
So a new name seems the way forward, do you have a suggestion?

#11 [ruby-core:86561] Updated by kddeisz (Kevin Deisz) 6 months ago

Thanks Benoit. A couple of suggestions would be:

anonymous_refine
inline_refine
class_refine
refining
refine_class

Any of these would be fine I think - anonymous_refine gets across that it's creating an anonymous module that will be used. inline_refine is similar in that sense. class_refine may be confusing in that it's not going up to the singleton class and refining. refining is inline with Rails' concerning, but doesn't particularly convey special meaning. refine_class gets across that it's only refining a specific class, which I kind of like the best.

Would something like refine_class be amenable to the core team?

Also available in: Atom PDF