Skip to content
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

Closed
wants to merge 12 commits into from
Closed

Passing generic arguments to template parameters #136

wants to merge 12 commits into from

Conversation

josh11b
Copy link
Contributor

@josh11b josh11b commented Aug 4, 2020

No description provided.

@josh11b josh11b added WIP proposal A proposal labels Aug 4, 2020
@googlebot googlebot added the cla: yes PR meets CLA requirements according to bot. label Aug 4, 2020
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
Copy link

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"

Copy link
Contributor Author

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.
Copy link

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

Copy link
Contributor Author

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.
Copy link

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

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link

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

Copy link
Contributor Author

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.

Copy link
Contributor

@mconst mconst Aug 5, 2020

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!

Copy link
Contributor Author

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.

Copy link
Contributor

@mconst mconst left a 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.

Comment on lines +105 to +112
- 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.
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.
Copy link
Contributor

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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.
Copy link
Contributor

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.

Copy link
Contributor Author

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:

  1. 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.
  2. It is transitive through generic calls in addition to templates.

@@ -0,0 +1,550 @@
# Passing generic arguments to template parameters
Copy link
Contributor

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".

Copy link
Contributor Author

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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`TemplateParameter`.
`TemplateParameter` in order to instantiate and type check the function template.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

docs/design/generics/generic-to-template.md Outdated Show resolved Hide resolved
docs/design/generics/generic-to-template.md Outdated Show resolved Hide resolved
docs/design/generics/generic-to-template.md Outdated Show resolved Hide resolved
- 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.
Copy link
Contributor

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).

Copy link
Contributor Author

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.

docs/design/generics/generic-to-template.md Outdated Show resolved Hide resolved
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
Copy link
Contributor

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
Copy link
Contributor

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 {
Copy link
Contributor

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.

josh11b and others added 6 commits August 11, 2020 11:13
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.

Copy link
Contributor Author

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.

@jonmeow jonmeow marked this pull request as draft April 20, 2021 16:19
@jonmeow jonmeow removed the WIP label Apr 20, 2021
@github-actions
Copy link

github-actions bot commented Aug 1, 2021

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 inactive label.
This PR is labeled inactive because the last activity was over 90 days ago. This PR will be closed and archived after 14 additional days without activity.

@github-actions github-actions bot added the inactive Issues and PRs which have been inactive for at least 90 days. label Aug 1, 2021
@github-actions
Copy link

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.
This PR was closed and archived because there has been no new activity in the 14 days since the inactive label was added.

@github-actions github-actions bot closed this Aug 16, 2021
@github-actions github-actions bot added the proposal deferred Decision made, proposal deferred label Jul 25, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes PR meets CLA requirements according to bot. inactive Issues and PRs which have been inactive for at least 90 days. proposal deferred Decision made, proposal deferred proposal A proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants