[early Pitch] retype (or newtype): strong type-alias (typedef) for redoing an interface; also: object aliases


(Daryle Walker) #1

In one of my earlier fixed-size array proposals, I had two kinds of array types: a compound type and a nominal type. I dropped the latter because there wasn’t too much of a value-add, especially once I copied its unique features to the compound type. It would be like a compound array property wrapped by a struct, except that member wouldn’t have a proper name so you would have to use “super” to refer to the inner object.

I have been looking up on how Rust handles its array/vector types, and I saw an issue on adding complex numbers. I then recalled that “long” ago I read up on C++’s complex numbers and how that class isn’t supposed to have any padding so you can alias a large array of double into a large-ish array of complex. That could have been resolved another way if we had a strong “typedef” so a new type would have the EXACT layout as another type without wrapping it in a struct and risking padding.

Over the past few weeks, I seen requests here for a strong type-alias. And requests for “I know that tuples are supposed to be protocol-less, but I want to (automatically) slap SuperReallyPeachyKeenProtocol on them anyway”. Now I think I can put all of these together.

Name a New Type Based on an Existing One:

“retype” IDENTIFIER “:” ORIGINAL-TYPE (“,” PROTOCOL )* “{“
    // Whatever, including exports…
“}”

If there is nothing in the braces, then the new type doesn’t any operations (besides the default assignment and “&” for function in-out parameters). You can get a reference to the object as the original type as “myNewObject.super”; it will have the same let/var status as the outer object’s declaration. Access to “super” lets you implement whatever new interface you want. Of course, you can implement new protocols added to the type list. The new type is implemented as the original one; no wrapping structs added; the same size, stride, and alignment.

You can republish parts of the old type’s interface. You use an “export OLD-NAME” declaration. There is also an “export default” declaration, which does:

* The application operator (i.e. “(whatever)”) for a function type
* The “.0”, “.1”, etc. member access for a tuple type
* All the original cases for an enumeration type
* Nothing for any other type

The default-export is optional for tuple and enumeration types, since you can export individual members or cases. Since there is no way to express the application operator in Swift, a function type’s retype has to use the default-export if it wants to export anything. All of the original type’s implementation of one of its direct or indirect protocols can be exported with “export TheOldProtocol”; but the new type won’t officially support the protocol unless it’s (directly or indirectly) in the new type's protocol list.

How do you initialize objects of this type? Besides using another object of the same type, you can use an object of the original type or of a retype that shares the implementation type as an initializer. If the initialization happens after the object’s declaration, then “expressionOfOtherType as MyReType” syntax has to be used. (Post-declaration initialization can use “myObject.super” instead.)

Now, I can do something like (assuming fixed-size arrays are added):

retype Complex<T: SomeNumericProtocol>: [2: T], Equatable /*, etc.*/ {
var re: T {
get { return super[0] }
set { super[0] = newValue }
}
var im: T {
get { return super[1] }
set { super[1] = newValue }
}

init(_: UninitializedFlag) { // “UninitializedFlag” is a library enum type with case “.uninitialized"
super = [] // Leave uninitialized
}
init(real: T = 0, imaginary: T = 0) {
super = [real, imaginary]
}
//…
}

func someFunc() {
let scalars: [_: Double] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
let rawComplexes = scalars as [5: [2: Double]] // Reshape command
let firstComplex = rawComplexes[0] as Complex // May have to use “Complex<Double>” if the “as” is too indirect.
//...
}

Object Aliasing

But what if I want to work on those scalars directly? What if I want to mutate my complex number objects and update the scalars too? What if we add a “pose” declaration, at the same level as “let” and “var”?

var scalars: [_: Double] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
pose rawComplexes: [5: [2: Double]] = scalars // OK as they have the same stride and inner non-array type
pose complexes: [5: Complex] = rawComplexes

complexes[0].re = -11
assert(scalars[0] == -11)

The source of a pose has to have an equal or greater scope (and lifetime) than the alias. (Instance- and type-level properties of a type definition are considered separately. An instance-level property can pose over a type-level one. A function-scope object can pose over a function argument, as long as it doesn’t try to persist after function-end.) A pose has the same let/var status as its source. (The source may not be directly marked as “let” or “var,” it may be a “pose” itself.) A pose always has to be initialized at declaration time with its source.

[Note: the “pose” idea sprang into my head while writing the “retype” one. So you can ignore “pose” if you want to focus on “retype” first.]

···


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com


(Daryle Walker) #2

I didn’t add a SuperReallyPeachyKeenProtocol example. It assumes we eventually add variadic generics:

retype TupleCollection<T, U…>: (T, …U), RandomAccessCollection allwhere U == T {
export default // Without this, instances of “self” below would be “super” instead.

var head: T {
get { return self.0 }
set { self.0 = newValue }
}
var tail: (…U) {
// Put some cool template meta-programming here, thanks.
}

subscript(index: Int) -> T {
get { return index == 0 ? self.0 : tail[index - 1] }
set {
if index == 0 {
self.0 = newValue
} else {
tail[index - 1] = newValue
}
}
}
//…
}

// Put terminal specialization of TupleCollection<> here.

//…

func myFunc2() {
var myTuple: (Int, Int, Int, Int) = (0, 0, 0, 0)
pose myCollection = myTuple as TupleCollection

for i in myCollection {
i += 2
}
print(myTuple) // (2, 2, 2, 2)
}

Now you don’t have to permanently add subscripting to tuple types.

[Oh, instead of expanding a parameter pack with U…, I automatically expand it with “where” variants where-any and where-all. The first considers each member of the pack separately and then applies logical-OR to the result (default to FALSE when empty). The second does a logical-AND (defaulting to TRUE when empty). The where-any marker could be renamed to “anywhere,” but that would need an “allwhere” to be consistent. Turns, out “allwhere” is a word, albeit archaic!]

···


Daryle Walker
Mac, Internet, and Video Game Junkie
darylew AT mac DOT com