- Proposal: SE-NNNN
- Authors: Daryle Walker, Author 2
- Review Manager: TBD
- Status: Awaiting review
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
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.
- Use Stronger Types!
- Has anybody tried using strong typedefs? | Code | Handmade Hero Forums
- foonathan::blog() - Tutorial: Emulating strong/opaque typedefs in C++
- Function Aliases + Extended Inheritance = Opaque Typedefs
- The Go Programming Language Specification - The Go Programming Language, Type declarations
- A Gentle Introduction to Haskell: Types, Again, section 6.1
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 {
}
}
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.
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
.
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.
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).]
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.
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.
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 possiblyTi
) - (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.
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.]
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.
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.
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!
.
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.
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.
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.]
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].
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.
Add to the "Grammar of a Declaration":
declaration → alter-declaration
Add a new section "Grammar of an Alternative Declaration":
alter-declaration → attributes_opt access-level-modifier_opt alter alter-name generic-parameter-clause_opt type-inheritance-clause generic-where-clause_opt alter-body
alter-name → identifier
alter-body → { alter-members_opt **}**
alter-members → alter-member alter-members_opt
alter-member → declaration | publish-clause | compiler-control-statement
publish-clause → publish published-members-list
published-members-list → published-member | published-member , published-members-list
published-member → decimal-digits
published-member → type-identifier
published-member → identifier | identifier ( argument-names_opt )
published-member → operator | operator ( argument-names_opt )
published-member → init | init ( argument-names_opt )
published-member → init ? | init ? ( argument-names_opt )
published-member → init ! | init ! ( argument-names_opt )
published-member → subscript | subscript ( argument-names_opt )
published-member → default
Change a production of the "Grammar of a Superclass Declaration":
superclass-expression → super | superclass-method-expression | superclass-subscript-expression | superclass-initializer-expression
/**
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
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.
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.
The API will not be affected unless any too similar types are changed to alternative type sets.
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.