[Discussion] What is the future of tuples in Swift?


(Adrian Zubarev) #1

Hi Swift community,

I’d love to discuss with you the future of tuples in Swift. I’m really curious whether tuples will ever evolve beyond their current state or not.

Last year we revamped custom operators with a whole new syntax in Swift, so why don’t we start a discussion on how tuples could evolve to enhance the language.

Currently tuples are a fixed and ordered set of types, which has only one syntactic sugar feature to provide a better way of accessing the stored vales. I was referring to labeled tuples. And that’s pretty much it what tuples provides for us up until now. Tuple is a primitive type, but so are structs, enums, string, character and number types in different languages. In Swift we have way more powerful primitive types, but we’d like to call them value types instead.

Could we make tuples as *extensible* value types?
Can we vectorize tuples with fixed and unbound length of types? Speaking of which, variadics share a similar fashion of providing an unbound ordered set of types, which could be more generalized with tuples. Beyond that, vectorized tuples could probably play well with generic variadics, and we also would have the ability to limit the number of types with tuple related constraints.
If a tuple contains a specific, ordered set of types we could probably make it conditional conforming to protocols like Sequence or Collection to easily convert it to an array or any other type that accepts such existential.
The following snippet is not a design of *new* tuples nor something I’d propose at this moment, but a simple sketch to memorize what we could do to tuples. However I’m not proposing to change the current syntax for tuples, lets call it _shorthand-syntax_ (label1: Int, String), but I’d like to open the door for an additional more flexible way to create tuples.

typealias MyTuple<T> = tuple {
  T,
  Int,
  vector String, // same as `String…`
  vector(10) Bool // fixed length of 10 booleans
}

typealias MyTuple<T> = (T, Int, vector String, vector(10) Bool)

extension MyTuple : MyProtocol { ... }
Your feedback and ideas of the future of tuples is much appreciated.

···

--
Adrian Zubarev
Sent with Airmail


Newtype without automatic protocol forwarding
(Anton Zhilin) #2

I think that tuples should remain what they are now. Static-length vectors
should be types on their own and interact with tuples, with structs and
with Array<…> in the same way.

newtype should be what enables extension of tuples:

newtype Money = (Int, Int)
extension Money: CustomStringConvertible {
    var description: String {
        return String(format: "$%d.%02d", arguments: [getSelfFirst,
getSelfSecond])
    }
}
let x = (0, 42)let y = x as Money // errorprint(x.description) // error
let z = Money(0, 42)print(z.description) //=> $0.42

Here, getSelfFirst and getSelfSecond are placeholders for some syntax to
access the underlying type.


(Adrian Zubarev) #3

Actually the generics manifesto has a nice solution on how we could extend tuples in the future. Parameterized extensions + variadic generics could allow us a lot.

I played with a simple sketch on how it could be enhanced. Especially I personally like the ability of fixed vs. unbound parameter length.

Here is the sketch:

Vectors

A vector contains at least one type.
vector T - is a list of type T of an arbitrary length, such as T1, T2, …
vector(n) T - is a list with the length of maximal n T’s, such as T1, T2, … , Tn
// vectorized tuple with arbitrary length (like an existetial that can accept a tuple of type `T` of any length)
var t1: (vector Int) = (1, 2)
t1 = (1, 2, 3)
t1 = (1, 2, 3, 4)

// vectorized tuple of fixed length
let t2: (vector(10) Int) = (1, 2, 3, 4, 5, 6, 7, 8, 9, 0)

// replacing the variadic syntax `T...` with vectors
func foo1(_: vector Int)
func foo2(_: (vector Int))
func foo3(_: vector (vector Int))

foo1(1, 2, 3, 4)
foo2((1, 2, 3, 4, 5))
foo3((1, 2), (1, 2, 3, 4), (1, 2, 3))

// variadic with fixed length
func foo4(_: vector(3) Int)
func foo5(_: vector(3) (vector(3) Int))

foo4(1, 2, 3)
foo5((1, 2, 3, 4), (1, 2, 3, 4), (1, 2, 3, 4), (1, 2, 3, 4))

// `foo5` would be equivalent to `func foo5(_: (Int, Int, Int), _: (Int, Int, Int), _: (Int, Int, Int))`
func foo3(_: vector (vector Int))
Variadic generics

generic T - a constraint applied to T to indicated that T is not fixed and can be changed iteratively.
generic does not provide any information about the length of the parameter list.
// combined with vectors we'll get variadic generics

// the reason why we need both `vector` and `params` is to be able to tell the compiler that `T` has
// an arbitrary paramater length (vector) *and* that `T` can change iteratively (generic)
struct ZipSequence<vector generic T> : Sequence where T : Sequence { ... }

