Feature #20093
openSyntax or keyword to reopen existing classs/modules, never to define new classs/modules
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
(andmodule 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 isreopen
.
Updated by kjtsanaktsidis (KJ Tsanaktsidis) 11 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) 11 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) 11 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) 11 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 Anonymous 11 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) 11 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) 11 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) 11 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) 11 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 '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) 11 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
.