-
Notifications
You must be signed in to change notification settings - Fork 50
Document the use of "base methods" to resolve hierarchy conflicts #392
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
base: main
Are you sure you want to change the base?
Conversation
This is *much* more realistic and better use of TQMIC:s than those I originally thought of, and the one pattern that have actually ended up being used. It's high time we document it proper. Thanks to Gustav for coming up with it!
8628465 to
e536d54
Compare
|
PR Verification: ✅ success |
|
I think the text is now very heavy. If this new case is the main TQMIC pattern that's used in practice, then I think we can throw out the current example text, and focus on a single hands-on example that is easy to consume. I would suggest starting off with your example code, without an abstract description of when the pattern is applicable. Something like this might be sufficient to present your example:
|
doc/1.4/language.md
Outdated
| // If even more conflicting templates could be in play, then one could | ||
| // consider converting write_1_clears to also leverage a base method | ||
| // instead of calling default and extend the call chain; on the other | ||
| // hand, the way write_1_clears manipulates the written value may violate | ||
| // the expectations of implementations later on in the chain, so it might | ||
| // make most sense to always place write_1_clears as the final link, and | ||
| // have all other templates leverage base methods instead. |
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.
I think the code snippet should explain itself only, no what-ifs. If this remark is needed, then we can place it after the snippet instead.
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 point is to touch upon the "thought is needed as to ordering" remark with a concrete example as to how it may matter.
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 is explained more briefly following the main example instead, now.
Your suggestion has merit. I'm slightly hesitant to discard the first case I document: if it's possible to leverage, then it is the simplest and best solution. Most notably, it's suitable for conflicts to |
Then maybe start with documenting case 2 end-to-end, and add a remark afterward that you sometimes can get away with a somewhat simpler/cleaner solution, and show the example from case 1. I don't think we need to describe the exact abstract circumstances when the pattern is applicable; this is tips-and-tricks rather than formal reference, and it should be easy for the reader to extrapolate from examples. |
That was exactly what I was going to suggest before my internet dropped out
Well. This is the DML reference manual. But I agree an elaborate description would be redundant. |
|
|
PR Verification: ❌ failure |
| > [!NOTE] | ||
| > The example given above applies regardless of whether the method | ||
| > implementations in the original templates are `shared` or not. However, the | ||
| > implementations in the template defined to resolve the conflicts might need to | ||
| > be non-`shared` if some of the implementations involved are not `shared`; | ||
| > see the final paragraph of this subsection. |
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 added value of this note is questionable -- I expect users would read it as "as usual, you sometimes need to add a shared annotation; the compiler will let you know". This is already true for all method declarations.
| This approach extends to any number of conflicting templates — as long as | ||
| all but (optionally) one can be made to offer overridable base methods (with | ||
| distinct names.) |
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.
It doesn't scale nicely though: with three templates A, B and C, you'd in principle need templates for A+B, A+C, B+C, and A+B+C, and with N templates the worst-case would be exponential, up to 2**N - N - 1 templates. There would also be a crazy amount of diamond expansion: the rank of the A+B+C template would need to beat that of A+B, A+C and B+C, which adds extra copies of the A/B/C vtables, again in an exponential manner (unless cough the #if(false) trick cough).
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.
Your point being? You want me to change anything?
unless cough the
#if(false)trick cough).
No, it doesn't help. The #if (false) trick only allows one to make one override more specific than a number of others. It doesn't help any in the question of combining the implementations.
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.
Regarding if false: If we have
template abc is (a, b, c) { method m() default { .. } }
template ab is (a, b) { method m() default { .. } }
template ac is (a, c) { method m() default { .. } }
template bc is (b, c) { method m() default { .. } }
group g is (a,b,c);
and apply in-each, then all four templates will be applied into g, which gives an ambiguity on g. You can use the abominable trick to make abc win; without it you need to spell out template abc is (ab, ac, bc) which explodes worse (although admittedly the exponential punishment only applies with 4 or more templates).
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.
Your point being? You want me to change anything?
Yes, your remark is incorrect, it only extends to N conflicting templates if you are ready to write O(2N) code and pay O(4N) memory. I'd say this is questionable for N==3, and unreasonable for N>3. So it's not honest to say "extends to any number of".
I don't know what to do about it; it feels like a genuine problem with this code pattern. Unless I'm missing something.
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.
It is honest -- at least, with the intention I had behind the statement. The point of it is only to clarify that the approach is not limited to two conflicting templates. I can, however, bring up the fact that is scales poorly:
This approach extends to any number of conflicting templates — as long as
all but (optionally) one can be made to offer overridable base methods (with
distinct names.) However, if one wishes to leverage the above approach to write code supporting arbitrary combinations across those templates, then the amount of code needed scales exponentially, becoming untenable past 4 possible conflicting templates being in play simultaneously. Because of that, in practice it is better to only apply the pattern for the specific conflicting combinations that prove necessary to support.
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.
Oh, it's not exponential, it's factorial. without #if (false), it's a simple N!; with #if (false) this is multiplied by sum(i/(factorial(N-i)*factorial(i)) for i in range(N)), which only pays off with N>=5, when the base vtables are multiplied by 80 instead of 120.
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.
Oh, it's not exponential, it's factorial.
Factorial is exponential though, if I remember my complexity theory correctly. As in they simplify to the same complexity class.
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.
I do not. I thought I learned factorial was equivalent to n^n, but not even that's true.
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.
If we want to summarize what we know in one sentence, then I would say "it is a pattern that scales poorly on conflicts that involve more than two templates". This is sufficient -- the message is that the pattern can NOT be extended without pain and extra thought. What probably matters the most is that you usually can find special-case circumstances that make multi-template conflicts solvable, and this is out of scope for this doc.
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.
@mandolaerik But only saying "it is a pattern that scales poorly on conflicts that involve more than two templates" is wrong. Or, at least, misleading. The pattern works well as a workaround for specific cases of conflicting combinations, even those involving more than two templates -- it doesn't work well as something you apply in order to take care of every possible situation as a just-in-case. Hence,
, if one wishes to leverage the above approach to write code supporting arbitrary combinations across those templates
and
Because of that, in practice it is better to only apply the pattern for the specific conflicting combinations that prove necessary to support.
You're saying "oh, people should find special-case circumstances" -- applying the pattern for only a particular combination is a special-case circumstance that works well.
| have the implementation introduced to resolve the conflict simply call each | ||
| conflicting implementation in turn — if that doesn't cause side-effects | ||
| to be duplicated in an undesirable way — or even only call one particular | ||
| implementation — if it makes sense to prefer it ahead of every other. |
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 triple mdash in one sentence doesn't look good. Maybe split the sentence and replace them with commas?
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.
Yeah. I'll give it a think, I had a particular reason to do it this way, but it's not exactly pretty.
| A template-qualified method implementation call is resolved by using | ||
| the method implementation provided to the object by the named template. | ||
| If no such implementation is provided (whether it be because the template does | ||
| not specify one, or specifies one which is not provided to the object due to its | ||
| If no such implementation exists (whether it be because the template does not | ||
| specify one, or specifies one which is not provided to the object due to its | ||
| definition being eliminated by an [`#if`](#conditional-objects)), then the | ||
| ancestor templates of the named template are recursively searched for the |
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.
Unexpected context switch: we were on example usage, and now go back to detailed reference docs. Maybe put the example last, perhaps even under a named subsection?
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 problem with that doing so is that it will bore and confuse the reader to tears before they gain any understanding of how TQMICs may actually be used.
Doing it this way is also not completely without precedence, see e.g. explicit_param_decls. Though admittedly, it is rare and the examples presented in other sections are smaller.
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.
I'm not sure what to do about this. A named subsection feels distasteful to me. Nothing else in the reference manual does it.
| on_write_attempted_when_not_allowed(); | ||
| } | ||
| method base_write_of_gated_write(uint64 val) default { | ||
| // This is the implementation that default() would've resolved to |
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.
would have
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.
Are we that stuffy? This feels like on the level of replacing every "don't" with "do not".
But bah, fine.
This is much more realistic and better use of TQMIC:s than those I originally thought of, and the one pattern that have actually ended up being used. It's high time we document it proper.
Thanks to Gustav for coming up with it! I've requested him as reviewer to double-check that what I've written is correct and also understandable.