[Discussion] Parameter `vector` keyword vs. triple dot prefix for variadic generics


(Adrian Zubarev) #1

Hello folks,

I’d like to revive the discussion about the Variadic generics feature from the generics manifesto.

There exist an older draft proposal from Douglas Gregor and Austin Zheng.

As part of this discussion I also would like to talk about the possibly of a new enum type Wrapped (some of you might be familiar with Result or Either type). To be clear about this, I do not want to introduce a union type here, because if you might not know:

Disjunctions (logical ORs) in type constraints: These include anonymous union-like types (e.g. (Int | String) for a type that can be inhabited by either an integer or a string). “[This type of constraint is] something that the type system cannot and should not support.”

Source: Commonly Rejected Changes
But I still have hope for the infix | type operator as part of the Wrapped enum.

To beginn with, I’d like to thank to Douglas and Austin for their work back then and putting this draft together. To be honest while reading the proposal I found it hard to keep track of where the prefix … and where the postfix … was used. Especially it was really confusing to understand the behavior of each … operator.

Let’s look at the following 3 examples from the proposal:

struct Foo<...C : Collection> {
    // Indices and C are bound to be the same length.
    typealias ...Indices = C.Index...

    // A value vector of indices
    var (...startIndices): (Indices...)

    init(...x: C...) {
        startIndices... = x.startIndex...
    }
}

struct Foo<...T : CustomStringConvertible> {

    var (...descriptions): (String...)

    init(...x: T) {
        // .description goes from Tn -> String
        // Because of this, descriptions' length is identical to that of T.
        descriptions... = x.description...
    }
}

// NOT ALLOWED!!!
struct Foo<...T : CustomStringConvertible> {

    var (...descriptions): (String...)

    mutating func doSomething() {
        descriptions... = ("foo", "bar", "baz")
    }
}
The first problem I realize here, is that the ... prefix has no ability to describe its own boundary and therefore the last example is not possible.

