[swift-evolution] Make generics covariant and add generics to protocols
Howard Lovatt
howard.lovatt at gmail.com
Wed Jan 13 15:54:12 CST 2016
@Simon,
In the Swift code below:
let array = [1]
array[0] // 1, OK
array[1] // Error not detected by compiler but detected at runtime
you have an example of an error detected at runtime that terminates program
execution, it doesn't return an optional.
In other languages this would be a type error, these languages are usually
described as dependent type (https://en.wikipedia.org/wiki/Dependent_type).
An example of the advantage of this would be vector (or matrix
multiplication), e.g. imagine that Swift had dependent types:
let row = RowVec(1, 2) // Type: matrix of int with 1 row and 0 columns
(note size is part of the type)
let col = ColVec(3) // Type: matrix of int with 0 rows and 1 column
let scaler = row * col // Compile time error because both vectors
should be the same length
At the moment if you wrote a matrix package in Swift the above example
would be a runtime error and not a compile time error, but with dependent
typing it would be a compile time error.
There is another discussion of Swift Evolution on calculable types that are
closely related to dependent typing.
To me you just strike the balance, sometimes static checking is best
sometimes runtime. You strike the balance by how practical it is to do the
static checking, if the burden that the static checking adds to the users
then it isn't worth it. This is the case with annotated variance in
languages like Java and Scala, the annotations do not add much. Hence I am
suggesting system that is simple to use, much like Swift arrays are easy to
use but not totally, but largely, statically typed.
Hope that explains my reasoning for making most type error compile time
checked but a small subset runtime checked,
-- Howard.
On 13 January 2016 at 14:01, Simon Pilkington <simonmpilkington at icloud.com>
wrote:
> I’d be interested in reading up on what the Oracle response was if you
> have links.
>
> As you mentioned even Swift doesn’t get away from runtime type checking
> but in the two examples you mentioned - array out of bounds and casts -
> Swift makes use of the Optionals system to highlight that an operation may
> fail and allows the user to handle that failure. Covariance should have
> similar syntax support (for example use of optional chaining similar to
> optional protocol requirements to indicate that a call may fail due to
> incorrect types). For the compiler to understand when such failure is
> possible, some kind of covariance syntax would be required.
>
> As a related question, do you see covariance syntax as such a burden?
>
> -Simon
>
>
> On 13 Jan 2016, at 12:47 PM, Howard Lovatt <howard.lovatt at gmail.com>
> wrote:
>
> Yes you can annotate for covariance, invariance, and contravariance, both
> Java and Scala, allow all three. The problem is that the code becomes
> splattered with variance annotations. The Java people themselves have
> publicly regretted this and wished that they had made covariance the
> default. If you look at generic code invariance and covariance are by far
> the most common requirements; this proposal would address these common use
> case without burdening the programmer.
>
> Swift, and no usable language, is completely statically typed. Examples in
> Swift of runtime type checking are array out of bounds and casts. There are
> other examples of non-type related runtime checks is Swift: numerical
> overflow, throws, using optionals to signal errors, and using enums to
> signal errors. I say use what is appropriate, static type checking if it is
> easy to do, otherwise runtime type checking. Note I am not proposing an
> unsafe language like C, it is still type checked.
>
> There is a strong positive precedence for a type check on write, Java
> arrays (not Java `List`s). Arrays in Java may be passed covariantly, and
> this is extensively used. However if you attempt to write the wrong type
> into the array you will get an `ArrayStoreException`. In practice you don't
> get many `ArrayStoreException`, non of my code has ever had one. Its just
> not something you do in practice, as noted before contravariance is rare.
>
> Thanks for you comments and I hope this eases your concerns,
>
> -- Howard.
>
>
> On 13 January 2016 at 11:33, Simon Pilkington <simonmpilkington at icloud.com
> > wrote:
>
>> The problem is that conceptually and behaviourally Box<Bottom> *is indeed
>> not* a Box<Top> and cannot be treated the same as one. The proposal
>> attempts to get around this difference with a runtime failure but this
>> would result in very fragile code - you get passed a Box<Top> and want to
>> pass it a subclass of Top, will it succeed, who knows. You probably would
>> be able to check the types but the complier wouldn’t highlight that this is
>> an operation that could potentially fail.
>>
>> This seems to be very much against Swift’s goal of safety being enforced
>> by the compiler as much as possible.
>>
>> Java uses the wildcard syntax to highlight this conceptual and
>> behavioural difference - Box<Bottom> is not covariant with Box<Top> but
>> rather with Box<? extends Top>. The compiler can then enforce that a
>> programmer doesn’t try to pass an incompatible type to a variable of such
>> type. Even though this is a complication to the language (many Java
>> programmers struggle with correctly using the wildcard syntax) I don’t see
>> covariance for generics being added to Swift in a robust manner without
>> some kind of similar syntax.
>>
>> -Simon
>>
>> On 12 Jan 2016, at 8:45 PM, Howard Lovatt via swift-evolution <
>> swift-evolution at swift.org> wrote:
>>
>> Currently you generics are invariant whereas function arguments etc. are
>> covariant. I am suggesting that if the way generics are implemented is
>> changed then they can be made covariant and that this will add considerable
>> utility to Swift generics.
>>
>> 1st a demonstration of the current situation of invariant generics:
>>
>> // Current system
>> class Top {}
>> class Bottom: Top {}
>>
>> struct Box<T: AnyObject> {
>> var value: T
>> init(_ initialValue: T) {
>> value = initialValue;
>> }
>> }
>>
>> let boxB = Box(Bottom())
>> // let boxT: Box<Top> = boxB // Covariance currently not allowed
>>
>> The key point is although `Bottom` 'is a’ `Top`, `Box<Bottom>` *is not* a
>> `Box<Top>`.
>>
>> I am suggesting:
>>
>> 1. That `Box<Bottom>` should be a `Box<Top>` (covariance).
>> 2. An implementation that allows the above covariance.
>> 3. That protocols are made generic, i.e. `protocol Box<T> { var value: T
>> { get set } }` and that this mechanism replaces associated types for
>> protocols.
>>
>> // Proposal:
>> // 1. No change to Box, i.e. programmer would just write Box as
>> before
>> // 2. Code transformed by comiler with write check for each
>> specific, generic type instance
>> // Best approximation of resulting code in current Swift to
>> demonstrate spirit of idea:
>>
>> // Compiler writes a universal form using the upper bound (it writes
>> the underlyting representation).
>> // In practice this would be called `Box` but used `BoxAnyObject` to
>> indicate that it has a generic argument bounded by `AnyObject`.
>> struct BoxAnyObject {
>> // Generated from generic argument `<T: AnyObject>`.
>> let T: AnyObject.Type // Store the actual type.
>>
>> // Generated from stored property `var value: T` and noting that
>> `T`'s upper bound is `AnyObject`.
>> private var _value: AnyObject // Access the stored property
>> through a setter so that type can be checked
>> var value: AnyObject {
>> get {
>> return _value
>> }
>> set {
>> // In all functions check that args declared as `T` are
>> actually a `T` or a sub-type.
>> // Note: `is` only works with type literal and there is
>> no `>=` operator for types :(.
>> // `is` would need changing or `>=` for types adding,
>> nearest at moment `==`.
>> precondition(T == /* >= */ newValue.dynamicType, "Type
>> of newValue, \(newValue.dynamicType), is not a sub-type of generic type
>> T, \(T)")
>> _value = newValue
>> }
>> }
>>
>> // Generated from `init(_ initialValue: T)` and noting that
>> `T`'s upper bound is `AnyObject`.
>> init(_ lowestCommonDeclaredT: AnyObject.Type, _ initialValue:
>> AnyObject) {
>> T = lowestCommonDeclaredT
>> _value = initialValue
>> }
>> }
>>
>> // Demonstrate that all `Box`es are the same size and therefore can
>> be bitwise copied
>> // Compiler supplies lowest-common, declared, generic type for all
>> the `T`s in the `init` call.
>> var bT = BoxAnyObject(Top.self, Top()) // In practice user would
>> write `let bT = Box(Top())`.
>> bT.T // Top.Type
>> sizeofValue(bT) // 16
>>
>> var bB = BoxAnyObject(Bottom.self, Bottom()) // In practice user
>> would write `let bB = Box(Bottom())`.
>> bB.T // Bottom.Type
>> sizeofValue(bB) // 16
>>
>> // Demonstration covariance.
>> bT = bB // Compiler would check covariance of declared generic types.
>> bT.T // Bottom.Type
>>
>> // Demonstrate generic returned type
>> // Compiler would add cast to declared, generic type.
>> bB.value as! Bottom // In practice user would write `bB.value`.
>>
>> // Demonstrate type safety
>> bT = BoxAnyObject(Top.self, Top()) // In practice user would write
>> `bT = Box(Top())`.
>> bT.value = Top() // OK
>> // bT.value = Bottom() // Doesn't work at present because need `>=`
>> for types, but would work in practice
>> // bB.value = Top() // Runtime error - wrong type
>>
>> The implications of this proposal are:
>>
>> 1. The compiler can statically type check a read from a stored property.
>> 2. A write to a stored property is type checked at runtime.
>> 3. Protocols can be made generic instead of having an associated type and
>> then they become a proper type with dynamic dispatch.
>> 4. Generic protocols can be a type just like non-generic protocols,
>> structs, and classes and unlike associated type protocols that can only be
>> a generic constraint.
>> 5. The awkwardness of dealing with associated type generics is replaced
>> by a more powerful and easier to understand semantic of a type, just like
>> the other types.
>> 6. There is a lot of ‘non-obvoius’, long code, for example `inits`, that
>> use a `where` clause to constrain an associated type protocol, this would
>> be unnecessary.
>> 7. There are whole types, `AnySequence`, `AnyGenerator`, etc., that would
>> be replaced by a generic protocols, `Sequence`, `Generator`, etc.
>>
>> Advantages:
>>
>> 1. Covariant generics are a powerful addition to the language.
>> 2. Generics’ invariance are inconsistent with the rest of the language.
>> 3. Generic protocols would become a ‘proper’ type and you could have
>> arrays and fields of a generic protocol.
>> 4. There are many threads on swift-evolution looking at how protocols can
>> be made into a ‘proper’ type or at least a concept that is easier to
>> understand.
>>
>> Compatibility:
>>
>> 1. This would be a major change since associated types in protocols would
>> be replaced by generics.
>> 2. The new implementation of generics might break some existing `struct`
>> and `class` code, for example if it is dependent on the exact size of an
>> object because the class will have extra fields, one for each generic type,
>> and therefore will be larger.
>>
>> Disadvantages:
>>
>> 1. Major change.
>> 2. Object size increases.
>>
>> Thanks in advance for any comments,
>>
>> — Howard.
>>
>> PS This is part of a collection of proposals previously presented as
>> “Protocols on Steroids”.
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>>
>>
>
>
> --
> -- Howard.
>
>
>
--
-- Howard.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160114/b5d8e246/attachment.html>
More information about the swift-evolution
mailing list