-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Passing generic arguments to template parameters #136
Conversation
Note that these options are somewhat independent. We could adopt none of these, | ||
or a combination of multiple options. | ||
|
||
### Adopting none of the below options |
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 would label this heading "Option 0: Forbid Calling Templates from Generics" as that really does describe this better than the phrase "don't do any of the below"
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 renamed the section, though I rephrased.
accepting `std::optional`, `std::unique_ptr`, and the like. | ||
|
||
As a consequence, we also consider more complex options where the usage is | ||
allowed. |
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 also make evolution harder. You cannot change a generic to a template without breaking all callers that are generic
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.
Added some text to try and capture this concern.
One concern with this approach is if we want to use a dynamic strategy for | ||
compiling generics that only generates one copy of the function. In this case, | ||
the dynamic type test will be left in the code at runtime, with the associated | ||
runtime costs. |
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 could also be annoying as it a generic could refuse to compile because there exists a specialization that you the author know will never be needed
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.
Yes, although in that case, shouldn't the generic explicitly declare T != Bool
as one of its constraints anyway? Otherwise the generic doesn't fulfill its own contract -- it says it works for any type T
, but it actually only works for types other than Bool
.
If you want compile-time duck typing, where you don't have to declare those constraints, I think that means you should be using a template 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 agree with mconst here, this is a necessary constraint to fulfill the generic contract.
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.
But then someone can add a template specialization to their template and break a generic at an arbitrary distance away
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 already the case that a change to a template, but particularly adding an overload with a different signature, can break callers of that template. It is true, though, that generic callers are a bit more fragile in this respect, since they can break because of cases that are not exercised in practice. I don't see an alternative other than option 2.
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. This does seem like a good argument against option 3A, because in that case even adding a template specialization that doesn't change the interface at all would become a breaking change!
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've added some text under "Recommendation: defer option 3" below that hopefully captures this concern.
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.
Thanks for writing this up! It looks good to me overall, and I think I agree with your evaluation of the three options.
- Cannot use templates even when the desired behavior would easily be achieved | ||
without using any part of the actual type parameters to the generic. The | ||
classic example here is using templates with pointers to generic type | ||
parameters that would work equally well with opaque pointers or `void` | ||
pointers. The specific value of the generic type parameter in this case is | ||
immaterial, but still blocks the usage because it is technically possible | ||
for the template to use it in some way -- even if it happens not to in most | ||
cases. |
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.
How important is this use case? In C++, templates like this are very common, but that's because C++ doesn't have generics. In Carbon, I was assuming this sort of code would normally be written with generics instead, so the issue wouldn't come up.
In fact, if we end up with a lot of templates like this in Carbon (where the actual type parameter isn't important most of the time, but it still can't be expressed as a generic for some reason), that kind of feels like a sign that the generics system isn't fulfilling its goals.
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.
A few points:
- We are starting from a large installed base of C++ code using templates that we are going to have to interop with to succeed. So this is important at the start before we can migrate code that can be generic to generics.
- Part of what it is being discussed here is that even when you have a template that does something special for some types, it will usually treat all pointer types the same.
- We don't know how big a concern this is, having both generics and templates is pretty new ground and, while I hope we can eventually migrate to mainly generic code, this depends a lot on how our users want to use Carbon.
This is some text I had copied from another doc without reviewing carefully. Are there some changes that would help clarify?
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.
Right, I certainly agree that interop with existing C++ templates is important. It feels like that's already covered by the second use case, though (lines 113-116).
For native Carbon templates, it's still not clear to me that we want to worry about this issue at all. The sort of examples you're talking about, where all pointer types are treated the same, seem like exactly the sort of things that ought to be handled with generics in Carbon code -- and if for some reason generics aren't sufficient, it feels like we should start by improving the generics system to handle those cases, rather than adding workarounds that make it easier to use templates instead. It's true that we can't know how people will end up using generics and templates, but that also means it's kind of premature to worry about usability features for use cases we don't even understand yet.
So it kind of feels like we could delete this bullet point entirely. Our motivation for the feature would just be improving interop with existing C++ templates, which is a nice clear goal with well-understood use cases.
Alternatively, if there are native Carbon use cases that are also important here, it would be nice to see some examples of those.
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 I was trying to make was that all the pointer types may be treated the same by a template even when other types are not.
Hmm, it occurs to me that some of these issues come up even with overloads that don't involve 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.
@chandlerc You were the source of this text originally, would you like to chime in here?
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.
In C++, templates like this are very common
I'm actually struggling to think of an example of such template. Most templates will use the type one way or another, even if it is to only know its size, or perform overload resolution.
One concern with this approach is if we want to use a dynamic strategy for | ||
compiling generics that only generates one copy of the function. In this case, | ||
the dynamic type test will be left in the code at runtime, with the associated | ||
runtime costs. |
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.
Yes, although in that case, shouldn't the generic explicitly declare T != Bool
as one of its constraints anyway? Otherwise the generic doesn't fulfill its own contract -- it says it works for any type T
, but it actually only works for types other than Bool
.
If you want compile-time duck typing, where you don't have to declare those constraints, I think that means you should be using a template instead.
One concern with this approach is if we want to use a dynamic strategy for | ||
compiling generics that only generates one copy of the function. In this case, | ||
the dynamic type test will be left in the code at runtime, with the associated | ||
runtime costs. |
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 approach also seems likely to cause slow compile times, which is one of the things we're trying to get away from with generics.
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.
Updated text to include this concern. I don't feel like I'm in a very good position to evaluate that concern myself, hopefully someone else can chime in.
- Requires writing modular descriptions of templates’ interfaces, potentially | ||
duplicating a large portion of an API already described in the template. | ||
- Requires instantiating transitive closure of templates at the root of any | ||
used generic. |
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.
These are just the instantiations that would happen if the generic were a template, right? That seems like a pretty natural consequence of calling a template.
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 will update the text to make this concern a bit clearer:
- Unlike for a template, this affects the signature of the generic -- it needs to mention any templates used. I give an example of that in the next section.
- It is transitive through generic calls in addition to templates.
@@ -0,0 +1,550 @@ | |||
# Passing generic arguments to template parameters |
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 feel like this title uses the words "arguments" and "parameters" backwards. Based on the problem description, I'd describe it like this:
"Passing expressions that depend on generic parameters as template arguments"
I realize it is a bit nitpicky, but it is important to be precise because generics discussions are often quite subtle.
A filename suggestion: "generic-uses-template.md".
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 was careful to be precise and I believe it is correct as is.
The problem is when you have a generic value for whatever reason (possibly but not necessarily because it depends on a generic parameter) that is used as an argument to a function that declares, in the position that argument is passed, that the parameter is a template.
the type is a generic value, is parameterized by a value only known generically, | ||
or as in the above example it could be a pointer-to-generic type. When `pointer` | ||
is passed to `TemplateFunction`, we need to assign some type to | ||
`TemplateParameter`. |
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.
`TemplateParameter`. | |
`TemplateParameter` in order to instantiate and type check the function template. |
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 using Chandler's suggested terminology where there are no "function templates", just parameterized functions where the parameters may be template or generic.
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 agree the terminology is awkward, I've started a discussion in #typesystem.
- Does not require any deferred work when compiling users of the generic, all | ||
type checking and code generation can be done in advance, as with all other | ||
generic code. | ||
- Does not require writing a modular description of templates’ interfaces. |
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.
Why doesn't it require writing such descriptions? In the example, isn't SomeInterface
such a modular description of requirements?
The only case where we don't need to write a description is when the template places no restrictions -- effectively, not using the type in any way (even for overload resolution).
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.
SomeInterface
is a description of the capabilities of the value we are passing to the template, not a description of the requirements of the template. This one thing could be reused for calls to multiple templates. In general this reflects a difference between options 1 and 2 -- option 2 needs work per template called in both function signatures and wrapper interfaces/impls.
such as `std::optional` or `std::variant` reusing bits. | ||
- May be a difficult model to teach as it affirms a distinction between the | ||
type within the generic and the type argument to the generic. | ||
- Does not enable use of C++ vocabulary types which are templates with generic |
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 wouldn't say that this option "does not enable" this use case. It enables this use case, but in a limited way -- the template instantiated on an archetype should better not escape the scope within which the archetype is defined, otherwise weirdness happens.
} | ||
``` | ||
|
||
This would likely introduce some performance penalty (assuming it wouldn't all |
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 performance penalty should be comparable to a compiler-based implementation.
|
||
``` | ||
// Assume `SomeInterface` has functions `F` and `G`. | ||
struct Archetype { |
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.
One way this implementation is different from the compiler-assisted one is how the archetype is typechecked.
In a compiler-assisted implementation, the archetype is based on the generic parameter. Therefore, we know that two objects that have archetype as their static type are wrapping two objects of the same exact actual type. That enables one to safely unwrap two archetype objects and pass the unwrapped objects to an operation that requires identical types on both sides, for example, ==
.
In a manual implementation, we can't assume this. Two instances of struct Archetype
can wrap objects of different types.
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com>
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com>
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com>
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com>
Co-authored-by: Dmitri Gribenko <gribozavr@gmail.com>
- Requires instantiating the transitive closure of templates at the root of | ||
any used generic. This affects the signature of the function parameterized | ||
by the generic, and transitively the signatures of any generic callers. | ||
|
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.
@dabrahams described to me a variant of this approach which makes a different trade off. In this variant, a statement by the user that "this template type conforms to this generic interface" is assumed to be true at type checking time for all types that satisfy the statement's constraints. This removes the second "con" -- there is no requirement to pass in a witness to the generic. It introduces another con in its place: we might get an error when we do instantiation/monomorphisation if the compiler discovers that for that particular parameter the template does not satisfy the generic interface. You will get this error even if it is due to some requirement of the interface that is not relied upon in the generic function.
This variant is described by dabrahams in this thread https://forums.swift.org/t/c-function-template-specialization-and-generic-functions-in-swift/42016/32 and was being considered as part of the C++/Swift interop investigation.
We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active, please comment or remove the |
We triage inactive PRs and issues in order to make it easier to find active work. If this PR should remain active or becomes active again, please reopen it. |
No description provided.