[Pitch] Light-weight same-type constraint syntax

Introduction

As a step toward the goal of improving the UI of generics outlined in Improving the UI of generics, we’d like to propose a couple of improvements that bridge the syntactic gap between protocols and generic types, and hide some of the complexity (both visual and cognitive) of writing same-type constraints on associated types and type parameters of generic types.

Motivation

Consider a global function concatenate that operates on two arrays of String:

func concatenate(_ lhs: Array<String>, _ rhs: Array<String>) -> Array<String> {
   ...
}

On a path to generalize such a function, a reasonable next step could be to declare it on Array itself via an extension and remove the left-hand side argument, because it could be supplied by the instance of an array it was called on:

extension Array {
  func concatenate(with: Array<String>) -> Array<String> {
     ...
  }
}  

As written, concatenate becomes available on all arrays, which is not what we really want. We want this new method to be available only if the element type of an array is String. Currently, the way to express that is via where clause on the extension:

extension Array where Element == String {
  func concatenate(with: Array<String>) -> Array<String> {
     ...
  }
}

Using a where clause is a different spelling from use-site where array of strings is expressed simply via Array<String>, or [String] to be concise. Aside from different spelling, the where clause forces the developer to lookup the name of a generic parameter expressed by angle brackets. That is made worse for more complex types, where there are more than one generic parameter that has to be constrained and the where clause itself could become quite large.

This problem is also exacerbated the more generic concatenate gets. Let’s try to make it so concatenate operates on a Collection instead of an Array:

extension Collection where ??? == String {
  func concatenate(with: Collection<String>) -> Collection<String> {
    ...
  }
}

There are a couple of issues here. First, we need to figure out what type to constrain to String and secondly, the language doesn’t allow Collection<String> with following error message:

error: cannot specialize non-generic type 'Collection'

First issue could be addressed by looking at the Collection protocol declaration and figuring out which of multiple associated types could be used as a viable constraint. But second issue requires a complete re-design of the concatenate declaration and learning multiple new things about protocols including type- and value-level abstraction, existential types, constraining associated types with where clauses, etc. This proposal aims to simplify same-type constraints on associated types.

Proposed Solution

We’d like to propose two enhancements (one building on the other) to the language to help with progressive disclosure for “generalization” refactorings and improve language ergonomics around generics in general.

Let’s start with associated types. Protocols can have one or more associated types, they serve a role similar to that of generic parameters in generic types such as structs and classes. In many day-to-day development situations, it’s not necessary to know exact semantics and differences between associated types and generic parameters; that understanding could be acquired gradually. In our previous example with Collection<String> and Array<String>, the angle-brackets should intuitively mean the same thing - a collection containing a number of elements of type String.

To accommodate this intuition, we propose to allow protocols to express primary associated type(s) in the same way they are declared in a generic type:

protocol Collection<Element> {
   associatedtype Index: Comparable
   
   ...
}

This is syntactic sugar for:

protocol Collection {
   associatedtype Element
   associatedtype Index: Comparable
   
   ...
}

Building on that new capability of protocols, we propose a new light-weight way to declare constrained extensions that matches syntax used in function and other declarations involving generic types:

extension Array<String> {
   ...
}

extension Collection<String> {
   ...
}

Is syntactic sugar for declarations with where clause:

extension Array where Element == String {
   ...
}

extension Collection where Element == String {
   ...
}

This new syntax can be used to simplify where clauses with same-type constraints more generally. Instead of having to write the same-type constraint in a where clause separately from the name of the protocol, use angle brackets hide all that complexity, which is exactly how classes are spelled today:

extension Array where Element: Collection<String> {
   ...
}

func action<T: Collection<String>>(_: T) {
   ...
}

Each of the above declarations have implicit constraints in the where clause, expanding where clause to be:

extension Array where Element: Collection, Element.Element == String {
   ...
}

func action<T>(_: T) where T: Collection, T.Element == String {
   ...
}

Detailed design

Under this proposals, protocols may declare one or more primary associated type in angle brackets at the protocol declaration. Expressing same-type constraints on primary associated types in angle brackets is narrowly scoped to extension declarations and generic constraints in angle brackets and where clauses. This syntax cannot appear in any other position; existential types - func test(_: Collection<String>) → ... and opaque types - func test(...) -> some Collection<String> are not supported under this proposal, because that requires additional expressive power that does not exist in the language today. Please refer to the Future Directions section for more details.