func zip<vector generic T>(_ sequences: vector T) -> ZipSequence<vector T> where T : Sequence { ... }
The generic keyword is more like a placeholder in that sketch.

Extending tuples:

// parameterized extensions
// vectorized tuples
extension<T> (vector T) : MutableCollection { ... }

// conditional conformances
extension<vector generic T> (vector T) : Equatable where T : Equatable { ... }

···

--
Adrian Zubarev
Sent with Airmail

Am 2. März 2017 um 12:24:03, Anton Zhilin (antonyzhilin@gmail.com) schrieb:

I think that tuples should remain what they are now. Static-length vectors should be types on their own and interact with tuples, with structs and with
Array<…> in the same way.

newtype should be what enables extension of tuples:

newtype Money = (Int, Int)

extension Money: CustomStringConvertible {
    var description: String {
        return String(format: "$%d.%02d", arguments: [getSelfFirst, getSelfSecond])
    }
}

let x = (0, 42)
let y = x as Money // error
print(x.description) // error

let z = Money(0, 42)
print(z.description) //=> $0.42
Here,
getSelfFirst and
getSelfSecond are placeholders for some syntax to access the underlying type.


(Slava Pestov) #4

I think that tuples should remain what they are now. Static-length vectors should be types on their own and interact with tuples, with structs and with Array<…> in the same way.

newtype should be what enables extension of tuples:

Does newtype add any new capability that’s not already covered by defining a struct?

Slava

···

On Mar 2, 2017, at 3:24 AM, Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:

newtype Money = (Int, Int)

extension Money: CustomStringConvertible {
    var description: String {
        return String(format: "$%d.%02d", arguments: [getSelfFirst, getSelfSecond])
    }
}

let x = (0, 42)
let y = x as Money // error
print(x.description) // error

let z = Money(0, 42)
print(z.description) //=> $0.42
Here, getSelfFirst and getSelfSecond are placeholders for some syntax to access the underlying type.

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


(Tino) #5

I think that tuples should remain what they are now. Static-length vectors should be types on their own and interact with tuples, with structs and with Array<…> in the same way.

I strongly agree: Fixed-size vectors are a well known and simple concept that shouldn't be conflated with tuples.

Afair there was a time when Swift had no set-type, and I think we are lucky that it was added, instead of forcing us to use other collections instead.
Mixing array and tuple feels like having a switch for array to behave like a set...


(Jaden Geller) #6

I’m not OP, but I imagine you can pattern match on the type. I don’t think that’s a compelling reason to add this feature though. I’d rather have active-patterns <https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/active-patterns> for structs.

···

On Mar 2, 2017, at 3:13 PM, Slava Pestov via swift-evolution <swift-evolution@swift.org> wrote:

On Mar 2, 2017, at 3:24 AM, Anton Zhilin via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

I think that tuples should remain what they are now. Static-length vectors should be types on their own and interact with tuples, with structs and with Array<…> in the same way.

newtype should be what enables extension of tuples:

Does newtype add any new capability that’s not already covered by defining a struct?

Slava

newtype Money = (Int, Int)

extension Money: CustomStringConvertible {
    var description: String {
        return String(format: "$%d.%02d", arguments: [getSelfFirst, getSelfSecond])
    }
}

let x = (0, 42)
let y = x as Money // error
print(x.description) // error

let z = Money(0, 42)
print(z.description) //=> $0.42
Here, getSelfFirst and getSelfSecond are placeholders for some syntax to access the underlying type.

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

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


(Anton Zhilin) #7

Does newtype add any new capability that’s not already covered by defining

a struct?

newtype would forward all members and conformances of the underlying type:

newtype RecordId = Int
let x: RecordId = 5let y = x + 10
extension RecordId {
    func load() -> String { … }
}
let a = x.load()let b = 42.load() // error

newtypes aim to carry purely semantic information, in this case, that those
integers can be used to fetch records from a database. We get additional
members only when we are sure that semantic of current instance is
appropriate.

As a side effect, it essentially allows to declare one-liner structs with
pattern matching. But I agree with Jaden, just adding pattern matching to
structs feels more practical. And this feature is more or less orthogonal
to the core functionality of newtype.

···

2017-03-03 2:13 GMT+03:00 Slava Pestov <spestov@apple.com>:


(Adrian Zubarev) #8

The question here is, how is a tuple different from a mathematical vector? I wasn’t referring to vector concepts from other languages, but used the keyword vector to indicate a shortcut that provides a fixed or an arbitrary length such as …T would do in variadic generics context.

A shortcut for (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) is probably not that bad to have. In my sketch it would be (vector(16) Int), but it still would be a tuple. Structs aren’t arrays, but we created a value type that represents dynamic arrays (called vectors in different languages) and called it Array. Furthermore the dynamic array is not a simple primitive type and contains a whole bunch of other things.

