Project

General

Profile

Actions

Feature #20093

open

Syntax or keyword to reopen existing classs/modules, never to define new classs/modules

Added by tagomoris (Satoshi Tagomori) 4 months ago. Updated 4 months ago.

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

Description

class A and module B will reopen existing class A or module B to add/re-define methods if A/B exists. Otherwise, these will define the new class/module A/B.
But, in my opinion, the code of class A for patching existing classes doesn't work expectedly when A is not defined beforehand. It expects other codes to define A before being called.

For example:

# string_exclude.rb
class String
  def exclude?(string)
    !include?(string)
  end
end

This code expects that there is the String class, and it has the include? method. This code doesn't work if the file is loaded in the way below:

load('string_exclude.rb', true)

This code doesn't raise errors and will define an almost empty class (only with a method exclude? to raise NameError). It should be unexpected for every user.

So, I want to propose a new syntax to reopen the existing class/module or raise errors if the specified class/module is not defined.

class extension String
  def exclude?(string)
    !include?(string)
  end
end # adds #exclude? to String class

class extension Stroooong
  def exclude?(string)
    !include?(string)
  end
end # will raise NameError (or something else)

Some additional things:

  • class extension String (and module extension String) causes a compile error (SyntaxError) on Ruby 3.3. So we have space to add a keyword between class/module and the class/module name.
  • I don't have a strong opinion about the keyword name extension. An alternative idea is reopen.

Updated by kjtsanaktsidis (KJ Tsanaktsidis) 4 months ago

Would class extension just be syntax sugar for this:

String.class_eval do
    def exclude?(string) = !include?(string)
end

or would it behave differently in some way?

Updated by Dan0042 (Daniel DeLorme) 4 months ago

I think what you're looking for is class_eval / module_eval

String.class_eval do
  def exclude?(string)
    !include?(string)
  end
end

Updated by rubyFeedback (robert heiler) 4 months ago

Dan0042 wrote:

I think what you're looking for is class_eval / module_eval

I am not entirely certain tagomoris meant to use .class_eval here.

See his alternative suggestion of "reopen".

Of course I may be wrong, but if I understood it correctly his issue
request may be more related towards being able to extend something in
ruby no matter if it is a class or a module or already-defined versus
yet-to-be-defined; a bit like a "promise" of functionality, not unlike
refinements make isolated modifications to existing classes/code.

E. g.:

class extension String
class reopen String
reopen String # just as an example to drop the class/module name altogether and may be shorter

In particular in the last case, I have had use cases to want to avoid having to type either "class" or "module" altogether. I also agree with the use case of refinements, by the way, but
I always felt the syntax awkward. This is also one slight problem I have here, in that I think
"class foobar String" is too verbose - and it may also be a bit confusing for ruby users. But I
think tagomoris has a point as well - the intention should be for a ruby developer to "modify behaviour xyz". As stated, I may be mistaken, so perhaps tagomoris can refer to the .class_eval example. We should ideally come up with a useful syntax, though - being an elegant language is
one of ruby's strong points, even if what is elegant differs between individuals naturally.

Updated by tagomoris (Satoshi Tagomori) 4 months ago

Thank you for your feedback. Yes, class_eval / module_eval can do the same things functionally. class extension String could be a syntax sugar of String.class_eval do ... end.
I think it's reasonable to have such a syntax sugar because we're using class Foo; ...; end instead of (Foo = Class.new).class_eval do ... end.
I never want to recommend someone to use class_eval for monkey patching.

As pointed out above, I want to have the more simple keyword reopen or extend(oops, it's not good) for both class and module (e.g., reopen String). But adding top-level keywords is unrealistic. That's the reason I proposed a bit of verbose syntax.

Updated by Edwing123 (Edwin Garcia) 4 months ago

If this feature has the purpose of making class/module extension explicit (meaning you can clearly see that a class/module is being open again to be extended), then I agree with it.

By the way, I think the keyword open is more suitable:

class Foo
end

# Yay! Explicitly opening
# a class for extension.
open class Foo
 def bar()
 end
end

I like that we're being explicit, so with this when reading code, we can answer that the class/module is being open for extension and that its "canonical" definition is somewhere else.

Regarding backward compatibility:

  • Should the open keyword be optional in order for existing code to keep working?

If that's the case, then existing code that doesn't use open won't benefit from this:

Raise errors if the specified class/module is not defined.

So, only code using open will opt into checking if the class/module being open is already defined.

I believe this is fine, and this proposal opens the door to adding a feature that will make reading Ruby code more clearly. Eventually gems and applications can adopt the new keyword!

Updated by zverok (Victor Shepelev) 4 months ago

Just a side note: we actually have a term for "reopen existing class," and it is "refine."

So a bit of "turning the head around," this proposal might be rephrased as "default refinements" (ones that are always activated when some file is required), we might phrase it as just

# written at the top-level
refine String do
  def exclude?(string)
    !include?(string)
  end
end

Updated by Eregon (Benoit Daloze) 4 months ago

tagomoris (Satoshi Tagomori) wrote in #note-4:

I never want to recommend someone to use class_eval for monkey patching.

Why not?
For clarity, I mean class_eval with a block so there are no syntax highlighting hurdles and no extra runtime parsing.
It is the existing way to be explicit this is reopening an existing class, and it fails clearly if the class was not defined before.
The only downside I see is that doesn't make it easy to define and access constants defined under that class/module.
A possibility in that case is to use class_eval(String).

I guess the motivation for this proposal is #19744?

Updated by matheusrich (Matheus Richard) 4 months ago

Why a new keyword? If it behaves like a class_eval, could it be an alias?

String.reopen do
  def stuff = here
end

Updated by tagomoris (Satoshi Tagomori) 4 months ago

I commented that "class_eval / module_eval can do the same things functionally.". However, as @Eregon (Benoit Daloze) pointed out, there is a different rule about constants around class_eval / module_eval.
The method definition in class_eval/module_eval cannot refer to the class/module constants, and the block of class_eval/module_eval block cannot add/update the class/module constants. So, guiding people to use class_eval / module_eval instead of class/module for monkey patching should cause trouble and confusion.
For the same reason, @matheusrich (Matheus Richard) 's idea (Module#reopen method) is not ideal in my opinion. Adding methods instead of a new keyword should be much easier, but it's not what I want.

The idea of top-level keywords, @Edwing123 (Edwin Garcia) 's open class and @zverok (Victor Shepelev) 's refine look very hard to implement properly to me. open class conflicts with the existing Kernel#open. If refine is a method to take a block argument, it has the same problem with class_eval. If refine is a new top-level keyword (without following do), it will conflict with the Module#refine (Ruby core team members may be able to find any solution to avoid the conflict, though).

So, My idea is still to have a new keyword between class/module and the name of class/module: class extension String.

@Eregon (Benoit Daloze) It's true that this ticket's idea came up in an offline discussion about namespaces, but I thought this idea has its own value to be proposed as an independent feature, and I think it's (partially, at least) true because some people are making comments to agree with the intention.

Updated by kjtsanaktsidis (KJ Tsanaktsidis) 4 months ago

The only downside I see is that doesn't make it easy to define and access constants defined under that class/module.
A possibility in that case is to use class_eval(String)

I actually found this quite surprising:

irb(main):008> String.class_eval { Module.nesting }
=> []
irb(main):009> String.class_eval "Module.nesting"
=> [String]

I get why the first one is empty (we're not inside a class String ... end block), but I find it surprising that the string form returns String.

Actions

Also available in: Atom PDF

Like1
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0