Types in angle brackets are connected to the associated types via same-type (==) constraint, in accordance with the principle of the least surprise. In other words, this angle bracket syntax behaves the same as it does for generic types. If the concrete primary associated type specified is a class, this is also transformed into a same-type constraint, rather than an inheritance constraint.

When type-checker encounters a protocol declaration that uses new syntax, it would implicitly convert angle brackets into a number of associatedtype <Name> declarations preserving information that each converted name could be referred in angle brackets at the use-site. At the use site, the user may specify none of the primary associated types, or all of them in angle brackets.

For extension declarations, the type-checker would either add a new implicit where clause, or use the existing one to add the same-type requirements matching stated types with corresponding associated types preserving source information of the type references.

Given following protocol declaration:

protocol MyProtocol<Element, Index> {
  associatedtype Constraint
}

It would be possible to declare an extension that constrains both associated types via new syntax:

extension MyProtocol<String, Int> { ... }

It is a syntactic sugar for:

extension MyProtocol where Element == String, Index == Int { ... }

Existing constrained extensions could be converted into new syntax as well, for example:

extension MyProtocol where Element == String, 
                           Index == Int, 
                           Constraint: BinaryInteger { ... }  

Becomes a more concise:

extension MyProtocol<String, Int> where Constraint: BinaryInteger { ... }  

Another areas where new syntax aids with ergonomics are where clauses and generic constraints like this:

extension Array where Element: MyProtocol<String, Int> { ... }  

func doSomething<T: MyProtocol<String, Int>>(_: T) { ... }

New syntax supplements the conformance constraint with a number of == parameters so existing semantics are preserved. This aligns with the goal of UI ergonomics and bridges the syntactic gap between classes and protocols:

extension Array where Element: MyProtocol, 
                      Element.Element == String,
                      Element.Index == Int { ... } 
                      
func doSomething<T: MyProtocol>(_: T) where T.Element == String, 
                                            T.Index == String {
    ...
}

Primary associated types cannot be inherited, just like regular generic parameters, they have to be explicitly specified in protocol declaration, for example:

protocol MySubProtocol: MyProtocol {
}

Declaring MySubProtocol this way means that it has no primary associated types, although its parent has two (Element and Index), so attempting to declare following extension in invalid:

extension MySubProtocol<Int, Int> {
  ...
}

In order to make aforementioned extension valid, MySubProtocol would have to list of all of its primary associated types explicitly and provide their association with MyProtocol if necessary at the declaration site:

protocol MySubProtocol<MyElement, MyIndex>: MyProtocol<MyElement, MyIndex> {
   ...
}

This is a syntactic sugar for the following declaration:

protocol MySubProtocol: MyProtocol where MyProtocol.Element == MyElement,
                                         MyProtocol.Index == MyIndex {
   associatedtype MyElement
   associatedtype MyIndex
}

Alternatives considered

Annotate regular associatedtype declarations with primary

Adding some kind of modifier to associatedtype declaration shifts complexity to the users of an API because it’s still distinct from how generic types declare their parameters, which goes against the progressive disclosure principle, and, if there are multiple primary associated types, requires an understanding of ordering on the use-site.

Use the first declared associatedtype as the primary associated type.

This would make source order load bearing in a way that hasn’t been in the past, and would only support one associated type, which might not be sufficient for some APIs.

Require associated type names, e.g. Collection<.Element == String>

Explicitly writing associated type names to constrain them in angle brackets has a number of benefits:

  • Doesn’t require any special syntax at the protocol declaration.
  • Explicit associated type names allows constraining only a subset of the associated types.
  • The constraint syntax generalizes for all kinds of constraints e.g. <.Element: SomeProtocol>

There are also a number of drawbacks to this approach:

  • No visual clues at the protocol declaration about what associated types are useful.
  • The use-site may become onerous. For protocols with only one primary associated type, having to specify the name of it is unnecessarily repetitive.
  • This more verbose syntax is not as clear of an improvement over the existing syntax today, because most of the where clause is still explicitly written. This may also encourage users to specify most or all generic constraints in angle brackets at the front of a generic signature instead of in the where clause, which goes against SE-0081.

Source compatibility

This proposal has no impact on existing source compatibility. For protocols that adopt this feature, adding or reordering the primary associated types will be a source breaking change for clients.

Effect on ABI stability