That said, I don’t see why tuples should not benefit from all of that, if features like parametrized extensions make it into Swift one day.

extension<…T> (T…) : MutableCollection { … }

// or from my sketch
extension<vector generic T> (vector T) : MutableCollection { … }
Being able to tell the size of such a vector is more beneficial than only having the … pre- and postfix for arbitrary length.

Currently variadics return Array<T>, which is *kinda* fine, but is not fully correct, because the returned sequence cannot be empty.

···

--
Adrian Zubarev
Sent with Airmail

Am 3. März 2017 um 13:35:47, Tino Heth (2th@gmx.de) schrieb:

I think that tuples should remain what they are now. Static-length vectors should be types on their own and interact with tuples, with structs and with Array<…> in the same way.

I strongly agree: Fixed-size vectors are a well known and simple concept that shouldn't be conflated with tuples.

Afair there was a time when Swift had no set-type, and I think we are lucky that it was added, instead of forcing us to use other collections instead.
Mixing array and tuple feels like having a switch for array to behave like a set...


(Matthew Johnson) #9

newtype feels like a leaky abstraction. Since the new type carries all the protocol conformance and members of the original type, in your example, I could add or divide RecordIds; but if they're database concepts, why would I want to do that?

If the original type were extended to add members or protocols, would the newtype gain those as well? It must, since types can be split into extensions like that in Swift a priori. So the new type is always effectively a subtype of the original, when what you often want is something that starts off with a smaller subset of its operations instead, and people can extend it in ways you don't expect.

I'm trying to imagine types where I automatically want *every* operation of its originating type, and having trouble. Even for a unit-like API, there are likely to be arithmetic operations that don't make sense.

I think a better model would be a general protocol forwarding mechanism for existing type constructs. Then I could define RecordId as a struct, internally it might have an underlying Int representation, but I would explicitly state which protocols I wanted the type to conform to and which of those are forwarded to that underlying Int. This allows us to maintain the semantic importance of protocols and build types in a way that fits existing Swift patterns, and without the magic/danger that newtype could provide.

The drawback is that if you want only a subset of operations in a protocol, you don't get that easily for free. But you could define your own protocol, retroactively conform the origin type to it *internally*, and then publicly conform the new type and forward... and as far as I can tell, that plugs the abstraction's leaks.

Agree. I have shared a rough draft of a protocol-based forwarding proposal last year. I was midway through a second draft when it became clear it was out of scope. I'm planning to revive it when the time is right.

···

Sent from my iPad

On Mar 2, 2017, at 6:56 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:

On Thu, Mar 2, 2017 at 4:08 PM Anton Zhilin via swift-evolution <swift-evolution@swift.org> wrote:
2017-03-03 2:13 GMT+03:00 Slava Pestov <spestov@apple.com>:

Does newtype add any new capability that’s not already covered by defining a struct?
newtype would forward all members and conformances of the underlying type:

newtype RecordId = Int

let x: RecordId = 5
let y = x + 10

extension RecordId {
    func load() -> String { … }
}

let a = x.load()
let b = 42.load() // error
newtypes aim to carry purely semantic information, in this case, that those integers can be used to fetch records from a database. We get additional members only when we are sure that semantic of current instance is appropriate.

As a side effect, it essentially allows to declare one-liner structs with pattern matching. But I agree with Jaden, just adding pattern matching to structs feels more practical. And this feature is more or less orthogonal to the core functionality of newtype.

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

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


(Tony Allevato) #10

newtype feels like a leaky abstraction. Since the new type carries all the
protocol conformance and members of the original type, in your example, I
could add or divide RecordIds; but if they're database concepts, why would
I want to do that?

If the original type were extended to add members or protocols, would the
newtype gain those as well? It must, since types can be split into
extensions like that in Swift a priori. So the new type is always
effectively a subtype of the original, when what you often want is
something that starts off with a smaller subset of its operations instead,
and people can extend it in ways you don't expect.

I'm trying to imagine types where I automatically want *every* operation of
its originating type, and having trouble. Even for a unit-like API, there
are likely to be arithmetic operations that don't make sense.

I think a better model would be a general protocol forwarding mechanism for
existing type constructs. Then I could define RecordId as a struct,
internally it might have an underlying Int representation, but I would
explicitly state which protocols I wanted the type to conform to and which
of those are forwarded to that underlying Int. This allows us to maintain
the semantic importance of protocols and build types in a way that fits
existing Swift patterns, and without the magic/danger that newtype could
provide.

The drawback is that if you want only a subset of operations in a protocol,
you don't get that easily for free. But you could define your own protocol,
retroactively conform the origin type to it *internally*, and then publicly
conform the new type and forward... and as far as I can tell, that plugs
the abstraction's leaks.