As contrast to this I’d like to pitch a new keyword vector and vector(Boundary_Size) (we could also use params instead, like in C#). Without the explicit Boundary_Size, which could be from 1 to (positive) n, the vector is unbound and acts exactly like the proposed ... prefix.

Let’s rewrite and reposition a few things from above with the new keyword:

struct Foo<vector C : Collection> {

    // Indices vector is bound to be the same length as its parent scope C vector, because we're using its `Index` here.
    typealias vector Indices = vector C.Index

    // A value vector of indices
    // Note: We *might* omit `vector` here for the value, because its type
    // already is a vector, or we want to be explicit everywhere?!
    vector var startIndices: Indices
     
    // Alternative 1 (preferred):
    var startIndices: Indices
     
    // Alternative 2 (still okay):
    var startIndices: vector Indices
     
    // Alternative 3 (really strange):
    vector var startIndices: vector Indices

    // Same here, `C` is a vector, so we could omit `vector` keyword before x
    init(vector x: C) {
     
        // Since the vectors `startIndices` and `x` are bound by a
        // unbound *parent scope vector* we don't need the postfix `...`
        self.startIndices = x.startIndex
    }
     
    // Alternative 1:
    init(x: C) {
        self.startIndices = x.startIndex
    }
     
    // Alternative 2:
    init(x: vector C) {
        self.startIndices = x.startIndex
    }
}

struct Foo<vector T : CustomStringConvertible> {

    // When there is no other context than the unbound parent scope vector,
    // the following vector will have the same length, otherwise it will be unbound.
    var descriptions: vector String

    init(x: T) {
        // .description goes from Tn -> String
        // Because of this, descriptions' length is identical to that of T.
        self.descriptions = x.description
    }
}

// Allowed now
struct Foo<vector T : CustomStringConvertible> {

    // This vector has an explicit boundary size and therefore not bound
    // to vector T
    vector(3) var descriptions: String
     
    // Alternative:
    var descriptions: vector(3) String

    mutating func doSomething() {
     
        // Assigning can be made with tuples
        self.descriptions = ("foo", "bar", "baz")
    }
}

Now let’s scratch the surface of the mentioned Wrapped enum.

Recently I bumped into an issue where no Swift feature could nicely solve it for me, not even conditional conformances with it’s full power. The only possible solution would be such a Wrapped enum (the only one in the whole language) or the ability to implicitly wrap any instances into enum cases when it’s possible.

// Implicitly wrapping

enum MyEnum {
    case string(String)
    case integer(Int)
}

let int = 42

// Matched against the cases and wrapped if possible, otherwise it'll be an error
let e1: MyEnum = int

// There is no `ExpressibleByStringLiteral` here, so the literal fallback to
// its default type `String`, than matched against all the cases and wraps into `.string(String)`
let e2: MyEnum = "Swift"

// Wrapped enum, with the same functionality as above.
enum Wrapped<vector T> {

    // How about the ability of extracting the vector index?
    vector case vector.index(T)
     
    // Alternative
    vector case #(T)
}

let e3: Wrapped<String, Int> = 0 // implicitly wrapped into `.1(Int)`

switch e3 {

case .0(let stringValue):
    print(stringValue)
     
case .1(let intValue)
    print(intValue)
}

// Yes, this is not a typo but a possible useless artifact.
// For the Wrapped enum we could check for same types (probably very complex to achieve), or just leave it as is.
let e4: Wrapped<String, String> = "test"

switch e3 {

case .0(let stringValue):
    print(stringValue)
     
case .1(let stringValue)
    // Will never print
    print(stringValue)
}
As mentioned above I’d like to use the infix | type operator for the Wrapped enum as a shortcut.

let x: Wrapped<A, B, C> = ...
let x: A | B | C = ...

// x.0(A)
// x.1(B)
// x.2(C)
Any feedback is much appreciated. :slight_smile:

···

From my personal point of view this new approach is way easier to read. A really good thing about it, is that it eliminates the ugly pre-/postfix ... confusion.

--
Adrian Zubarev
Sent with Airmail


(Karl) #2

Hello folks,

I’d like to revive the discussion about the Variadic generics <https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#variadic-generics> feature from the generics manifesto.

There exist an older draft proposal from Douglas Gregor and Austin Zheng <https://github.com/austinzheng/swift-evolution/blob/az-variadic-generics/proposals/XXXX-variadic-generics.md>.

As part of this discussion I also would like to talk about the possibly of a new enum type Wrapped (some of you might be familiar with Result or Either type). To be clear about this, I do not want to introduce a union type here, because if you might not know:

Disjunctions (logical ORs) in type constraints: <https://lists.swift.org/pipermail/swift-evolution-announce/2016-June/000182.html> These include anonymous union-like types (e.g. (Int | String) for a type that can be inhabited by either an integer or a string). “[This type of constraint is] something that the type system cannot and should not support.”

Source: Commonly Rejected Changes <https://github.com/apple/swift-evolution/blob/master/commonly_proposed.md#miscellaneous>
But I still have hope for the infix | type operator as part of the Wrapped enum.

To beginn with, I’d like to thank to Douglas and Austin for their work back then and putting this draft together. To be honest while reading the proposal I found it hard to keep track of where the prefix … and where the postfix … was used. Especially it was really confusing to understand the behavior of each … operator.

Let’s look at the following 3 examples from the proposal:

struct Foo<...C : Collection> {
    // Indices and C are bound to be the same length.
    typealias ...Indices = C.Index...

    // A value vector of indices
    var (...startIndices): (Indices...)

    init(...x: C...) {
        startIndices... = x.startIndex...
    }
}

struct Foo<...T : CustomStringConvertible> {

    var (...descriptions): (String...)

    init(...x: T) {
        // .description goes from Tn -> String
        // Because of this, descriptions' length is identical to that of T.
        descriptions... = x.description...
    }
}

// NOT ALLOWED!!!
struct Foo<...T : CustomStringConvertible> {

    var (...descriptions): (String...)

    mutating func doSomething() {
        descriptions... = ("foo", "bar", "baz")
    }
}
The first problem I realize here, is that the ... prefix has no ability to describe its own boundary and therefore the last example is not possible.

As contrast to this I’d like to pitch a new keyword vector and vector(Boundary_Size) (we could also use params instead, like in C#). Without the explicit Boundary_Size, which could be from 1 to (positive) n, the vector is unbound and acts exactly like the proposed ... prefix.

Let’s rewrite and reposition a few things from above with the new keyword:

struct Foo<vector C : Collection> {

    // Indices vector is bound to be the same length as its parent scope C vector, because we're using its `Index` here.
    typealias vector Indices = vector C.Index

    // A value vector of indices
    // Note: We *might* omit `vector` here for the value, because its type
    // already is a vector, or we want to be explicit everywhere?!
    vector var startIndices: Indices
     
    // Alternative 1 (preferred):
    var startIndices: Indices
     
    // Alternative 2 (still okay):
    var startIndices: vector Indices
     
    // Alternative 3 (really strange):
    vector var startIndices: vector Indices

    // Same here, `C` is a vector, so we could omit `vector` keyword before x
    init(vector x: C) {
     
        // Since the vectors `startIndices` and `x` are bound by a
        // unbound *parent scope vector* we don't need the postfix `...`
        self.startIndices = x.startIndex
    }
     
    // Alternative 1:
    init(x: C) {
        self.startIndices = x.startIndex
    }
     
    // Alternative 2:
    init(x: vector C) {
        self.startIndices = x.startIndex
    }
}

struct Foo<vector T : CustomStringConvertible> {

    // When there is no other context than the unbound parent scope vector,
    // the following vector will have the same length, otherwise it will be unbound.
    var descriptions: vector String

    init(x: T) {
        // .description goes from Tn -> String
        // Because of this, descriptions' length is identical to that of T.
        self.descriptions = x.description
    }
}

// Allowed now
struct Foo<vector T : CustomStringConvertible> {

    // This vector has an explicit boundary size and therefore not bound
    // to vector T
    vector(3) var descriptions: String
     
    // Alternative:
    var descriptions: vector(3) String

    mutating func doSomething() {
     
        // Assigning can be made with tuples
        self.descriptions = ("foo", "bar", "baz")
    }
}
From my personal point of view this new approach is way easier to read. A really good thing about it, is that it eliminates the ugly pre-/postfix ... confusion.

I’m okay with prefix … in the generic parameter list, but I don’t think variable declarations should have any annotation at all. We don’t declare an array as `array var myList: String`.

Now let’s scratch the surface of the mentioned Wrapped enum.

Recently I bumped into an issue where no Swift feature could nicely solve it for me, not even conditional conformances with it’s full power. The only possible solution would be such a Wrapped enum (the only one in the whole language) or the ability to implicitly wrap any instances into enum cases when it’s possible.

// Implicitly wrapping

enum MyEnum {
    case string(String)
    case integer(Int)
}

let int = 42

// Matched against the cases and wrapped if possible, otherwise it'll be an error
let e1: MyEnum = int

// There is no `ExpressibleByStringLiteral` here, so the literal fallback to
// its default type `String`, than matched against all the cases and wraps into `.string(String)`
let e2: MyEnum = "Swift"

// Wrapped enum, with the same functionality as above.
enum Wrapped<vector T> {

    // How about the ability of extracting the vector index?
    vector case vector.index(T)
     
    // Alternative
    vector case #(T)
}

let e3: Wrapped<String, Int> = 0 // implicitly wrapped into `.1(Int)`

switch e3 {

case .0(let stringValue):
    print(stringValue)
     
case .1(let intValue)
    print(intValue)
}

// Yes, this is not a typo but a possible useless artifact.
// For the Wrapped enum we could check for same types (probably very complex to achieve), or just leave it as is.
let e4: Wrapped<String, String> = "test"

switch e3 {

case .0(let stringValue):
    print(stringValue)
     
case .1(let stringValue)
    // Will never print
    print(stringValue)
}
As mentioned above I’d like to use the infix | type operator for the Wrapped enum as a shortcut.

let x: Wrapped<A, B, C> = ...
let x: A | B | C = ...

// x.0(A)
// x.1(B)
// x.2(C)
Any feedback is much appreciated. :slight_smile:

--
Adrian Zubarev
Sent with Airmail

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

So you want a variadic enum whose cases are bound by the number of generic type parameters. The cases would all need to be unique types, or your assignment syntax wouldn’t work. I’m guessing you want to avoid the boilerplate of having several enums with different names, so you want to merge them all in to one enum you can use everywhere.

Effectively, what you end up with is indistinguishable from an anonymous logical OR type which the core team have expressly said they don’t want in Swift. For that reason I’m -1 on the `Wrapped` idea.

- Karl

···

On 22 Nov 2016, at 11:08, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:


(Adrian Zubarev) #3

The problem I run into where only something similar as described could safe me is as follows.

I’ve got a type Document which is like an ordered dictionary of [String: Value] where Value is my custom value type. I also have a protocol ValueConvertible. Now I want to use the protocol with ExpressibleByDictionaryLiteral in my Document. The Value type is an enum and can also wrap a Document.

The problem that I get here is when creating nested dictionary literals.

// This works just fine with only with
// `public init(dictionaryLiteral elements: (String, Value)...)`

let valueConverible: ValueConvertible = …

let document: Document = [
    "double": 2.0,
    "string": "test",
    "document": [
        "level": 1,
        "document": ["level": 2]
    ],
    "array": [1, 2, 3],
    "bool": true,
    "int64": 42,
    "doc": [["key": 1]],
    "HERE": valueConverible.value // <— get a `Value` from the computed property.
]
Instead I want a more natural way.

// For // `public init(dictionaryLiteral elements: (String, ValueConvertible)…)`

let document: Document = [
    "double": 2.0,
    "string": "test",
    "document": [
        "level": 1,
        "document": ["level": 2]
    ], // Error `Dictionary` does not conform to `ValueConvertible`
    "HERE": valueConverible // It will unwrap the value inside the initializer.
]
Now I lost nested dictionaries. You may think that something like extension Dictionary : ValueConvertible where Key == String, Value == ModuleName.Value will solve the issue here. But that’s not true because a Dictionary as a default type for dictionary literals is unordered, where the order from my literal is needed to be strict.

Now assume that I’d have Wrapped.

// For // `public init(dictionaryLiteral elements: (String, Wrapped<Value, ValueConvertible>)…)`

let document: Document = [
    "double": 2.0,
    "string": "test",
    "document": [
        "level": 1,
        "document": ["level": 2]
    ], // Fine, compiler is happy again. `Value` conforms to `ExpressibleByDictionaryLiteral`
    "HERE": valueConverible // fine
]
To me it would be enough if Wrapped would match types from left to right.

let x: Wrapped<A, B, C> = b // checks A -> fails, checks B, fine wrap into `Wrapped<A, B, C>.1(b)`
Again even we’d allow Wrapped<A, A> the compiler would walk from left to right during the matching process, and never reach the .1(A) when wrapping an instance of A. This last example is simply useless but it makes Wrapped simple.

Basically I imagine Wrapped like this:

enum Wrapped<A, B> {
    case 0(A)
    case 1(B)
}

enum Wrapped<A, B, C> {
    case 0(A)
    case 1(B)
    case 2(C)
}

enum Wrapped<A, B, C, D> {
    case 0(A)
    case 1(B)
    case 2(C)
    case 3(D)
}

// or simply
enum Wrapped<vector T> {
     
    vector case #(T)
}
Wouldn’t sucht a type come handy in other areas as well? Any idea?

If not Wrapped, so what about the first pitch of allowing implicitly wrap enums?

···

--
Adrian Zubarev
Sent with Airmail

Am 22. November 2016 um 12:49:39, Karl (razielim@gmail.com) schrieb:

So you want a variadic enum whose cases are bound by the number of generic type parameters. The cases would all need to be unique types, or your assignment syntax wouldn’t work. I’m guessing you want to avoid the boilerplate of having several enums with different names, so you want to merge them all in to one enum you can use everywhere.

Effectively, what you end up with is indistinguishable from an anonymous logical OR type which the core team have expressly said they don’t want in Swift. For that reason I’m -1 on the `Wrapped` idea.


(Adrian Zubarev) #4

The general idea of a keyword vector kept me thinking for quite a while.

Could such a keyword potentially replace a Tuple?

let aTuple: (Int, Int, Int, Int, Int, Int, Int, Int) = (1,2,3,4,5,6,7,8)

let aVector: vector(8) Int = (1,2,3,4,5,6,7,8)

// or `Int vector(8)`
// or `vector(of: 8) Int`
// or `Int vector(of: 8)`
It’s way easier to write and it reads well to.

There are other potential designs vor such a keyword that I could think of:

#vector(Int, 8)
#vector(8, Int)
#vector(of: 8, Int)
Or we could combine the idea of vectors with tuples. By that I mean that we still should be able to write labeled tuples/vectors like:

typealias MyType = (a: Int, b: Int) // labeled `vector(2) Int`
As for variadic generics:

func foo<vector T>(a: vector(3) String, b: T) {
     
    print(a.0)
    print(a.1)
    print(a.2)
     
    print(b)
}
When using vectors inside the angle brackets, vector T represents an arbitrary number of n individual, independent type parameters such as T1, T2, … , Tn. And we also could limit the number of the generic parameters we want.

func boo<vector(2) T>(a: T) {
     
    // here we know the exact boundary and it's safe to use the index of the vector
    print(type(of: a.0)) // T1
    print(type(of: a.1)) // T2
    print(type(of: a.2)) // T3
}
We should also be able to define a variable boundary which can only be using in other vectors of the same scope.

let a: vector(8) Int = (1,1,1,1,1,1,1,1)
let b: vector(8) Int = (1,1,1,1,1,1,1,1)

// vectors can be passed by tuples/vectors
// here is `x` a variable boundary
func add(a: vector(x) Int, b: vector(x) Int) -> vector(x) Int {
    return a + b
}

add(a: a, b: b) // returns (2,2,2,2,2,2,2,2)
The Swift community also want to extend tuples one day. We should be able to this with vectors as well:

extension (Int, Int) { … }

// VS:

extension vector(2) Int { … }
Vectors would fully eliminate that ugly … pre-/postfix and leave it only for ranges.

···

--
Adrian Zubarev
Sent with Airmail

Am 22. November 2016 um 12:49:39, Karl (razielim@gmail.com) schrieb:

I’m okay with prefix … in the generic parameter list, but I don’t think variable declarations should have any annotation at all. We don’t declare an array as `array var myList: String`.


(Adrian Zubarev) #5

Bikeshedding:

enum MyEnum<vector T> {
     
    vector case name(T)
}
As for my second thought, allowing auto wrapping for every enum seems like an overkill.

For instance

enum Test {
    case a(Int, Int)
    case b(Int, Int)
}

let test: Test = (0, 1) // You might really want here `b(0, 1)` and not `a(0, 1)`
Therefore a single Wrapped type that would support this auto wrapping would come handy. We already have auto wrapping for Optionals. Wrapped would be a second enum that supports this behavior.

Btw to make Wrapped not wrap a single value we’d make it like this:

enum Wrapped<T, vector U> {

    case 0(T)
    vector case #(U) // make # somehow start from +1
}

···

From my imagination, by the time we’ll have variadic generics in Swift one would assume to be able to write own variadic generic enums, but without the pitched auto wrapping feature, and probably without the index of the parameter.

--
Adrian Zubarev
Sent with Airmail

Am 22. November 2016 um 14:00:20, Adrian Zubarev (adrian.zubarev@devandartist.com) schrieb:

The problem I run into where only something similar as described could safe me is as follows.

I’ve got a type Document which is like an ordered dictionary of [String: Value] where Value is my custom value type. I also have a protocol ValueConvertible. Now I want to use the protocol with ExpressibleByDictionaryLiteral in my Document. The Value type is an enum and can also wrap a Document.

The problem that I get here is when creating nested dictionary literals.

// This works just fine with only with
// `public init(dictionaryLiteral elements: (String, Value)...)`

let valueConverible: ValueConvertible = …

let document: Document = [
    "double": 2.0,
    "string": "test",
    "document": [
        "level": 1,
        "document": ["level": 2]
    ],
    "array": [1, 2, 3],
    "bool": true,
    "int64": 42,
    "doc": [["key": 1]],
    "HERE": valueConverible.value // <— get a `Value` from the computed property.
]
Instead I want a more natural way.

// For // `public init(dictionaryLiteral elements: (String, ValueConvertible)…)`

let document: Document = [
    "double": 2.0,
    "string": "test",
    "document": [
        "level": 1,
        "document": ["level": 2]
    ], // Error `Dictionary` does not conform to `ValueConvertible`
    "HERE": valueConverible // It will unwrap the value inside the initializer.
]
Now I lost nested dictionaries. You may think that something like extension Dictionary : ValueConvertible where Key == String, Value == ModuleName.Value will solve the issue here. But that’s not true because a Dictionary as a default type for dictionary literals is unordered, where the order from my literal is needed to be strict.

Now assume that I’d have Wrapped.

// For // `public init(dictionaryLiteral elements: (String, Wrapped<Value, ValueConvertible>)…)`

let document: Document = [
    "double": 2.0,
    "string": "test",
    "document": [
        "level": 1,
        "document": ["level": 2]
    ], // Fine, compiler is happy again. `Value` conforms to `ExpressibleByDictionaryLiteral`
    "HERE": valueConverible // fine
]
To me it would be enough if Wrapped would match types from left to right.

let x: Wrapped<A, B, C> = b // checks A -> fails, checks B, fine wrap into `Wrapped<A, B, C>.1(b)`
Again even we’d allow Wrapped<A, A> the compiler would walk from left to right during the matching process, and never reach the .1(A) when wrapping an instance of A. This last example is simply useless but it makes Wrapped simple.

Basically I imagine Wrapped like this:

enum Wrapped<A, B> {
    case 0(A)
    case 1(B)
}

enum Wrapped<A, B, C> {
    case 0(A)
    case 1(B)
    case 2(C)
}

enum Wrapped<A, B, C, D> {
    case 0(A)
    case 1(B)
    case 2(C)
    case 3(D)
}

// or simply
enum Wrapped<vector T> {
      
    vector case #(T)
}
Wouldn’t sucht a type come handy in other areas as well? Any idea?

If not Wrapped, so what about the first pitch of allowing implicitly wrap enums?

--
Adrian Zubarev
Sent with Airmail

Am 22. November 2016 um 12:49:39, Karl (razielim@gmail.com) schrieb:

So you want a variadic enum whose cases are bound by the number of generic type parameters. The cases would all need to be unique types, or your assignment syntax wouldn’t work. I’m guessing you want to avoid the boilerplate of having several enums with different names, so you want to merge them all in to one enum you can use everywhere.

Effectively, what you end up with is indistinguishable from an anonymous logical OR type which the core team have expressly said they don’t want in Swift. For that reason I’m -1 on the `Wrapped` idea.


(Adrian Zubarev) #6

Actually no, I must correct myself here. We still need the postfix … to indicate how we want to pass the vector.

For instance:

func foo<vector(3) T>(label v: T) { … }

foo(label: (1,2,3))

func boo<vector(3) T>(label v: T…) { … }

boo(label: 1, label: 2, label: 3)
This topic is really interesting to explore. :slight_smile:

···

--
Adrian Zubarev
Sent with Airmail

Am 23. November 2016 um 19:50:31, Adrian Zubarev (adrian.zubarev@devandartist.com) schrieb:

Vectors would fully eliminate that ugly … pre-/postfix and leave it only for ranges.