This change does not impact ABI stability.

Effect on API resilience

This change does not impact API resilience. Lifting an existing associated type to be primary is a resilient change. Adding a completely new associated type has the same resilience impact as adding an ordinary associated type.

Future Directions

It would be useful to enable new syntax in more structural positions such as:

  • Existential types, e.g. func test(_: Collection<String>)
  • Opaque result types. The proposed syntax provides a natural evolution path for opaque types because it allows them to state associated type requirements without any cumbersome new syntax, such as a second generic signature, e.g. func evenValues<C: Collection>(in collection: C) -> <Output: Collection> Output where C.Element == Int, Output.Element == Int.

To further improve the experience of generalizing a concrete API, we could also allow some on parameter types to indicate an implicit type parameter conforming to the specified protocol. Combined with this proposal, it would allow the following signature (which isn’t even expressible in Swift today):

func evenValues<C: Collection>(in collection: C) -> <Output: Collection> Output where C.Element == Int, Output.Element == Int

to be simplified to:

func evenValues(in collection: some Collection<Int>) -> some Collection<Int>

Acknowledgments

Thank you to Joe Groff for writing out the original vision for improving generics ergonomics — which included the initial idea for this feature — and to Alejandro Alonso for implementing the lightweight same-type constraint syntax for extensions on generic types which prompted us to think about this feature again for protocols.

37 Likes

Overall, I love this. Thank you so much for pitching this and putting in the work to implement this. I believe this will be a massive QOL improvement to using protocols in Swift.

I have a couple of questions about how this works with protocol composition:

:one:

protocol A<T1> { }
protocol B: A { }

Can I do B<AType>? What about extension B<AType>?

:two:

protocol A<T1> { }
protocol B<T2> { }
protocol C: A, B { }

How would I declare a constrained C-type value? some C<T1, T2>? Something else? If it's C<T1, C2> does that now imply that changing the order of conformance is an ABI- and source-breaking change?

5 Likes

No, primary associated types are not inherited. To be able to write B<AType> and extension B<AType>, you would need to also declare a primary associated type in angle brackets at the declaration of B:

protocol B<T1>: A<T1> {}

I believe this also answers the second question: you need to explicitly declare primary associated types (and their order) on C, so reordering the protocol inheritance clause has no source or ABI impact.

Thanks for the question, we'll update the pitch to specify this behavior!

8 Likes

Updated pitch to reflect this. Thank you for a very good point, @davedelong!

While attractive, I'm a bit wary.

I had thought that many key folks (some even on the core team?) in earlier discussions had explicitly not used angle brackets for associated types because those brackets imply generic parameters, while these protocols are pointedly not generic--a point of confusion that has been visited at some length during even the original discussions about the Generics Manifesto.

As someone who subscribes to the philosophy that similar things should look similar and different things different, I am both curious as to what has changed in the interim to put this syntax back on the table and a little worried that the concerns regarding them are legitimate and haven't been addressed.

For example, the detailed design section of this pitch immediately details how the syntax cannot appear anywhere except extension declarations and generic constraints, speaking to how borrowing the syntax of generics naturally invites users to try to use the syntactic sugar like actual generic constraints in ways that we must forbid.

Moreover, I'm concerned as to whether this lightweight syntax makes the right thing lightweight. Take the example provided as motivation:

extension Collection where ??? == String {
  func concatenate(with: Collection<String>) -> Collection<String> {
    ...
  }
}

Do we want users to reach for this method signature by analogy with the Array<String> example? It may be sort of reasonable here with the semantics of this particular method, but in the general case it could be a bit of a trap, as the argument and return value here are of existential type and the dynamic type of the return value spelled in this way would be totally unrelated to either the argument's dynamic type or that of Self--which isn't the case for Array.

There's already a lot of difficulty for users in learning how to use generic constraints versus associated types, existentials versus opaque types, etc. I am not super confident that what's being made more lightweight in this pitch should be so light relative to the syntax of these other related features in trying to achieve the right balance that nudges users towards their most correct use.

31 Likes

I'm only going to address one question from your reply at the moment:

We just wanted to be explicit about the semantics here, use of this syntax could be extended to more places but we'd like this proposal to be more targeted since extending it to e.g. opaque result types would require implementing another features as pointed out in the same section, although it feels like a natural progression for it.

3 Likes