···

On Thu, Mar 2, 2017 at 4:08 PM Anton Zhilin via swift-evolution < swift-evolution@swift.org> wrote:

2017-03-03 2:13 GMT+03:00 Slava Pestov <spestov@apple.com>:

Does newtype add any new capability that’s not already covered by defining
a struct?

newtype would forward all members and conformances of the underlying type:

newtype RecordId = Int
let x: RecordId = 5let y = x + 10
extension RecordId {
    func load() -> String { … }
}
let a = x.load()let b = 42.load() // error

newtypes aim to carry purely semantic information, in this case, that
those integers can be used to fetch records from a database. We get
additional members only when we are sure that semantic of current instance
is appropriate.

As a side effect, it essentially allows to declare one-liner structs with
pattern matching. But I agree with Jaden, just adding pattern matching to
structs feels more practical. And this feature is more or less orthogonal
to the core functionality of newtype.

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


(Stephen Canon) #11

There are a number of ways in which vectors are different from tuples:

Relatively obvious:
- vectors are homogenous, tuples are not (necessarily). One could say that vectors are just homogenous tuples, but:
- vectors should (probably) have arithmetic operators. Tuples, even homogenous tuples, should (probably) not, because they may represent types for which arithmetic doesn’t make sense, or for which arithmetic might need to do different things per-lane.

More subtle:
- vectors and tuples want to have different machine-level calling conventions. If arithmetic is done on tuples at all, it is likely to do different things per-lane, so tuples are best passed with each lane in a different register. Vectors generally do arithmetic uniformly across lanes, so the most efficient calling convention is usually to put the vector contiguously in a vector register (if the targeted architecture has sufficiently large registers).

Vectors, Tuples, and Structs are all closely related concepts with a lot of overlap, but also some key differences. If anything, IMO a Tuple is closer to an anonymous ad-hoc Struct than it is to a Vector.

– Steve

···

On Mar 3, 2017, at 7:57 AM, Adrian Zubarev via swift-evolution <swift-evolution@swift.org> wrote:

The question here is, how is a tuple different from a mathematical vector? I wasn’t referring to vector concepts from other languages, but used the keyword vector to indicate a shortcut that provides a fixed or an arbitrary length such as …T would do in variadic generics context.

A shortcut for (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) is probably not that bad to have. In my sketch it would be (vector(16) Int), but it still would be a tuple. Structs aren’t arrays, but we created a value type that represents dynamic arrays (called vectors in different languages) and called it Array. Furthermore the dynamic array is not a simple primitive type and contains a whole bunch of other things.

That said, I don’t see why tuples should not benefit from all of that, if features like parametrized extensions make it into Swift one day.

extension<…T> (T…) : MutableCollection { … }

// or from my sketch
extension<vector generic T> (vector T) : MutableCollection { … }
Being able to tell the size of such a vector is more beneficial than only having the … pre- and postfix for arbitrary length.

Currently variadics return Array<T>, which is *kinda* fine, but is not fully correct, because the returned sequence cannot be empty.


(Adrian Zubarev) #12

That is why I explicitly noted that I used it as a keyword in my concept of fixed and arbitrary long tuples or variadics. Simply rename vector in my whole sketch to something else if you think that vectors should have a different meaning in the language.

What I seek for is the ability to have a nice shortcut for tuples, which in it’s concept could also find some use in generic variadics. (This overlap is also visible in the generics manifesto.) I simply called it a *vector*, one could rename it as variadic, but that seems not to be precise, not for (variadic(16) UInt8).

···

--
Adrian Zubarev
Sent with Airmail

Am 3. März 2017 um 16:53:03, Stephen Canon (scanon@apple.com) schrieb:

Vectors, Tuples, and Structs are all closely related concepts with a lot of overlap, but also some key differences. If anything, IMO a Tuple is closer to an anonymous ad-hoc Struct than it is to a Vector.


(Adrian Zubarev) #13

That’s what conditional conformances are made for in my opinion. The generics manifesto shows an example of equatable tuples iff every type is also equatable. That said, one probably should be able to extend a specific set of tuples to arithmetic operations if there is a need for that in your own project. I’m not saying that the standard library should do that, but we should have that ability to do that at some point, iff we’ll have parametrized extensions and generic variadics.

···

--
Adrian Zubarev
Sent with Airmail

Am 3. März 2017 um 16:53:03, Stephen Canon (scanon@apple.com) schrieb:

- vectors should (probably) have arithmetic operators. Tuples, even homogenous tuples, should (probably) not, because they may represent types for which arithmetic doesn’t make sense, or for which arithmetic might need to do different things per-lane.