[Pre-Pitch] Another go at "strong typedef" / alternative types / reduced-state types

(Warning: rambling stream-of-conscious mode while slightly tired)

The names of new keywords and identifiers are subject to bike-shedding.

1. Set-up Protocols

protocol HardRawRepresentable: RawRepresentable {
    init(rawValue: RawValue)
}

Just like RawRepresentable, but the initializer has to be non-failable (“init?” and “init!” are no longer options).

protocol RawProcessable: RawRepresentable {
    mutating func withMutableRawValue<R>(_ body: (inout RawValue) throws -> R ) rethrows -> R
}

Maybe someone will find a use for this outside of type-copies.

2. Type-copy Type Syntax

A “typecopy” puns another value type, either nominal (structures, enumerations, or other type-copies) or structural (tuples, and fixed-size arrays if added). I guess it’d be implemented like a structure with a single instance-level stored property or an enumeration without indirect and/or associated values. Unlike a type alias, a type-copy is considered distinct from its underlying type and conversions have to be explicit casts. It has no operations by default besides copying (which every type has); adaptations of the underlying type’s interface have to be explicitly copied, and can be selective.

typecopy NewType: SPECIAL-ATTRIBUTES UnderlyingType, DesiredProtocols {
    // Anything other nominal types have, except enumeration cases and instance-level stored properties.
    // Also have new syntax to copy parts of the underlying type’s interface.
}

All type-copies conform to RawProcessible; the particulars depend on the special attributes (which are conditional keywords).

The initializer that satisfies RawRepresentable is the type’s designated initializer. It cannot be defined by the user; only secretly by the system; it’s an error to introduce one anyway in the primary definition block, an extension, or a default implementation from an added protocol. Any user-declared initializer that doesn’t forward to another user-declared initializer must forward to the designated initializer.

2a. No Special Attributes

The type must define a method called

static func #map(rawValue: RawValue) -> RawValue?

I’m using “#map” as the name so we don’t take out any more identifiers from the user. (The existing ones, “self,” “init,” “super,” and “Self,” were already done by Objective-C.) The function must be pure relative to the input; it can call non-impurities, like “print,” but the return value must depend only on the argument and not any global state. The user cannot call this function, only the system (but it can be implemented with regular, accessible functions you do write); let’s call this accessibility “reallyprivate". The designated initializer is in the “init?” form and calls this special method for the initial state.

The underlying state is accessed with this stored property:

pp reallyprivate(set) var rawValue: RawValue

where “pp” is the type’s accessibility level.

The implementation of “withMutableRawValue” copies “rawValue” to a mutable copy, calls the closure passing that copy, then assigns “Self(rawValue: newCopy)!” to self, causing a runtime error if the initialization part fails.

2b. The “injective” Attribute

This means that the mapping function must be a total function, and not partial:

static func #map(rawValue: RawValue) -> RawValue

Every legal state of RawValue is usable in the new type. (It may not be stored in the new type; the mapping function can be surjective.) These type-copies also conform to HardRawRepresentable.

The main external effect of this attribute is that downcasts from the underlying type to the new type use unconditional-“as”. (Downcasts and cross-casts from a type that shares the same implementation type also use unconditional-as if all the downcast phase links also use unconditional-as.)

2c. The “selfIdeal” Attribute

This means that all approved input states map to themselves in the storage state, this changes the required secret method to:

static func #approve(rawValue: RawValue) -> Bool

It effectively acts like case [2a] where “#map” calls “#approve” and returns either the input for TRUE or NIL for FALSE.

The main external effect of this attribute is that downcasts (and the downcast phase of cross-casts) can be trivially done with direct type punning, assuming all the “#approve” calls AND to TRUE, instead of possibly longer “#map” calls.

2d. Using “injective” and “selfIdeal” together

This combination means every input state is accepted, and map to themselves; this is exactly a “strong typedef”. Such types conform to HardRawRepresentable. There is no special method required; the value is just copied in without wasting time calling the bijective identity function. And the “rawValue” property has its “set” just as public as its “get”. (Technically, you can change “rawValue” by setting it directly or calling “withMutableRawValue”.)

3. Why would I want this?

The original inspiration was my first fixed-size array ideas have a simple structural version and a complex nominal version. Then I thought it would be better to keep arrays simple and move the nominal stuff to a generalized concept of augmentation. I also read about the C++ guys trying to add this idea in.

(An inspiration was reading that C++ requires std::complex to have the same layout as T[2] and allow reinterpret-casts between them, for reading in a raw array of numbers and converting them easily to complex numbers. I was thinking Swift-y of reading in a fixed-size array of ten Double (i.e. [10; Double]), reshaping it to [5; [2; Double]], then map with pun-cast to [5; Complex<Double>]. Here, Complex<T> would be a type-copy of [2; T].)

I got additional inspiration by reading "Quotient Types for Programmers” at <http://www.hedonisticlearning.com/posts/quotient-types-for-programmers.html>. Type-copies can be used for subset types and quotient types (and weird combo subsetted quotient types).

This past autumn, Apple’s latest operating systems’ APIs changed their String-ly typed values for its Views, Notification Centers, and such to use manual versions of this idea. Maybe they could change them to this.

4. Detection

How would I know if a given type is a type-copy? The protocols generally can be used for regular work, so testing them won’t help. (That was a change from an earlier idea.) I realized that maybe we can move them to “Mirror”. Add a “`typecopy`” case to “Mirror.DisplayStyle”. Add an “underlyingTypeMirror” property like the super-class one; it would be NIL if the target type isn’t actually a type-copy. Maybe add a global “implementationType(of:)” function that returns the type-copy’s implementation type (The underlying type can already be found with “RawValue”.); should calling this on a non-typecopy return NIL or itself?

Maybe the actual printing could be like: “MyType punning(“ + Mirror of self.rawValue + “)”.

I guess you could check for HardRawRepresentable to suspect a type is a injective type-copy, but there’s no way to test for self-ideal type-copies. Would that be a problem? Could it be fixed (without a lot of extra compiler magic)?

5. Trampolines

I don’t have any new ideas on how to declare a type-copy is reusing an interface from its underlying type. However, the “withMutableRawValue” method now provides a way for copied methods (manually or using “publish” for automatically) to actually work.

···


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

1 Like