I completely agree with this point, and I think the fact that reaching for existential types is far easier than generics, especially given Swift's emphasis on the power of value types and static type safety, is a really important problem to solve. Even without the sugar in this pitch, this is already a problem because programmers often abstract away concrete types with existential types without fully realizing that they're erasing important static type information, and better solution might be to use a type parameter.

I've posted a discussion topic about exactly this, and I'd love to hear your thoughts:

4 Likes

I agree with @xwu - the difference between generic parameters and associated types is a fundamental thing. As nice as this syntax is, it makes me uncomfortable to mix the concepts this way.

The way I've always thought about it is that generic parameters are part of the type's identity - Array<Int> and Array<Bool> are entirely unrelated types in the Swift type system, despite both being Arrays.

Associated types are different - every conformer has its own version of an associated type, so they really define the way a type conforms to a protocol, rather than the protocol type itself. Collection<Int> and Collection<Bool> are not different protocols - there is just one Collection protocol, and you conform to it with one Element type.

In a constraint position, I think code which is generic over a protocol but cares very much about a particular associated type is rather rare. I think we would want people to try using broader constraints where possible, e.g. Numeric or StringProtocol over same-type Int and String constraints.

3 Likes

I think this is a valid point. The reason why this new syntax is proposed is purely pragmatic, it accounts for the fact that other languages don't have all the distinctions between protocols and generic types Swift does, so unification of the syntax here helps us to archive multiple goals at the same time - make it simpler/concise to work with protocols, unify syntax for handling of generic/type constraints, in combination with other possible features mentioned in [Discussion] Easing the learning curve for introducing generic parameters - #8 by hborla it makes the language more intuitive, and helps with progressive disclosure for people switching from other languages. Also since the syntax is the same, that helps the type-checker to understand what people are trying to express and provide useful suggestions.

3 Likes

I'm fine with tackling the extension and user-site part of the issues, but I'm strongly against misusing the generic parameterization on protocols declaration just for associated types. This simply kills off any potential future for generic protocols (an expert feature) and to me this feels like an "shut up already" move (please don't take that offensively).

As for the call-site, I still prefer the some Collection<.Element == String> spelling, because it leaves me with more control over the type of constraints I want to create. I still can write some Collection<.Element == Foo, .Index == Int>, while without an explicit associated type reference, not only would I be forced to use every previous generic parameter until I reached the one for the Index associated type, but I will need to memorize the exact order of the (inherited) associated types from that protocol which would just increase the cognitive load on me.

The order of associated types never mattered, so why would we want to make that a thing, especially in combination with some sugar code?!

I strongly think that any Collection<.Element == String, .Index == Int> should be the same existential as any Collection<.Index == Int, .Element == String>

Yes you have to type a bit more, but we’re not trying to eliminate every bit of unwanted characters here aren’t we? We want to unlock a new constraining feature using protocols, existentials and opaque types.

protocol Foo {
  associatedtype A
  associatedtype B 
}

protocol Bar: Foo {
  associatedtype C
}

_: any Bar<Int> // what is the first parameter here? is it `C` or `A`?
// if it was `A` then writing an existential that constrains `C` will be a bit painful and a hit or miss
_: any Bar<_, _, Int>

// On the flip side, the referenced associated type has non of these issues, 
// except being a bit more verbose
_: any Bar<.A == Int> // okay
_: any Bar<.C == Int> // okay
_: any Bar<.C == Int, .A == Int> // order does not matter

While this discussion focussed is only on same-type constraint. Can I ask why we shouldn't ever explore a sub-type constraint on the use-site?

Bikeshedding code:

// It's not important what concrete type `.Element` would have,
// it's only important that it conforms to `Foo`
_: some Collection<.Element: Foo, .Index == Int>

Something like this would be completely non-expressible with the syntax as pitched.

8 Likes

We have provided some pro/cons analysis regarding use of . syntax in Alternatives Considered section. The primary reason why we didn’t pitch it instead is related to the incremental disclosure principle, we didn’t want to introduce a new variant of angle brackets spelling because the goal here is not make the language more expressive but instead to unify the concepts.

1 Like

No offense, I've read the pitched analysis but to me this proposal still tries to jump over too many hoops. If anything, the first step to unlock the same-type constraint should be received by using the where clause.

typealias AnyCollection<T> = Collection where Self.Element == T

Only then we should even consider starting to think about sugar code. This approach would be truly and safely additive to the language. In fact, it's also where the Collection<.Element == T> syntax came from, as it signals exactly why the explicit reference to the associated type is needed on the call-site.

So far, from my personal point of view, the proposed sugar in the current state of the pitch would not improve anything except unlocking the ability to write the constraint, but the use-site will be extremely limited as not only does it seem to increase the cognitive load on me but it also will prevent future extension of general language features.

That said, in my opinion, the ability to write a few less characters and not introducing any new spelling has too many tradeoffs in the long term, which is simply not worth it.

6 Likes

What are the trade-offs you are talking about besides not being able to spell generic protocols?

I'm going to continue using the non-existing any keyword in context of explicit protocol as types (existentials), just for the sake of clarity in the example code.

If we'd force primary associated types, I think the call-site would become too restricted and the declaration-site to be too opinionated during the protocol design phase. I see no reason for having different types of associated types (primary vs. non-primary). I as a protocol user may want to constraint any of the associated types, and I would prefer to have that ability equally exposed through whatever syntax will be the final. As mentioned before, associated types have no specific order, but by mimicking a sub-set of associated types (primary associated types) as generic type parameters would enforce just that.

// today
protocol Foo {
  associatedtype A
  associatedtype B 
}

// what I would expect
typealias AnyFoo_1<A> = Foo where Self.A == A
typealias AnyFoo_2<B> = Foo where Self.B == B
typealias AnyFoo_3<A, B> = Foo where Self.A == A, Self.B == B

// sugared 
any Foo<.A == SomeA>
any Foo<.B == SomeB>
any Foo<.A == SomeA, .B == SomeB>
// the order remains irrelevant
any Foo<.B == SomeB, .A == SomeA>

// ==============
// what you propose

// we're forced to decide which associated type
// is a primary one and which is not
protocol Foo_1<A> {
  associatedtype B 
}

protocol Foo_2<B> {
  associatedtype A 
}

protocol Foo_3<A, B> {}

// the issue that I see here
any Foo_1<SomeA> // cannot constrain B
any Foo_2<SomeB> // cannot constrain A
// forced to use placeholder type if I'm
// not interested in constraining A
any Foo_3<_, B> 

That said, I'd rather type a few more characters but keep the ability to be able to express the exact constraint I need when and where I need it.

If the constrained type is still too long, I can shorten it by using a typealias.

typealias AnyFoo<A, B> = any Foo<.A == A, .B == B>

If we're fine with some more bike shedding. The sugar code that I personally might be okay with, could look something like this:

// explicit leading dot to indicate that those are still 
// associated types and not true generic type parameters
// on the protocol
protocol Foo<.A, .B> {}

// desugared
protocol $Foo {
  associatedtype A
  associatedtype B
}
typealias Foo<A, B> = $Foo<.A == A, .B == B>

So if I needed to regain the flexibility I need, I can opt-out and refer to the $Foo protocol directly.

The prefix $ was randomly picked and is considered as bikeshedding. Feel free to expand on this idea if you want.

6 Likes

This got me a bit off guard, but it actually makes sense.

I'm not too happy that this adds another concept (primary associatedtypes) but in a way (which is what the proposal states) it's actually putting this concept first and leaving the rest for more experienced devs, which is cool.

I'd just like to have a way to extend on a subset of primary constraints, like

protocol MyProtocol<Element, Index> {
  associatedtype Constraint
}

extension MyProtocol<Int, _> { ... }

_ has been used in the generics context as a placeholder to be inferred by the compiler, but I think the semantics fits with the pattern matching catch-all

(note: I know this can be still done with where clauses)

I expect many people will be cheerful when they see examples like Collection<Int>, imho this will be followed by a huge disappointment: You still won't be able to have an Array<Collection<Int>> (will you?), and that kind of issue is probably the most annoying limitation of PATs.
There is already quite a lot confusion caused by the fact that protocols and "protocols with associated types" are actually two different kinds of beast, and I'm convinced that mixing in generics syntax as well would make the situation worse:
There would not only be two kinds of entity with identical spelling, but also two spellings for a single concept — and one of those already has a slightly different meaning!

There are other very interesting possibilities of what Protocol<T> could mean, so I don't think it's a good idea to settle with the pitched interpretation now: Topics like nesting protocols in other types and named generic parameters should be decided before starting to work on an implementation for the pitch.

12 Likes

I was initially pleased with the shorter syntax, but I don’t think this is a good goal because the concepts really are very different. In particular, any two mentions of Array<Int> refer to the same type, while any two mentions of Collection<Int> don’t; this will lead to all sorts of confusion, including the disappointments @Tino described. (And if we do make “array of constrained existential” a possible type, we definitely don’t want it to be overly convenient!)

I think the proposed change would achieve its goal of making it easier to step into certain kinds of generic programming, but at the cost of making it harder to advance beyond that point because it’s harder to see when new concepts are being introduced. On top of that, extending the Collection<Int> syntax to declarations would make it even easier to reach for existentials, and not doing so would leave a confusing and aggravating inconsistency in the language.


On the other hand, I don’t buy into this:

Collection.Element is clearly more prominent than Collection.Index, and this extends to all sorts of functors/monads/containery types that have type-level extension points in addition to their primary content type. I would welcome syntax that recognizes this distinction but doesn’t fully conflate associated types with generic parameters.

2 Likes

I agree that there are cases where certain associated types are "more important" than others — but I don't think that there is a clear distinction in general. This might even shift in the lifetime of a library, so it's another tough decision for authors to make on behalf of their users.

Even when it's obvious that some associated types are not primary, I don't think an annotation has value on its own; actually, I'd say it's also not necessary to have special documentation for this, because users will realise what they need anyway.

In general, I'm in favour of making generic parameters more like function parameters: Allowing default values, and first and foremost, don't force us to hide their names (Array<Element: Int>).
If the latter would be possible, I don't think adding a new spelling for associated types would make sense at all, because people would be used to parameter names which would resolve any ambiguity.

Even for the obvious example of Collection, you might not be interested in Collection<Element: Int>, but rather Collection<Index: Int> (imagine you want every index divisible by a certain number, or with some other distinct features). Afaics, that wouldn't be possible with the pitched syntax (at least it would be inconvenient), so even if it's decided to "burn" the spelling Protocol<T>, the pitched way of declaring protocols would be seriously hampered.

1 Like

A big +1 on anything that @Karl and @DevAndArtist already wrote int this discussion, but most importantly (emphasis mine):

2 Likes

Generic Protocols

Generic protocols are a major gap in Swift's current generics and opportunistically giving them up in favor of a syntactic sugar for a completely different feature that could be expressed syntactically at a minimal cost using existing syntactic elements via <.T == …> would be a huge mistake, imho.

Unification

That's because the T of Array<T> and Array.Element are two completely different things serving two completely different purposes and roles in the language, which are strictly orthogonal to each other. Of course they are used and spelled differently. It would be bad if they weren't.

  • Generic arguments are passed in by the user of a generic type and provided at the time of its instantiation (i.e. Array<T> -> Array<Int>).
  • Associated types are returned out by the specific instantiation of a generic type at the time of its declaration (i.e. typealias = …).

If generic types/kinds were functions of the type system, mapping from a generic abstract type to a concrete type, then …

  • generic arguments would be the inputs of such a function.
  • associated types would be the outputs of such a function.

So not only would such a unification conflate two orthogonal concepts that cannot be used interchangeably into a single syntax, it also introduces a major inconsistency by doing so:

Every existing use of "instantiate a generic type" (i.e. pass a set of types as its input) in Swift (that I'm aware of) is using <T>.
Every existing use of "constrain a generic type" (i.e. retrieve a set of types as its output) in Swift (that I'm aware of) is using where T == _.


Just imagine for a second a hypothetical and admittedly silly proposal that wanted to unify the passing of inputs and the returning of outputs for a function into a single syntax and went something like this:

We propose that instead of passing values to a function via foo(42, "bar") and obtaining its output via let baz = foo(…) you can now do let foo(42, "bar", baz), where 42 and "bar" are the inputs to foo(…) and baz is its output (i.e. a local binding), resulting in a unified syntax.

A proposal like this would be shut down immediately, if not outright ridiculed, as it would be conflating two orthogonal concepts (inputs vs. outputs) into a shared syntax and effectively would make it impossible to know the semantics implied by (…) without looking up the function signature. Even though it effectively proposes the same thing proposed here: unify syntax of inputs and outputs of a function (on a kind), making them look the same, even though they are orthogonal. It's just that the semantics of "function of types" is familiar to most us, while the semantics of "function of kinds" is somewhat more obscure, yet rally not that different. What's bad in one context is similarly bad in the other.

7 Likes