-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[DOC] Clarify Class#subclases behavior quirks #5480
[DOC] Clarify Class#subclases behavior quirks #5480
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I believe warning about this in the doc is useful, I wouldn't use the non-deterministic
terminology.
I think it would be better to refer those as weak reference.
I also think it would be better to frame it as how subclasses
don't prevent sublclasses from being GCed rather than to frame it as a cruft.
class.c
Outdated
* is not a representation of some internal state but is | ||
* calculated dynamically. It might lead to a non-deterministic |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is not a representation of some internal state but is calculated dynamically
It very much is a representation of some internal state. Class
has basically a weaklist of references to it's subclasses.
class.c
Outdated
@@ -1504,6 +1504,35 @@ class_descendants(VALUE klass, bool immediate_only) | |||
* A.subclasses #=> [D, B] | |||
* B.subclasses #=> [C] | |||
* C.subclasses #=> [] | |||
* | |||
* | |||
* Note that unlike Module#ancestors, this method's result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only real difference with ancestors
is that one is a strong reference, the other a weak reference. So yes, if you hold a module, it's ancestors won't ever be GCed, if you hold a class, it's sublclasses might be GCed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, that's from the point of view of who implements the language.
From the point of view of the programmer, it's not about strong/weak. From the point of view of the programmer, superclasses and mixins are added to the ancestor chain. There's no API to alter the ancestor chain, in particular for removing or replacing.
That said, the point of my feedback is not so much about whether there's an internal materialization of the collection or the collection is computed walking some internal structures. I don't think we need to word it this way.
The reality is that this method returns the "subclasses that are alive in memory", which is a weird way to say it. So wording this in terms of weak references may be worth a try.
The user has to know that the same exact program, without touching a comma, can behave in a different way, like the synthetic
Class.new
Object.subclasses
does.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reality is that this method returns the "subclasses that are alive in memory", which is a weird way to say it
Yes, all Ruby objects are either alive in memory, or either you can't interact with them, hence why I'm not kin on this kind of phrasing.
I think a simple reminder that subclasses may be GCed should be enough.
class.c
Outdated
* GC.start | ||
* | ||
* A.subclasses | ||
* # => [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can't be in the docs, it is not guaranteed.
So the current documentation is:
What about adding something along the line of:
|
I like it. It's concise and enough. |
@byroot @fxn I am a bit unsure about this wording, though:
For me, this has some vibe of a "note to self" (like, experienced Rubyists/language authors, "yeah, we didn't forget to mention it"). In my view, one of the important audiences of documentation is people new to language—maybe they are new to programming in general, maybe not; maybe they are new to dynamic/Garbage collected languages. From my memory of tinkering with Ruby many years ago, and from my experience of teaching Ruby for several years, I believe that "interestingly named" methods of core classes draw attention to those trying to make a mental model of the language, to understand how its entities are related and how they are behaving. And I suspect that (I believe it is part of what we discussed with @fxn on the tracker) for some readers presence of WDYT about this:
class A; end
# create dynamic subclass, not associated with a constant
c = Class.new(A)
A.subclasses
# => [#<Class:0x00007fe8cc5e3690>]
# drop the reference to subclass, it can be garbage-collected now
c = nil
A.subclasses
# It can be
# => [#<Class:0x00007fe8cc5e3690>]
# ...or
# => []
# ...depending on whether garbage collector was run WDYT? |
I'm not particularly attached to the specific wording, it's more about the overall point.
IMHO, I'm also worried that having 1ish line of description for 10ish line of what seem like a warning might put people off. Especially when it's something that few people will encounter. Active Support's implementation of Eventually mentioning that anonymous classes are returned too might be interesting. But ultimately I'm a fairly lousy documenter, so if you feel strongly about some part, go for it. |
Agree. Also, this is not the place to explain in detail what a class object is, and that they can go out of scope and be garbage collected like any other object. That's learning the Ruby language. So, I believe we need to say something to have a more complete description of what the method does, but there has to be a balance and the small print has to be short. The small print may make some readers wonder: "wait, a class can be GCed, how's that possible?", and that would make them go and learn the part of Ruby they did not know yet. But that has to be elsewhere in my opinion. |
@byroot @fxn TBH, I already feel bad about wasting so much of your time/resource on discussing a small paragraph in docs (I am saying it honestly, and not as a passive-agressive attack!) Therefore, I want to clarify my general intentions one more time (because it would be helpful for future work), and after that, I'll go with whatever the consensus will be. Or we can just close this PR, actually :) So, there are a few things I generally consider:
So... I think @byroot is onto something with "Eventually mentioning that anonymous classes are returned too might be interesting.", and I propose this form as a balance between "Mario, your details are in another castle!" and too much fine print, and a compilation of various versions proposed during the discussion. That might be the whole doc for the method:
This way the "fine print about disappearance" resides in the end (after a more commonly useful example of the dynamic subclass), and doesn't look like "the main thing that docs are saying". PS: @fxn you got me extremely confused 😂 It was from your initial concern that this effort started, but now we seem to "change places" in who considers what is important! |
No problem at all! Designing an API and writing good docs is not trivial at all. Your contribution and the discussion are necessary and totally welcome. You think as much about something as you need to come with a final result you like. And that final result is often the product of discussions, listening to different points of view, and sleeping on the topic. I am not sure about anonymous classes. A subclass is a parent/child relationship and no names are involved in that concept. Nor for the parent, nor for the child. In docs, sometimes you are a bit redundant for didactic purposes, though. The balance here, whether to mention this or not, I'd leave to @byroot. @zverok you example with
Sorry for the confusion! In Redmine, my position was that I believe this shouldn't be in Ruby, and I tried to explain why. And I say that from a pure API-design POV, in the sense that I have a huge respect for @byroot. It's just that in this particular case, we see it differently. Of course, this API was blessed by Ruby core, so all my respect there too, and opening the discussion was a way to say: "hey, have you considered this from this angle?" If the answer is, "yeah, it is fine", I accept it. Now, if the method has to stay, then I believe the original docs are incomplete. We need to be more precise. You can present it like happy path first, and then small print. But we need some small print because this is core Ruby. |
Followups: c = Class.new(A)
A.subclasses # => [#<Class:0x00007f003c77bd78>, D, B] this, in isolation, is not guaranteed. After line 1,
I didn't express myself very well here. All documentation should excel at being didactic, correct, and comprehensive. As a programmer, I have to get all what the method does described for me to be informed. I was trying to say that, if of all of them, there's a documentation that has to be this way, that's the one of the core language and its standard library (which definitely has room for improvement in this regard). |
No, you'd need to exit the current method or block or whatever the scope is. e.g.: def foo
c = Class.new(A)
1_000.times { GC.start } # GC won't ever collect `c` here.
end |
One thing is how CRuby works internally today, and a different thing is which is the public contract. What you can assure because Ruby, the language, guarantees in the abstract. When you document, you need to abide strictly to what the public contract is. In particular, GCs can change over the time, and different Rubies have different GCs. I don't think the garbage collector has any public contract. You don't even know what |
You being a generic you there :). |
Let me explain a bit more what I am saying. As a developer trying to squeeze the last drop of performance, you know the internals, you know how things work in some particular version of Ruby and program taking advantage of those undocumented things. Like, better intern this string, better do this this way or that way because while on paper they are the same, we know this thing performs better than the other today. And when you upgrade, maybe you tweak again for the particularities of the new one. But, as a documenter, all that has to be forgotten. You only have the public contract, and you have to carefully and consistently abide to the public contract. Ruby does not tell you when something is elegible for GC, or when GC runs, or what a GC run does. And Ruby could in theory be smart enough to understand the value is not used anymore anywhere and can be garbage collected right away. Nothing is telling you that does not happen. If you don't have guarantees as a public contract, you can't assume any particular behavior in the official documentation. |
I believe there is a great difference between documentation statements like "Foo will do this" or "Foo will fail if ...", and examples. Everywhere throughout the Ruby documentation examples are informal, relying on a vague feeling of "demonstrating the essence of the behavior". No, we can't guarantee the particular GC behavior (especially if we'll try to extend the guarantee to any implementation in any year in the future). As we can't guarantee the object id of some example object (and still show it in examples), or behavior of particular IO operation on some OS (and still demonstrate how I have a strong belief that most examples' goal is to "give an idea", not "show the formally verifiable and 100% reproducible demonstration of behavior, or show nothing". And I believe that "giving an idea" of dynamic subclass in |
No, no, the examples are public contract too.
That's a different level. In some examples, the outcome obviously depends on the individual execution and the reader understands that. If that is not obvious, the documentation has to make it explicit. In a case like c = Class.new(A)
A.subclasses # => [#<Class:0x00007f003c77bd78>, D, B] I don't think it is obvious that the class may or may not be there, and people could assume that is guaranteed. And that is my whole point about this method, you can't rely on it in the general case. Same program, different output. |
374b71b
to
ee0f046
Compare
@fxn I appreciate your choice of a hill to die on but I don't see it this way, honestly. ATM, I don't know what we can do here. @nobu @jeremyevans Maybe you can advise on the best way to proceed? Long story short: I am trying to update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current language in the pull request seems fine to me. I would remove the extra empty comment line added at the top, though.
I don't like that you put it in that way, honestly. One way to not mess with unespeficied behavior is to give it a side-kick, like the original patch did: class A; end
c = Class.new(A)
A.subclasses # => [#<Class:0x00007f003c77bd78>]
c = nil
A.subclasses # => [#<Class:0x00007f003c77bd78>] or [] because writing it like this, you know for sure what happens in line 4, and do not need to even mention the other case. |
@jeremyevans we were writing at the same time. If the current patch is good for you, I'm fine. |
@fxn Sorry, it was a joke of ill taste (I am Ukrainian which means I could be clumsy with English and also coming from a post-Soviet culture of a pretty adversary online community discussions, still trying to get rid of bad habits). |
@zverok one of my first comments was "I like it. It's concise and enough.". That was the end of it for me. Appreciate the clarification, in text, I could not see a joke in those words. |
@fxn All I tried to say (with badly chosen idiom!) is that I see your attention to a problem of whether invoking the assumption that dynamic subclass would be produced by |
ee0f046
to
274399d
Compare
Done. And commits squashed. |
It is public contract. APIs such This was discussed at length in many tickets, for a Ruby implementation to change this, it would have to get rid of several very important APIs. So no, it's not an implementation detail, it's the semantic of Ruby. |
Or maybe a smart analyzer could understand that those need the variable in scope, like However, this method is your baby, you are fine with it, and @jeremyevans is fine too. I am fine with it too then. |
My doubts about this method are conceptual, are about API design. To me, an API of this level should not need to depend on GC. This is a criteria that is not shared and I consider the discussion over in that sense. However, you seem to think "dynamic" classes are kind of rare, or exceptional, or something. I don't know what do you mean exactly by "dynamic" classes. All classes are created equal, either assigned to a constant, or a variable, or nothing. But when Rails reloads, there may be thousands and thousands of orphan classes in memory. Every reload, in every Rails application in the world, generates orphan subclasses. So the situation where non-reachable classes may show up non-deterministically is actually very common. Let me stress, though, that in the discussion I am not thinking about Rails or about Zeitwerk, just API design. Active Support has a similar API. But in my view Active Support may take some license. A Ruby core API has different demands of consistency and rigor. |
As per discussion in [Feature #18273], explain the non-deterministic nature of the method.
274399d
to
038b1b1
Compare
As per discussion in Feature #18273, explain the non-deterministic nature of the method.
Rendering of a version achieved after some discussion:
![image](https://user-images.githubusercontent.com/129656/151247556-f31f8b1b-167c-40b7-9eb3-1f1ec329a60d.png)