[Pre-Proposal] Type Aliases as Pseudo-Types


(Haravikk) #1

This is an idea I had while working with collections, and is particularly inspired by those that use common index types.

Consider for example an array; for indices it simply uses an integer, however, while this is a perfectly valid type to use it opens up the possibility of integers from any number of different sources being passed in by mistake and causing run-time errors. The same is true for wrapping types that use AnyIndex, or really any type that uses Any* to hide underlying types, as on the surface all AnyIndex instances give the illusion of being compatible when they're not, and will only produce errors at run-time when a conflict arises.

The idea to combat this is simple; a new attribute that can be applied to a typealias, my working name is @unique, but there's probably a much better name for it. When applied to a type-alias it indicates to the type-checker that the type being aliased should be treated as a unique type outside of the scope in which it is declared.

For example:

  struct MyCollection<T> : Collection {
    @unique typealias Index = Int
    var startIndex:Index { return 0 }
    …
  }

  var foo = MyCollection<Foo>()
  var bar = MyCollection<Bar>()
  foo[bar.startIndex] // warning: incompatible types

Although the underlying type is an Int for both collections, and Ints can be used internally to satisfy methods/properties of type Index (i.e- within MyCollection the concrete type is still Int), externally indices are treated as a unique type of MyCollection<T>.Index, that just happens to have all the same methods, operators and properties as Int, minimising the risk of passing an index from the wrong source.

If you actually want to pass the "wrong" type you can still do it by casting:

  foo[bar.startIndex as MyCollection<Foo>.Index]

Bit verbose, but it makes absolutely clear that you want this to happen, rather than it happening by mistake. The same can also be done in reverse (to change an index to an Int).

Of course this isn't completely fool-proof, as two different instances of MyCollection<Foo> could still confuse indices as they're of the same pseudo-type, but in other cases it reduces the possibility of a mistake. It was the recent discussion on the .enumerate() method that reminded me of this, as a common mistake is to think that the offsets produced by enumerate are indices, just because they happen to be compatible with an Array, this could eliminate that as a mistake by forcing developers to see what's actually happening.

Currently to do something like this requires duplicating types, or using wrappers around Int just to make it appear to be different; it's not hard as such but there's no standard for it, so making it easier and more prevalent is desirable.

This shouldn't have any implications to ABI compatibility as it's a type-checker only feature (the code should still compile exactly as the same is now), but it's a partially source-breaking change in that if Array etc. are changed to use this feature then any code relying on Array indices being integers will need to add a cast. Since the types are still known to be compatible though a mismatch needn't be an error, a warning should suffice, in which case the change won't actually prevent compilation of existing code.


(Brent Royal-Gordon) #2

I've encountered the same problem in essentially the same place, so I'd like to see a solution too.

This sounds like a slight variation on what, in previous discussions, has been called `newtype`. IIRC, one of the reasons we've never done `newtype` is that it's not clear which features you want to bring over from the base type, or which types should be used for things like operators. (If you have `func + (lhs: Int, rhs: Int) -> Int`, you don't want `func + (lhs: Index, rhs: Index) -> Index`; you want `func + (lhs: Index, rhs: Int) -> Index`.)

I'd like to suggest a design that I don't think has been considered before. Currently, if the first type in an enum's inheritance clause is a concrete type, a set of magical behaviors occur:

* The enum is conformed to `RawRepresentable` with a `RawValue` of the concrete type.
* Each case is associated with a raw value, specified by a literal attached to the case.
* `init?(rawValue:)` and `var rawValue { get }` are automatically generated.

There is currently no equivalent for structs, but I suggest we add one.

If you say:

  struct Index: Int {}

This is automatically equivalent to saying:

  struct Index: RawRepresentable {
    var rawValue: Int
    init(rawValue: Int) { self.rawValue = rawValue }
  }

And a special rule is applied: You may not declare any other stored properties.

Additionally, for both `enum`s and `struct`s with raw types, I would suggest that, if you conform to a protocol which the raw type conforms to and then fail to fulfill its (non-defaulted) requirements, Swift should generate a member which forwards to the raw value's implementation. It might even be nice to do the same when an initializer, method, property, or subscript is declared without providing a body. This would make it easy to decide which functionality should be exposed and how it should be provided--and it would provide a partial way to fulfill the frequent request for syntactic sugar for `Equatable`, `Hashable`, and `Comparable` conformances. (I could imagine this being generalized later on.)

The main drawback I can see is that the `rawValue` could not be encapsulated, since the conformance to the public `RawRepresentable` protocol could not be made private. That might be acceptable in a convenience feature, or we might decide (perhaps for both `struct`s and `enum`s) that Swift should generate the members without actually conforming the type unless explicitly asked to.

···

On Feb 18, 2017, at 2:18 AM, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

This is an idea I had while working with collections, and is particularly inspired by those that use common index types.

Consider for example an array; for indices it simply uses an integer, however, while this is a perfectly valid type to use it opens up the possibility of integers from any number of different sources being passed in by mistake and causing run-time errors. The same is true for wrapping types that use AnyIndex, or really any type that uses Any* to hide underlying types, as on the surface all AnyIndex instances give the illusion of being compatible when they're not, and will only produce errors at run-time when a conflict arises.

The idea to combat this is simple; a new attribute that can be applied to a typealias, my working name is @unique, but there's probably a much better name for it. When applied to a type-alias it indicates to the type-checker that the type being aliased should be treated as a unique type outside of the scope in which it is declared.

--
Brent Royal-Gordon
Architechies


(Matthew Johnson) #3

This is an idea I had while working with collections, and is particularly inspired by those that use common index types.

Consider for example an array; for indices it simply uses an integer, however, while this is a perfectly valid type to use it opens up the possibility of integers from any number of different sources being passed in by mistake and causing run-time errors. The same is true for wrapping types that use AnyIndex, or really any type that uses Any* to hide underlying types, as on the surface all AnyIndex instances give the illusion of being compatible when they're not, and will only produce errors at run-time when a conflict arises.

The idea to combat this is simple; a new attribute that can be applied to a typealias, my working name is @unique, but there's probably a much better name for it. When applied to a type-alias it indicates to the type-checker that the type being aliased should be treated as a unique type outside of the scope in which it is declared.

I've encountered the same problem in essentially the same place, so I'd like to see a solution too.

This sounds like a slight variation on what, in previous discussions, has been called `newtype`. IIRC, one of the reasons we've never done `newtype` is that it's not clear which features you want to bring over from the base type, or which types should be used for things like operators. (If you have `func + (lhs: Int, rhs: Int) -> Int`, you don't want `func + (lhs: Index, rhs: Index) -> Index`; you want `func + (lhs: Index, rhs: Int) -> Index`.)

I'd like to suggest a design that I don't think has been considered before. Currently, if the first type in an enum's inheritance clause is a concrete type, a set of magical behaviors occur:

* The enum is conformed to `RawRepresentable` with a `RawValue` of the concrete type.
* Each case is associated with a raw value, specified by a literal attached to the case.
* `init?(rawValue:)` and `var rawValue { get }` are automatically generated.

There is currently no equivalent for structs, but I suggest we add one.

If you say:

  struct Index: Int {}

This is automatically equivalent to saying:

  struct Index: RawRepresentable {
    var rawValue: Int
    init(rawValue: Int) { self.rawValue = rawValue }
  }

And a special rule is applied: You may not declare any other stored properties.

Additionally, for both `enum`s and `struct`s with raw types, I would suggest that, if you conform to a protocol which the raw type conforms to and then fail to fulfill its (non-defaulted) requirements, Swift should generate a member which forwards to the raw value's implementation. It might even be nice to do the same when an initializer, method, property, or subscript is declared without providing a body. This would make it easy to decide which functionality should be exposed and how it should be provided--and it would provide a partial way to fulfill the frequent request for syntactic sugar for `Equatable`, `Hashable`, and `Comparable` conformances. (I could imagine this being generalized later on.)

The main drawback I can see is that the `rawValue` could not be encapsulated, since the conformance to the public `RawRepresentable` protocol could not be made private. That might be acceptable in a convenience feature, or we might decide (perhaps for both `struct`s and `enum`s) that Swift should generate the members without actually conforming the type unless explicitly asked to.

A lot of this is very similar to the protocol-based forwarding proposal I worked on last year. That proposal would avoid the problems you describe around not being able to properly encapsulate `RawRepresentable`. It was also able to handle many nuances around forwarding of self and associated type requirements. It even had an example of how something like `newtype` could be defined in terms of the mechanisms it provides.

I was mid-way through a second draft when it became clear it was not in scope for Swift 3. At that point I deferred further work until the time is right. I would like to revisit it eventually, but it is clearly out of scope for Swift 4 as well.

···

On Feb 18, 2017, at 4:54 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 18, 2017, at 2:18 AM, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

--
Brent Royal-Gordon
Architechies

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


(Haravikk) #4

Hmm, that's a very interesting alternative; it's a lot like what we have to do now ourselves anyway, plus more flexible overall. I still like the idea of being able to do something at the typealias itself purely for convenience, but that's something that could always come later.

For example:

  typealias Index:Int // shorthand for struct IntIndex: Int {}, typealias Index = IntIndex

Future possibility anyway.

···

On 18 Feb 2017, at 10:54, Brent Royal-Gordon <brent@architechies.com> wrote:

On Feb 18, 2017, at 2:18 AM, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

This is an idea I had while working with collections, and is particularly inspired by those that use common index types.

Consider for example an array; for indices it simply uses an integer, however, while this is a perfectly valid type to use it opens up the possibility of integers from any number of different sources being passed in by mistake and causing run-time errors. The same is true for wrapping types that use AnyIndex, or really any type that uses Any* to hide underlying types, as on the surface all AnyIndex instances give the illusion of being compatible when they're not, and will only produce errors at run-time when a conflict arises.

The idea to combat this is simple; a new attribute that can be applied to a typealias, my working name is @unique, but there's probably a much better name for it. When applied to a type-alias it indicates to the type-checker that the type being aliased should be treated as a unique type outside of the scope in which it is declared.

I've encountered the same problem in essentially the same place, so I'd like to see a solution too.

This sounds like a slight variation on what, in previous discussions, has been called `newtype`. IIRC, one of the reasons we've never done `newtype` is that it's not clear which features you want to bring over from the base type, or which types should be used for things like operators. (If you have `func + (lhs: Int, rhs: Int) -> Int`, you don't want `func + (lhs: Index, rhs: Index) -> Index`; you want `func + (lhs: Index, rhs: Int) -> Index`.)

I'd like to suggest a design that I don't think has been considered before. Currently, if the first type in an enum's inheritance clause is a concrete type, a set of magical behaviors occur:

* The enum is conformed to `RawRepresentable` with a `RawValue` of the concrete type.
* Each case is associated with a raw value, specified by a literal attached to the case.
* `init?(rawValue:)` and `var rawValue { get }` are automatically generated.

There is currently no equivalent for structs, but I suggest we add one.

If you say:

  struct Index: Int {}

This is automatically equivalent to saying:

  struct Index: RawRepresentable {
    var rawValue: Int
    init(rawValue: Int) { self.rawValue = rawValue }
  }

And a special rule is applied: You may not declare any other stored properties.

Additionally, for both `enum`s and `struct`s with raw types, I would suggest that, if you conform to a protocol which the raw type conforms to and then fail to fulfill its (non-defaulted) requirements, Swift should generate a member which forwards to the raw value's implementation. It might even be nice to do the same when an initializer, method, property, or subscript is declared without providing a body. This would make it easy to decide which functionality should be exposed and how it should be provided--and it would provide a partial way to fulfill the frequent request for syntactic sugar for `Equatable`, `Hashable`, and `Comparable` conformances. (I could imagine this being generalized later on.)

The main drawback I can see is that the `rawValue` could not be encapsulated, since the conformance to the public `RawRepresentable` protocol could not be made private. That might be acceptable in a convenience feature, or we might decide (perhaps for both `struct`s and `enum`s) that Swift should generate the members without actually conforming the type unless explicitly asked to.

--
Brent Royal-Gordon
Architechies


(Haravikk) #5

Did you get as far as putting up anywhere to look at?

It may be relevant to Swift 4 stage 2, though I'll admit I'm confused as hell what is and is not in scope; but I believe the ABI compatibility stuff has been pushed back so it might be accepting more general proposals again, I was going to re-submit some of mine that were out-of-scope before.

···

On 18 Feb 2017, at 16:28, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 18, 2017, at 4:54 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 18, 2017, at 2:18 AM, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

This is an idea I had while working with collections, and is particularly inspired by those that use common index types.

Consider for example an array; for indices it simply uses an integer, however, while this is a perfectly valid type to use it opens up the possibility of integers from any number of different sources being passed in by mistake and causing run-time errors. The same is true for wrapping types that use AnyIndex, or really any type that uses Any* to hide underlying types, as on the surface all AnyIndex instances give the illusion of being compatible when they're not, and will only produce errors at run-time when a conflict arises.

The idea to combat this is simple; a new attribute that can be applied to a typealias, my working name is @unique, but there's probably a much better name for it. When applied to a type-alias it indicates to the type-checker that the type being aliased should be treated as a unique type outside of the scope in which it is declared.

I've encountered the same problem in essentially the same place, so I'd like to see a solution too.

This sounds like a slight variation on what, in previous discussions, has been called `newtype`. IIRC, one of the reasons we've never done `newtype` is that it's not clear which features you want to bring over from the base type, or which types should be used for things like operators. (If you have `func + (lhs: Int, rhs: Int) -> Int`, you don't want `func + (lhs: Index, rhs: Index) -> Index`; you want `func + (lhs: Index, rhs: Int) -> Index`.)

I'd like to suggest a design that I don't think has been considered before. Currently, if the first type in an enum's inheritance clause is a concrete type, a set of magical behaviors occur:

* The enum is conformed to `RawRepresentable` with a `RawValue` of the concrete type.
* Each case is associated with a raw value, specified by a literal attached to the case.
* `init?(rawValue:)` and `var rawValue { get }` are automatically generated.

There is currently no equivalent for structs, but I suggest we add one.

If you say:

  struct Index: Int {}

This is automatically equivalent to saying:

  struct Index: RawRepresentable {
    var rawValue: Int
    init(rawValue: Int) { self.rawValue = rawValue }
  }

And a special rule is applied: You may not declare any other stored properties.

Additionally, for both `enum`s and `struct`s with raw types, I would suggest that, if you conform to a protocol which the raw type conforms to and then fail to fulfill its (non-defaulted) requirements, Swift should generate a member which forwards to the raw value's implementation. It might even be nice to do the same when an initializer, method, property, or subscript is declared without providing a body. This would make it easy to decide which functionality should be exposed and how it should be provided--and it would provide a partial way to fulfill the frequent request for syntactic sugar for `Equatable`, `Hashable`, and `Comparable` conformances. (I could imagine this being generalized later on.)

The main drawback I can see is that the `rawValue` could not be encapsulated, since the conformance to the public `RawRepresentable` protocol could not be made private. That might be acceptable in a convenience feature, or we might decide (perhaps for both `struct`s and `enum`s) that Swift should generate the members without actually conforming the type unless explicitly asked to.

A lot of this is very similar to the protocol-based forwarding proposal I worked on last year. That proposal would avoid the problems you describe around not being able to properly encapsulate `RawRepresentable`. It was also able to handle many nuances around forwarding of self and associated type requirements. It even had an example of how something like `newtype` could be defined in terms of the mechanisms it provides.

I was mid-way through a second draft when it became clear it was not in scope for Swift 3. At that point I deferred further work until the time is right. I would like to revisit it eventually, but it is clearly out of scope for Swift 4 as well.


(Matthew Johnson) #6

This is an idea I had while working with collections, and is particularly inspired by those that use common index types.

Consider for example an array; for indices it simply uses an integer, however, while this is a perfectly valid type to use it opens up the possibility of integers from any number of different sources being passed in by mistake and causing run-time errors. The same is true for wrapping types that use AnyIndex, or really any type that uses Any* to hide underlying types, as on the surface all AnyIndex instances give the illusion of being compatible when they're not, and will only produce errors at run-time when a conflict arises.

The idea to combat this is simple; a new attribute that can be applied to a typealias, my working name is @unique, but there's probably a much better name for it. When applied to a type-alias it indicates to the type-checker that the type being aliased should be treated as a unique type outside of the scope in which it is declared.

I've encountered the same problem in essentially the same place, so I'd like to see a solution too.

This sounds like a slight variation on what, in previous discussions, has been called `newtype`. IIRC, one of the reasons we've never done `newtype` is that it's not clear which features you want to bring over from the base type, or which types should be used for things like operators. (If you have `func + (lhs: Int, rhs: Int) -> Int`, you don't want `func + (lhs: Index, rhs: Index) -> Index`; you want `func + (lhs: Index, rhs: Int) -> Index`.)

I'd like to suggest a design that I don't think has been considered before. Currently, if the first type in an enum's inheritance clause is a concrete type, a set of magical behaviors occur:

* The enum is conformed to `RawRepresentable` with a `RawValue` of the concrete type.
* Each case is associated with a raw value, specified by a literal attached to the case.
* `init?(rawValue:)` and `var rawValue { get }` are automatically generated.

There is currently no equivalent for structs, but I suggest we add one.

If you say:

  struct Index: Int {}

This is automatically equivalent to saying:

  struct Index: RawRepresentable {
    var rawValue: Int
    init(rawValue: Int) { self.rawValue = rawValue }
  }

And a special rule is applied: You may not declare any other stored properties.

Additionally, for both `enum`s and `struct`s with raw types, I would suggest that, if you conform to a protocol which the raw type conforms to and then fail to fulfill its (non-defaulted) requirements, Swift should generate a member which forwards to the raw value's implementation. It might even be nice to do the same when an initializer, method, property, or subscript is declared without providing a body. This would make it easy to decide which functionality should be exposed and how it should be provided--and it would provide a partial way to fulfill the frequent request for syntactic sugar for `Equatable`, `Hashable`, and `Comparable` conformances. (I could imagine this being generalized later on.)

The main drawback I can see is that the `rawValue` could not be encapsulated, since the conformance to the public `RawRepresentable` protocol could not be made private. That might be acceptable in a convenience feature, or we might decide (perhaps for both `struct`s and `enum`s) that Swift should generate the members without actually conforming the type unless explicitly asked to.

A lot of this is very similar to the protocol-based forwarding proposal I worked on last year. That proposal would avoid the problems you describe around not being able to properly encapsulate `RawRepresentable`. It was also able to handle many nuances around forwarding of self and associated type requirements. It even had an example of how something like `newtype` could be defined in terms of the mechanisms it provides.

I was mid-way through a second draft when it became clear it was not in scope for Swift 3. At that point I deferred further work until the time is right. I would like to revisit it eventually, but it is clearly out of scope for Swift 4 as well.

Did you get as far as putting up anywhere to look at?

It may be relevant to Swift 4 stage 2, though I'll admit I'm confused as hell what is and is not in scope; but I believe the ABI compatibility stuff has been pushed back so it might be accepting more general proposals again, I was going to re-submit some of mine that were out-of-scope before.

No, it’s in a kind of mid-draft state. I’ll try to get it into a reasonable place and post a link sometime soon, but I don’t plan to officially propose it unless there is a lot of support, especially from the core team. This feels like something that is probably out of scope so I don’t want to invest too much time into it right now unless that proves to be wrong.

···

On Feb 18, 2017, at 3:16 PM, Haravikk <swift-evolution@haravikk.me> wrote:

On 18 Feb 2017, at 16:28, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 18, 2017, at 4:54 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 18, 2017, at 2:18 AM, Haravikk via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote: