Skip to content

Instantly share code, notes, and snippets.

@CTMacUser
Last active July 29, 2017 05:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CTMacUser/c493f775075e946efdcfd85d38473291 to your computer and use it in GitHub Desktop.
Save CTMacUser/c493f775075e946efdcfd85d38473291 to your computer and use it in GitHub Desktop.
Swift proposal for strong type-aliases

Alternative Interface Value Types

Introduction

This proposal adds a nominal type whose structure is an existing value type and interface can be customized, including selective use of the original type's interface. It is equivalent to the type declaration feature in Go, newtype in Haskell, and the oft-requested "strong typedef" in C(++).

Swift-evolution thread: Discussion thread topic for that proposal

Motivation

There are two main kinds of repurposing. Types in unit libraries (meters, kilograms, etc.) need most of the operations of a default numeric type. Resource-obtaining functions may represent a handle to their data as a integer or pointer type, which is passed to resource reading/writing/deallocating functions.

A type can be repurposed with a typealias declaration. But the alters are synonyms for the implementation type; the operational interfaces of the implementation type and all of its alters are shared, without compiler-level enforcement of keeping mixed sets of object types from interacting. This also means the types cannot be used for overload resolution in function or generic arguments.

A way to make related type that counts as a separate type in terms of overload resolution is to declare the alter to be of a wrapper around the original type. The wrapper contains an object of the implementation type, while the wrapper's interface copies to some degree the implementation type's interface, using forwarding functions, called trampolines, to the implementation type's versions. The degree of copying depends on the reason of repurpose. An wrapper for a unit type would want to keep most of the numeric interface, except a type only interfaces with itself, and not the implementation type or co-alters. A resource handle doesn't use the implementation type's interface for anything, so it would forward only assignment and maybe comparisons.

Languages differ on how to set up trampolines. Since C++ has no code-level concept of an interface group, its programmers have to manually write trampoline functions for each interface to be copied over, and the procedure differs for built-in and class types. Go and Haskell do have interface groups, like Swift protocols, so trampolines for the implementation type's interface can be set much quicker. It's automatic for Go, based off the original's method set, and semi-automatic for Haskell with a specification list. C++ would work better for a handle-type purpose, and the other two for unit-type purposes.

A downside is that the compiler may differ between the various kinds of types (records, enumerations, built-ins, etc.) on how it lays out data or sets up base code. If the original type and wrapper code get different policies, then the wrapper isn't transparent from the object code side and the performance of that difference may be critical.

A built-in way of repurposing types could be more efficient than a library-level wrapper. Since we have protocols, we could take advantage of that when specifying what interfaces to carry over, hopefully without Go's automatic and full-coverage trampoline policy but more automation than Haskell.

References

Proposed solution

An alternative declaration introduces a named value type into a program. Alternative declarations are declared using the alter keyword and have the following form:

alter alternative-name : underlying-type , adopted-protocols { declarations }

The body of an alternative contains zero or more declarations. These declarations can include computed and/or type properties, instance methods, type methods, initializers, subscripts, type aliases, and even other structure, class, alternative, and enumeration declarations. Alternative declarations can't contain stored instance properties or deinitializer or protocol declarations.

An alternative type bases its layout from exactly one type, its underlying type, but can adopt any number of protocols. The underlying type appears first after the alternative name and colon, followed by any adopted protocols. The underlying type may be any value type, named or compound. This includes structures, enumerations, other alternatives, and tuples (and fixed-size arrays if added). Generic alternatives can be based on other generic and non-generic types, but a non-generic alternative can only be based on other non-generic types. When you specify a generic named type after the colon, you must include the full name of that generic type, including its generic parameter clause. When a compound type is the underlying type of a generic alternative, that compound type may use any accessible generic parameters as part of its structural description.

enum Outer<T: Equatable> {

    // Indirectly make a generic tuple....
    alter Inner<U: Collection>: (T, (T, T) -> U) where U.Element == Bool {
    }

}

Marker Protocol

Every alternative conforms to the AnyAlternative protocol:

protocol AnyAlternative: RawRepresentable {
    associatedtype CoreRawValue
}

The alternative's underlying type is set as the associated RawValue type (inherited from RawRepresentable). An alternative is structurally the underlying type; using the same size, stride, and alignment requirements; with a different interface on top.

A simple alternative type is an alternative that has as its underlying type a non-alternative type. The implementation chain for a simple alternative type is a list consisting of the alternative type itself followed by its underlying type. The implementation chain for a non-simple alternative type is the alternative prepended to its underlying type's implementation chain. An alternative is considered to be at the shallow end of its implementation chain. The non-alternative type at the deep end of an implementation chain is considered the implementation type for all the alternatives in the chain. The implementation type of an alternative is accessible from the latter with its associated CoreRawValue type.

Like AnyObject, the AnyAlternative protocol cannot be applied to a non-qualifying type by the user. But it can be inherited in a protocol declaration to force conforming types to be alternatives.

Designated Initializer

The initializer required to conform to AnyAlternative is an alternative's designated initializer. It can be a failable initializer per RawRepresentable or instead be implemented as an implicitly unwrapped failable initializer or a nonfailable initializer.

The implementation of the designated initializer must initialize the instance as if it was of the underlying type. The code of the initializer must end up doing one of the following:

  • Call super.init with the desired initializer of the underlying type.
  • Assign to super to assign-initialize the underlying type.
  • Return nil to cause the initializer to fail.
alter Test1: Int {
    init?(rawValue: RawValue) {
        guard let t = RawValue(exactly: rawValue * 0.1) else { return nil }
        super = t  // Reached if rawValue is a multiple of ten
    }
}

alter Test2: Int {
    init(rawValue: RawValue) {
        super.init(exactly: rawValue / 10.0)!
    }
}

[Grammar Note: Assignment-initialization during an alternative's designated initializer is the only time super can appear by itself in an expression, without a member-specifying expression following it.]

Like in classes, an alternative's designated initializer is the only initializer that can have references to super.

Automatic Definition

If no initializers are defined, then a designated initializer is automatically defined as if it was:

init(rawValue: RawValue) {
    super = rawValue
}

A basic alternative type has a designated initializer that either is automatically defined or is explicitly defined but can be determined by the compiler to be equivalent to the could-have-been automatically-defined initializer. A trivial alternative type is an alternative where itself and all other alternatives in the former's implementation chain are basic.

The code for a basic alternative's designated initializer shall be elided for direct initialization of the instance with the raw input value. The effect cascades, essentially calling the designated initializer of the shallowest alternative in the implementation chain that isn't basic, or initialization of the implementation type if the alternative is trivial.

Pure Evaluation of the Raw Value

The value ultimately stored in an instance of an alternative must be based solely on the raw input argument (i.e. the equivalent of a pure function). In other words, a given argument value must either always cause an initializer failure or always map to the same stored value.

[Note: This stipulation isn't technically required, but I like it so we can use alternatives to implement subtypes and quotient types. Those can't be done if the mapping is unstable due to global state. It can't be enforced by the compiler (AFAIK) and I don't want to ban inconsequential impurities like print statements. I guess a stability requirement when the initializer has no impurities could help with caching the mapping (and therefore the code of the designated initializer).]

Casts

An instance of an alternative can be upcast to any type in the alternative's implementation chain with unconditional as. The result is the same as calling the rawValue property (from RawRepresentable) if the destination is the underlying type, or using that property repeatedly for other result types.

Given an alternative type T and any non-identical type U in the implementation chain of T, an instance of U can be downcast to T by first calling the designated initializer of the alternative immediately shallower than U, then cascading with wrapping designated initializer calls down to T. A conditional as? cast can be used, returning nil the first time a designated initializer in the calling chain fails. A conditional as! cast can be used, implemented by forced unwrapping of the result of as?. Only if every designated initializer in the calling chain is either nonfailable or implicitly-unwrapped failable can an unconditional as be used.

Two alternatives with the same implementation type but are not in the same implementation chain can be cross-cast to each other. The cross-cast is implemented as if it was an upcast from the source type to the shallowest common type in both alternatives' implementation chains followed by a downcast to the destination type. Both as? and as! are supported. Unconditional as is supported if the downcast phase supports it.

alter Test3: Test2 {
    // Use automatically-defined designated initializer
}

let a = Test3(rawValue: Test2(rawValue: 20))
let b = a as Test2
let c = a as Int
let d = c as? Test1  // Not nil
let e = Test1(rawValue: 30)! as Test3

[An upcast should be implementable as an assignment/copy without any extra code. A downcast is also that simple if all the required initializers' owning types are basic. A non-basic alternative in the downcast chain would require fuller code.]

The implementation(of:) global function performs an upcast of its argument to the argument's type's implementation type. The argument type has to be an alternative. This function lets you generate values of the implementation type without having to explicitly write out the name of that type.

Direct Members

Alternatives support any kind of member that is common to both structures and enumerations; neither stored instance properties nor enumeration cases are allowed. An alternative cannot define members that would interfere with conformance with AnyAlternative (or RawRepresentable).

The definition of a member can read the current state as an instance of the underlying type with rawValue. The state can be written with assignment to self.

[Future Direction: allow mutation of self, in whole or in part, through rawValue. Maybe only as a private(set)?]

Like for classes, any initializer besides the designated one is a convenience initializer and must be marked with the convenience modifier. A convenience initializer must either call the designated initializer or another convenience initializer that eventually calls the designated one.

Copying the Underlying Type's Interface

A trampoline member that forwards to the underlying type's equivalent can be declared in a publish clause. The trampoline for a published member has the same name as the underlying type's version and is treated like a direct member. Name conflicts between a published member and a direct member work the same as conflicts between two direct members of the same kinds.

Certain members of the underlying type may refer to that type within their own type. In some circumstances, the trampoline will use the alternative type in place of the underlying type. This cannot be done outside of simple cases, so for anything else the item's type would stay as-is. (The workaround is to manually define the trampoline and have its implementation call the underlying type's version. Depending on the applicable overload resolution rule, you may not be able to publish the underlying type's version too.)

With a given underlying type U, replacement can occur with properties with types:

  • U
  • X?
  • X!
  • [X]
  • [X: T]
  • [T: X]
  • [X1: X2]
  • Tuples with various members of types Xi (and possibly Ti)
  • (If added, fixed-size arrays with element type X)

where X, X1, X2, Xi are types that recursively match the list and T and Ti don't. When reading from the underlying type's version, sub-objects of type U are downcast to the alternative type. Any casts use as? if the result is an optional, returning nil if any sub-object conversion fails. Otherwise the cast uses as!. When writing to the underlying type's version, sub-objects of type U and U! are upcast from the alternative type and sub-objects of type U? are translated to nil if the source is nil and upcast otherwise, all using unconditional as as appropriate.

[Trampolines shouldn't be as code-extensive as they initially appear. If the involved alternatives are basic or close to it, then a lot of the downcast code can be elided and the compiler can inline a call to the original implementation of the function.]

Replacement can also occur with routines (functions, methods, initializers, and subscripts) that use the above property types for parameters and/or return types. Arguments are upcast on the way in and return values and inout arguments are downcast on the way out, using the same translation rules as for properties above.

A publish clause consists of the keyword publish followed by a comma-separated list of protocols and/or member names. Routine names may include parameter labels or parameter labels and types. It is an error to mention a protocol that the underlying type does not conform to, or a member that the underlying type does not have. Compiler-control statements of the underlying type cannot be published. (If some declarations in the underlying type are conditional, then the alternative has to use the same compilation-control structure to publish them, either directly for individual publishing or indirectly if a member group is published and the compiler-control statements are completely within the group.)

If a member is published more than once, the subsequent publishings are ignored. (Besides direct duplicate publish-clauses, some clauses designate groups and therefore intersecting members would get published multiple times.)

Members cannot be published in extensions.

Published Types

Publishing a type or generic type adds a (generic) type-alias to a post-replacement translation of the underlying type's version. Note that types that are instantiations of generic types that use the underlying type as an argument and are neither Array nor Dictionary are not pierced; they are aliased without translation. [For all the compiler knows, the generic type could differentiate between alternatives and non-alternatives (by checking for AnyAlternative conformance). We know that Array and Dictionary won't.]

[Future Direction: add replacing-translation for Set and any other generic container types from the standard library.]

Published Properties

Publishing a type property from the underlying type creates a computed type property in the alternative type with the same mutability and of the replacing-translation type.

Publishing a instance property, labeled tuple member, or anonymous tuple member creates a computed instance property with the same mutability and of the replacing-translation type. Published tuple members are always mutable. Anonymous tuple members' names are integer literals; these names are not allowed for direct member declaration, but can be published to other alternatives.

Published Routines

Publishing a routine with its parameter labels and types specified creates a new routine with the original's signature after replacing-translation. The new routine calls the old, translating the arguments and return value as needed. When the routine is mutating, the underlying type's version is called on an upcast copy of self, translating the arguments/return-value as needed, and then self is assigned the copy downcast using as!.

Publishing a routine with its parameter labels but not types publishes each routine that can overload the name and parameter labels.

Publishing a routine with only its name publishes each routine that overloads the name.

When publishing an initializer, its trampoline is a convenience initializer for the alternative.

Published Cases

Publishing an enumeration raw-style case adds a type property of the alternative type with the value of the enumeration case downcast with as!. Publishing an enumeration union-style case adds a type method returning the alternate type and taking as parameters the types of the case's tuple, in order and with a label matching the case's member's label (or label-less if the case member was label-less); the return value is the corresponding case initialized with the corresponding members, then downcast with as!.

Published Protocols

Publishing a protocol publishes all members in the underlying type that provide conformance to that protocol. Since the types within member declarations are translated (for the most part), protocol signatures that refer to Self still do from the alternative's perspective.

An alternative automatically conforms to AnyAlternative and that protocol's base protocols, so none of them are allowed to be directly published. If any of those protocols are indirectly published by publishing a protocol that inherits from any from them, any conflicting members are ignored instead of published.

Published Defaults

Using default as the member key publishes a predefined set of members, dependent on the underlying type:

  • The default for a tuple underlying type is all of its anonymous tuple members.
  • The default for an enumeration underlying type is all of its cases.
  • The default for an alternative underlying type with a implementation type that is a tuple or enumeration are the anonymous tuple members or enumeration cases still published by the underlying type.
  • The default is empty for any other underlying type.

Protocols

An alternative's protocol conformances are independent of those of its underlying type. The type can use directly-declared or published members to fulfill a protocol, or a mix if the protocol has multiple members.

alter MyResourceHandle: Int16, Hashable {

    publish init(integerLiteral:)

    publish Equatable

    var hashValue: Int {
        return 2000 &+ rawValue.hashValue
    }

}

[Recall that a trampoline member for a protocol may have the wrong type to match again for the alternative if the original's type is too complex. Manual intervention with a direct member would be required.]

Detailed design

ABI Considerations

At the bitcode level, an alternative is represented the same as its implementation type. Features like new members are implemented as associated functions.

// This is represented as <{ i64, i8 }> in LLVM
struct S {
    var x: Int
    var y: UInt8
}

// So are these
alter S1: S {
    // Even if nothing is published
}

alter S2: S {

    publish x

}

alter S3: S2 {
    publish x
    var y: UInt8 {
        get { return rawValue.rawValue.y }
        set { var ss = self as S ; ss.y = newValue ; self = ss }  // Need to work on this....
    }
}

For in-program Swift accounting, there is a new kind of metadata record for alternative types. It'll be similar to the metadata for structures, except the section for fields is replaced by a reference to the underlying type's metadata. A new kind of nominal type descriptor is needed too [but I'm not sure what, if any, alternative-specific data needs to be put in it].

API Considerations

An unsafe pointer to an alternative is compatible with unsafe pointers to the implementation type and with unsafe pointers to other alternatives that share that implementation type.

Grammar

Add to the "Grammar of a Declaration":

declarationalter-declaration

Add a new section "Grammar of an Alternative Declaration":

alter-declarationattributes­_opt­ access-level-modifier_­opt ­alter alter-name­ generic-parameter-clause_­opt­ type-inheritance-clause­­ generic-where-clause­_opt ­alter-body­

alter-nameidentifier­

alter-body{ ­alter-members_­opt **­}**­

alter-membersalter-member­ alter-members_­opt­

alter-memberdeclaration­ | publish-clause | compiler-control-statement

publish-clausepublish published-members-list

published-members-listpublished-member | published-member , published-members-list

published-memberdecimal-digits

published-membertype-identifier

published-memberidentifier | identifier ( argument-names_opt )

published-memberoperator | operator ( argument-names_opt )

published-memberinit | init ( argument-names_opt )

published-memberinit ? | init ? ( argument-names_opt )

published-memberinit ! | init ! ( argument-names_opt )

published-membersubscript | subscript ( argument-names_opt )

published-memberdefault

Change a production of the "Grammar of a Superclass Declaration":

superclass-expressionsuper | superclass-method-expression | superclass-subscript-expression | superclass-initializer-expression

Library Support

/**
    The protocol to which all alternatives implicitly conform.

    It inherits from `RawRepresentable`, which provides the interface to convert to and from the alternative's underlying type.  This protocol adds an association to non-alternative type used to implement the conforming type.

    There is an extra condition added to the initializer `RawRepresentable` requires.  The connection between the initializer's argument and the stored value or if it returns nil instead must be pure; any specific value for the input must give the same output and not change between calls.

    This protocol cannot be explicitly applied to any types, but can be added to a protocol to ensure its conforming type is an alternative.
 */
protocol AnyAlternative: RawRepresentable {

    /// The type used to implement the conforming type.  When `RawValue` is a non-alternative type, this is that same type.  Otherwise, it aliases the raw type's implementation type.
    associatedtype CoreRawValue

}

/// - Returns: The argument `of` upcast to its type's implementation type.
func implementation<T: AnyAlternative>(of: T) -> T.CoreRawValue

Source compatibility

Besides making alter and publish keywords and AnyAlternative, CoreRawValue, and implementation(of:) library symbols, the changes are additive. The keywords should be conditional if possible.

Effect on ABI stability

As a new kind of type is being added, the changes to the ABI should be only additive.

If the standard library has sets of structurally identical types that either should be distinct for overload resolution but not, or made distinct by a wrapping structure or enumeration, then those can be replaced with alternative types, and those API and ABI will change accordingly.

Effect on API resilience

The API will not be affected unless any too similar types are changed to alternative type sets.

Alternatives considered

An alternative would to not add alternative types. Then code will keep using type-aliases, wrapping structures or enumerations, and re-implementing structures. All of these are weaker than layout-identical but overload-distinct types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment