Improving the UI of generics

Just reading this thread and thought I'd chime in on this. I've done a lot of C# programming over the last couple of years and the main conceptual difference between protocols in Swift and interfaces in C# is that in C# an interface is a reference type, effectively a specialised abstract class. So old concepts are re-used. "Conforming to an interface" = "inheriting the interface". "Interface as a type" vs "interface as a constraint" doesn't arise because the interface is essentially a class and its use as an "existential type" is just plain old inheritance.

I'm not saying things don't get more complicated under the hood or that the above holds perfectly when conforming structs to interfaces (I hardly ever use structs in C#) but this is how things are presented to the programmer and it works well. If you use an interface the way you would an abstract class you virtually never go wrong.

I think where Swift gets complicated by comparison is because protocols are a completely different thing compared to classes/structs and deviate from traditional ideas of inheritance (whilst retaining the same syntax). I'm sure there are reasons for this, just pointing out how it feels to the programmer.

Having said this, associated types in Swift protocols are fantastic compared to generic parameter types in C# interfaces. The amount of boilerplate in C# generics due to the absence of associated types is unbelievable. I wrote a comment in support of a proposal to add associated types to C# interfaces (terminology note: "existential types" means associated types in proposal). Personally I have come to the view that associated type and generic type parameters complement each other well and I am pretty confident the C# team will add associated types (including the Self type) to C# interfaces after they finish their work on static interface members.

6 Likes

That doesn't really clarify much in my mind. Interfaces in C# don't seem to me any more like inheritance than protocols in Swift. In C# you can only inherit from a single base class, but you can conform to multiple interfaces (even with overlapping methods). Calling C#'s interface semantics "inheritance" doesn't help me understand the distinction at all.

As far as I can tell the answer to "why was Swift designed this way instead" is "so that it can be better optimized", but I guess I'm still not convinced that was a worthwhile tradeoff. It just feels like at this point you need to be an expert in type systems to make use of Swift generics with ease, and I never felt that way when working with C#. For sure there were things I couldn't always express in C# generics that maybe I wished I could, but it seemed like the simple cases were simple, and in Swift I don't feel like that's true today.

So far in my career I've used C++ templates, C# generics, and now Swift generics. I remember when learning C++ templates the syntax sometimes was confusing, but overall it was relatively simple to understand how they worked and how to use them. For C# I remember feeling limited at times in what I could do (what kinds of constraints I could use), but otherwise it felt very straightforward. In Swift I often feel like I have no clue what I'm doing, and it feels like even simple things are just overly difficult. I don't feel more productive. I feel like "if I'm not writing a library for a large audience then maybe I just shouldn't even bother because it's probably not worth the effort". To me that just feels like a miss...

Even so, I think the recent proposals to allow existential to be used in more places will help a lot. Other proposals, though, just feel like they require so much more conceptual understanding to even get started, which, again, I never really felt when using C++ or C#.

If you're familiar with C# generics, then associated types are in most respects isomorphic to generic parameters on interfaces. If not for the limitations on existentials, which IMO have lasted for way too long, they can be used in pretty much all of the same situations. I would say that the design choice to use associated types was driven not so much by performance as by scalability and library evolution concerns; you should be able to add associated types to a protocol that aren't necessarily part of the primary interface, without needing to then specify that associated type every single place you use the protocol as a constraint, like you would with the generic parameters to a C# interface. For example, with a fully armed and operational Swift type system, you ought to be able to use Collection where .Element == Int, without having to also bind the Collection's generator, subsequence, index, and other accessory associated types, but Collection can still use those associated types to provide a stronger-typed relationship between those related parts of a collection implementation. Similarly, you can add associated types to an already-published protocol, with a default binding, and not break the API or ABI of existing clients of the protocol.

9 Likes

That sounds very promising!

That doesn't really clarify much in my mind. Interfaces in C# don't seem to me any more like inheritance than protocols in Swift.

Sorry not to be helpful. The main point I was trying to make is that in C# interfaces are reference types like classes (see here) whereas I have no clear mental model for how Swift implements protocols (are they like classes?). And C# ensures all intuition about reference types carries over so that "interface conformance" functions like a limited form of multiple inheritance. And when an interface is used as a type it functions just like a base class.

1 Like

As I have been working on an implementation of the syntax func foo() -> <T> T, one particular implication of that syntax has come to bother me quite a lot and I have not seen it discussed in this thread. The syntax func foo() -> <T> T means exists a. (() -> a) not () -> (exists a. a), as it would lead one to believe.

If you don't know Haskell it's okay because I'll give an explanation in plain English after the following code, but the difference between the former and the latter can be illustrated by the this Haskell code (credit to my friend William Brandon who I was discussing this thread with for the example):

data MPair where
    MPair :: forall a. (Show a, Monoid a) => a -> a -> MPair

foo :: Bool -> MPair
foo True = MPair 10 20
foo False = MPair "hello" " world"

main :: IO ()
main = do
    let MPair x1 y1 = foo True
    let MPair x2 y2 = foo False
    print (mconcat x1 y1)
    print (mconcat x2 y2)

The first thing to note is that the type of the output of foo depends on the value of the input. This is possible with bool -> (exists a. (a, a)) but not exists a. (bool -> (a, a)). So what is difference between bool -> (exists a. (a, a)) and protocol or class based subtyping? Well if we tried to use subtyping to achieve the same effect, we would end up with something like bool -> (exists a. a, exists b. b) i.e. we would not have the same type constraint on the two tuple elements and could therefore could not call mconcat.

Because of the above, I have a new syntax proposal: named opaque return types would be written like func foo<some T>() -> T where T.Elem == Int. If we mix opaque return types and generic parameters, we end up with something like func foo<T, some U>(_ t: T) -> U.

For parody with opaque return types func foo<T>() could be sugar for func foo<any T>(). In other words, I would like to use any as a keyword, but for an entirely different purpose than discussed in this thread. As an alternative to what any was used for in the above discussion I would propose dyn (like Rust) or dynamic if Swift prefers whole words too abbreviations. If we wanted a symmetric syntax to func foo() -> some P it would turn into func foo(_ t: any T) and not func foo(_ t: some T) as discussed above.

1 Like

I'm not sure I'm understanding your discussion about Haskell correctly, but I agree with this from another point. My concern is about use of some for generics, and previously held discussion here.

I think it makes much more sense to use any for generics, some for reverse generics, and use other keyword for existential types.

I have an idea that I quite like so far but I could be convinced otherwise - I would love to hear what people think about it. I know that the idea is inspired by many ideas of others that I've read here on the forum, but I don't remember seeing exactly this as I'm proposing it. However, it is also possible that without knowing it I'm proudly presenting someone else's idea as if it were my own.

The idea:

What if we expand the use of the generic <T: Constraint> syntax to be usable in every (or maybe almost every) situation where a normal type name can be used? The syntax would have the same meaning the new context as it does in its current usage, namely that the type in question will be chosen by the caller (subject to certain constraints).

Simplest example:

// Current syntax
func discard <Value> (_ value: Value)

// New syntax
func discard (_ value: <Value>)

The Value type is introduced at the same time as being used in the type signature.

The placeholder type names that are introduced in this way are accessible in the whole function signature and within the body of the function just like with the current generic syntax:

// Current syntax
func first <C: Collection> (of collection: C) -> C.Element

// New syntax
func first (of collection: <C: Collection>) -> C.Element

Any type names wrapped in angle brackets must be unique within the scope. This, for example, is an error:

func assign (_ newValue: <Value>, to destination: inout <Value>) // Error - invalid redeclaration of `Value`

Exactly one usage of Value must be wrapped in angle brackets, and everywhere else it is referenced by name like any other type. Generic constraints can be applied either within the angle brackets or by way of a where clause.

If this type declaration construct appears in the return type that does not mean that it is a reverse generic. It is still a regular generic type, in the sense that the caller chooses the return type.

All of these signatures are equivalent:

// Current syntax
func echo <Value> (_ value: Value) -> Value

// New syntax
func echo (_ value: Value) -> <Value>
func echo (_ value: <Value>) -> Value

The order in which the types are declared within the function signature doesn't matter, in the sense that the declared types can be referenced in earlier parameters:

// Old syntax
func feed <Recipient: Eater> (_ food: Recipient.Food, to recipient: Recipient) -> Recipient.FormOfThanks

// New syntax
func feed (_ food: Recipient.Food, to recipient: <Recipient: Eater>) -> Recipient.FormOfThanks

I find the reduction of angle-bracket-blindness in the first relative to the second fairly significant.

It seems reasonable to me to allow this syntax to be nested in a type expression:

// Old syntax
func dropLatterHalf <T> (of array: [T]) -> [T]

// New syntax
func dropLatterHalf (of array: [<T>]) -> [T]
func dropLatterHalf (of array: [T]) -> [<T>]

Given that <T> means a type that will be chosen by the caller, how do we interpret this?:

let foo: <T> = 7

This is effectively the same as this:

typealias T = Int
let foo = 7

in the sense that after using <T> as the type of foo we can then reference T for the rest of the scope:

let foo: <T> = 7
let maximumInteger = T.max // This is `Int.max`

(I can't quite put my finger on it at the moment but I have a feeling that something about this use-case that could prove extremely useful for writing and especially for maintaining unit tests).

If there is a constraint included in the type declaration then it is enforced at compilation time as always:

let a: <T: Numeric> = 1.4 // Ok
let b: <T: Numeric> = "string" // Error

let c: <T: Numeric>
switch something {
case .oneThing: c = 1.2
case .anotherThing: c = 1.9 // Ok - both are `Double`
}

let d: <T: Numeric>
switch something {
case .oneThing: d = 1.5
case .anotherThing: d = Int(7) // Error: mismatched types
}

This would allow computed properties to have generic return types:

var anyKindOfSevenYouWant: <T: ExpressibleByIntegerLiteral> {
    .init(integerLiteral: 7)
}

This syntax would naturally allow us to unwrap existentials. For example:

let existential: any Equatable = ...
let otherExistential: any Equatable = ...
let value: <T: Equatable> = existential
if let otherValue = otherExistential as? T {
    if value == otherValue {
        // Do something
    }
}

I'm thinking where clauses would be allowed on any declaration that contains a type placeholder declaration:

let existential: any Equatable = ...
let value: <T> = existential where T: Equatable

I suppose that in many cases the generic constraint on the type declaration can be implicit:

let existential: any Equatable = ...
let value: <T> = existential // T is known to conform to `Equatable`

Here's another thought (and this one's a little bit out there) - could using one of these within the type declaration of a stored property of a type be interpreted as a new generic parameter of the type?

struct Queue {

    private(set) var elements: [<Element>]
}

would be equal to:

struct Queue <Element> {

    private(set) var elements: [Element]
}

it could also be done like this:

struct Queue {

    private var _privateDictBecauseWhoKnowsWhy: [Int: <Element>]

    var elements: [Element] {
        Array(_privateDictBecauseWhoKnowsWhy.values)
    }
}

Either way, the Queue type would be usable as a normal generic type (e.g., Queue<Int>).

The result type could then be defined:

enum Result {
    case success (<Success>)
    case failure (<Failure: Error>)
}

I suppose the proper order of generic type parameters for a type could be determined simply by the order in which they appear in the type declaration.
This has the order A then B:

struct Foo {
    var a: <A>
    var b: <B: Collection>
}

let _: Foo<Int, Array<Bool>> // Ok
let _: Foo<Array<Bool>, Int> // Error, the Collection must come second

Lastly, perhaps this would also be the right syntax for extending any type (if that's actually a good idea in the first place):

extension <T> {
    func somethingGenericallyUseful () -> Self
}
3 Likes

Unfortunately, I didn't have enough time during my summer internship to finish implementing this. The relevant posts about what I did have time to get to: evolution proposal, future work.

4 Likes

While I understand the term, I find the syntax daunting and the concept unnecessary, since we already have opaque return types. Couldn't the above just as well be written

func evenValues<C: Collection, Output: some Collection>(in collection: C) -> Output
  where C.Element == Int, Output.Element == Int
{
  return collection.lazy.filter { $0 % 2 == 0 }
}

? Am I missing something? (apologies if the idea was covered in this thread already; I couldn't find it).

1 Like

Opaque return types, the strawman syntax from the OP, and your proposed syntax are all different syntax for the same underlying concept—there's an opaque generic parameter that's being bound by the callee rather than being passed in from the caller.

6 Likes

That's what I thought, thanks. I didn't understand that this was a strawman syntax and not part of your proposal. The bullet in the “Moving forward” section seems to suggest that it is, so I'm a little confused.

Also are you not concerned that the shorthand syntax proposed involves adding more angle brackets? I'm not necessarily opposed to them myself, but they do have kind of a bad rap. I'm a little worried people will feel caught between long where clauses and more angle brackets.

More than that, I worry that the overall complexity of the language will suffer from having many ways to write the same thing. I just came back from a C++ conference, and boy if C++ complexity was a problem when I started working on Swift, it's pretty out of hand now. I one of the things I said to the whole group was, “Swift got a lot of mileage out of saying ‘no’ to stuff.” Let's not lose sight of that, please.

15 Likes

This is a quick and sketchy attempt to ditch angle brackets altogether:

    // current syntax:
    func foo<Apple> (x: Apple, y: Apple) -> [Apple]
    // proposed equivalent syntax:
    func foo(x: generic Apple, y: Apple) -> [Apple]

    // note that "generic" is specified only for the first occurrence
    // of Apple to reduce noise
    
    // current syntax:
    func foo<Apple: Comparable> (x: Apple, y: Apple) -> [Apple]
    // proposed equivalent syntax:
    func foo(x: Comparable Apple, y: Apple) -> [Apple]
    // note that `generic` is implied here to reduce noise
    // also Comparable is specified only once
    
    // current syntax:
    func foo<Apple: Comparable, Orange: Comparable,
         Banana: Comparable>(a1: Apple, a2: Apple, o: Orange) -> [Banana]
    // proposed equivalent syntax:
    func foo(a1: Comparable Apple, a2: Apple,
             o: Comparable Orange) -> [Comparable Banana]
    
    // for existentials:
    // current syntax:
    func foo(x: Comparable, y: Comparable) -> [Comparable]
    // proposed equivalent syntax:
    func foo(x: any Comparable, y: any Comparable) -> [any Comparable]

    // if more than one protocol conformance needed then:
    func foo(x: Comparable & Hashable Apple, ...) // generic
    func foo(x: any Comparable & Hashable, ...) // existential

to sum up:

  • "generic Apple" would mean generic unconstraint type
  • "Comparable Apple" would mean generic type conforming to a protocol (Comparable in this case)
  • constraints specified only for the first occurrence of a type
  • "any Comparable" would mean existential
  • just "Comparable" could be a warning (or an error in some future versions).

Wonder if this direction is worth exploring for swift.

This is interesting.

I like this in part because it opens up a natural way to name an opaque type too without using angle brackets (example: func f(generic Orange) -> opaque Apple where Orange: Sequence, Apple: Sequence, Orange.Element == Apple.Element).

I don’t think this part’s necessary, because your first bullet point already composes nicely with where (example: func f(generic Apple) where Apple: Comparable), and for even more concise writing we’d have the already suggested func f(some Comparable). Moreover, eliding generic would mean that the same syntax could not be extended to opaque types, and not eliding generic would be clunky. Overall, I don’t see a rationale for introducing twice as much syntax when there’s already so many alternatives both more concise and more verbose.

Overall, the main barrier I can see is that this a lot of churn for the syntax of Swift, but I think your first bullet point is workable.

Thank you.

Just occurred to me that within these three pairs the first and second lines are (or should be) treated absolutely equivalent today in the current language:

protocol Proto { ... }

func foo<Apple: Proto>(apple: Apple) {}
func foo(apple: Proto) {}

func foo<Apple: Proto, orange: Proto>(apple: Apple, orange: Orange) {}
func foo(apple: Proto, orange: Proto) {}

func foo<Apple: Proto, orange: Proto, banana: Proto>
    (apple: Apple, orange: Orange) -> Banana {}
func foo(apple: Proto, orange: Bar) -> Bar {}

is this correct statement? If not, why?

(edited to make it compileable :)

I don't think they are equivalent.

My understanding is that generic function may generate different versions of the code using the concret type directly, while the second version will always using Boxing.

For instance, if you have:

extension Int: Proto {}

// With the first version, the compiler can create an optimised version of the generic.
foo<Int>(apple: Int) {}

// With the second one, it always encapsulate the Int in a Proto Box
2 Likes

What bad would it make if compiler generated an optimised version of func foo(apple: Int) {} for the second version? In other words treated the second version as if it was written as the first version, early in the compilation process.

For the version with the empty body? It would cause no problems whatsoever, but only if the compiler can see into the function. If it's across a module boundary, then the compiler has to pass it through a box. Because if the compiler can't see into the implementation of the function it has to assume that it can do something like this:

extension Int: Proto { ... }
extension String: Proto { ... }
func foo(apple: Proto) {
     apple = "12345"
}

EDIT: Actually, I'm dumb and forgot that parameters are immutable. There's no reason why the compiler can't always do this for this function.

@Joe_Groff Could you please tell me the details about the number of reasons? The 'any', 'some', 'existential'.... etc keywords and concepts.... make many programmers very confused.
Is it a limitation of the language implementation like ABI stable or? For example, if the protocol witness tables allow associated type metadata to be recovered dynamically, then it can work out as smoothly as we had originally hoped?
Please give a more detailed and clear introduction.
Thanks a lot. Sincerely hope to understand the core mechanism and reasons behind it.

1 Like

I outlined the primary reason right after that sentence in the original post:

Trying to put it another way: because Swift has Self types in protocols, and allows them to be used as function arguments, you can write a protocol whose behavior as a type is fundamentally incompatible with its behavior as a generic constraint. If you consider a protocol like Swift's Equatable:

protocol Equatable {
  static func ==(a: Self, b: Self) -> Bool
}

The Self requirement allows the protocol to require that an Equatable type is only comparable to itself, not some arbitrary other Equatable value. This makes Swift protocols able to express more powerful relationships than the features they took syntactic inspiration from, such as Java and C#'s interfaces, but it creates the situation where the type Equatable can't directly satisfy the requirements of the protocol Equatable—two values of type Equatable can dynamically contain different types, but the requirement applies to two values of the same type. Changing the spelling to something like any Equatable doesn't eliminate that situation, but my hope is that it makes it clearer that you're asking for a distinct thing from the protocol, and that it's easier to explain why the type any Equatable doesn't conform to the protocol Equatable than the seemingly-broken statement that Equatable doesn't conform to Equatable.

By contrast, the closest that those other languages' interface features can get to self and associated type requirements is to represent them as generic arguments, this is why Comparable<T> in Java makes you repeat the conforming class name in class Foo implements Comparable<Foo>. And you can only use interfaces as types by providing all of their generic arguments, so you don't have the covariance issues of Swift's type erasure, though at the same time, Comparable<Foo> is unlikely to be very useful as a type because only Foo and its subclasses can usually implement it, so it provides little expressivity beyond using Foo directly. (In the case of Java, you do also have the ? wildcard to get something like Swift's type erasure, though Java's implementation is unsound and has problems of its own.) I do think that the strain on Swift's protocol types would be felt less by typical users if we had the analogous ability to bind their associated types, so that you could write any Sequence<Int> instead of only any Sequence. I talk about that a bit later in this post, but more recently Holly has just begun a pitch for this feature.

21 Likes