-
-
Notifications
You must be signed in to change notification settings - Fork 48
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
Use Site Variance and Implicit Interfaces for Generic Types. #123
Open
jemc
wants to merge
1
commit into
main
Choose a base branch
from
feature/variance-and-implicit-interfaces
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
- Feature Name: variance-and-implicit-interfaces | ||
- Start Date: 2018-04-19 | ||
- RFC PR: | ||
- Pony Issue: | ||
|
||
# Summary | ||
|
||
Provide sugar for structural typing (implicit interfaces) by allowing use sites of generic types to notate a type argument as covariant or contravariant, or omit it entirely. | ||
|
||
# Motivation | ||
|
||
Generic types are very useful, but they can also be cumbersome to use at times. | ||
|
||
Any time you wish to reference a generic type, you must also specify the type arguments, because all type parameters must be reified with type arguments during compilation, fully resolving the implementation of the type before code generation. However, there are times when you want to have a reference that can hold any possible reification of that generic type, or some specific subset of the set of all possible reifications. | ||
|
||
This goal can be accomplished with structural typing (interfaces), but writing the interfaces by hand creates needless repetition in the code, where every relevant method must have its signature declared again. This takes unnecessary time and effort, slows down the speed of authoring and refactoring code, and introduces more code to review, troubleshoot, and maintain. | ||
|
||
Ultimately, this is exactly the kind of non-creative task that the compiler would be much better at than humans are. As long as it's given the appropriate information, the compiler can automatically generate an implicit interface that contains all of the correct method signatures, based on how you invoked it at the use site. | ||
|
||
# Detailed Design | ||
|
||
This new feature would introduce three new ways of specifying a conceptual set of types as a type argument, resulting in an overall type that is an interface representing some subset of the methods of the generic type: | ||
|
||
- `C[+T]` - the subset of methods of `C` where the type argument `T` is used in a covariant position (for example, in the return type), or not at all. | ||
|
||
- `C[-T]` - the subset of methods of `C` where the type argument `T` is used in a contravariant position (for example, in an argument type), or not at all. | ||
|
||
- `C[_]` - the subset of methods of `C` where the omitted type argument is not used at all. | ||
|
||
To demonstrate these concepts, consider the following real-world example, taken from the `pony-resp` library, in which an `Elements` class provides a small wrapper for the `Array` class, meant to act as a read-only representation of nested lists of data received "over the wire": | ||
|
||
```pony | ||
type Data is (None | OK | Error | String | I64 | ElementsAny) | ||
|
||
primitive OK | ||
fun string(): String => "OK" | ||
|
||
class val Error | ||
let message: String | ||
new val create(message': String) => message = message' | ||
fun string(): String => "Error(" + message + ")" | ||
|
||
interface val ElementsAny | ||
fun string(): String | ||
fun size(): USize | ||
fun apply(i: USize): Data? | ||
fun values(): Iterator[Data] | ||
|
||
class val Elements[A: Data = Data] is ElementsAny | ||
embed array: Array[A] = array.create() | ||
fun ref push(elem: A) => array.push(elem) | ||
fun size(): USize => array.size() | ||
fun apply(i: USize): A? => array(i)? | ||
fun values(): Iterator[A] => array.values() | ||
fun string(): String => | ||
let buf = recover String end | ||
buf.push('[') | ||
for (idx, elem) in array.pairs() do | ||
if idx > 0 then buf.>push(';').push(' ') end | ||
buf.append(elem.string()) | ||
end | ||
buf.push(']') | ||
buf | ||
``` | ||
|
||
Note the `ElementsAny` interface that was defined here, and note that it is in the type union of `Data` (because `Elements` instances can be nested). Because this is a data structure that is only meant for reading after it has been filled, `ElementsAny` is defined using the subset of methods of `Elements` where the type parameter `A` (reified here as `Data`) is used in covariant positions (in return types). | ||
|
||
With the new syntax, this could have been defined more simply as: | ||
|
||
```pony | ||
type ElementsAny is Elements[+Data] | ||
``` | ||
|
||
This simple type alias would be equivalent to the interface: | ||
|
||
```pony | ||
interface val ElementsAny | ||
fun string(): String | ||
fun size(): USize | ||
fun apply(i: USize): Data? | ||
fun values(): Iterator[Data] | ||
``` | ||
|
||
If the use site had instead used the contravariant syntax (`Elements[-Data]`), a different set of methods would be included in the interface: | ||
|
||
```pony | ||
interface val ElementsAny | ||
fun string(): String | ||
fun size(): USize | ||
fun ref push(elem: Data) | ||
``` | ||
|
||
Or, if the type argument had been omitted (`Elements[_]`), only the methods not using the `A` type parameter at all would remain: | ||
|
||
```pony | ||
interface val ElementsAny | ||
fun string(): String | ||
fun size(): USize | ||
``` | ||
|
||
You might guess that the type argument omission syntax may not be very useful in the real world, because it prevents you from using any methods where the type parameter is in the method signature. However, in the seven examples of this `XAny` pattern that I studied from the body of my own libraries, six of them would be satisfied using the type argument omission interface instead of covariant or contravariant interfaces. These are often cases where the type parameter is used to parameterize some implementation detail of the generic type, but the generic type still implements a set of useful methods where the type parameter doesn't come into play as part of the signature (that is, the signatures are identical in the general case). | ||
|
||
# How We Teach This | ||
|
||
The tutorial chapter on generics would need a new page to explain these concepts and give some practical examples of use. | ||
|
||
# How We Test This | ||
|
||
Add compiler sugar tests that verify the anonymous interface that was invoked into existence at the use site gets the proper set of methods in every case. | ||
|
||
# Drawbacks | ||
|
||
* Adds a new concept and new syntax to represent it in the language. However, I think the burden of learning new syntax is lessened by the following: | ||
* The `+`/`-` syntax to represent covariance and contravariance is fairly similar to its use in other languages, like Scala | ||
* The `_` syntax for omission should be quite intuitive, given that it's used in pattern matching and some other places as the "don't care" symbol. In this position it means "Give me the interface for when I don't care what the type argument was". | ||
|
||
* Adds some new complexity into the compiler to implement and maintain in the type system. | ||
|
||
# Alternatives | ||
|
||
- Create the anonymous interface as a trait instead, so that other types not mentioned at the use site won't end up as "accidentally" implementing the anonymous interace. | ||
|
||
- Move covariance and contravariance to be part of the type definition (as Scala does) instead of being part of the use site. This has generally been considered an unpopular option in previous discussions, due to the fact that it can be very cumbersome to use correctly, and often there is no single right answer for co/contra-variance to use in the definition of a type - many common generic types include type parameter uses in both covariant and contravariant positions. | ||
|
||
- Don't do this at all, leaving the user to always create explicit interfaces when they need them in their code, even when they are highly tedious and/or repetetive with respect to the class or other type they are referencing. | ||
|
||
# Unresolved questions | ||
|
||
- None. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Nit: this wouldn't actually be a trait, but some mix between the two (an interface which requires the class name to be the same).
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 thinking of it as an anonymous trait that was implicitly created, as well as being implicitly applied to your class, so that your class was the only type that was nominally linked to the trait.
I think this way of thinking about it is easier to understand than saying the the compiler checks the class name, but maybe others feel differently.