[swift-evolution] [Potential Idea] How to indicate a trivial strong-typedef?

Daryle Walker darylew at mac.com
Wed Nov 1 09:21:28 CDT 2017

Here’s a quick review of a strong-“typedef” idea I’ve mentioned here:

typecopy MyNewType: OriginalValueType, AnyProtocolsIWant {
// Direct member declarations
// “publish MemberFromOriginalType” for all overloads (or the singular item for non-functions)
// “publish MemberWithSignatureFromOriginalType” to pick out one subset of overloads
// “publish MemberWithSignatureAndParameterNamesFromOriginalType” to pick out one overload
// “publish ProtocolThatOriginalTypeConfromsTo"

(The name “typecopy” is changed from “alter”.)

All type-copies automatically conform to the new system AnyTypeCopy protocol, which derives from RawRepresentable. It just adds the “CoreRawValue” type. This type alias matches RawValue if that is a non-typecopy; otherwise it aliases that type copy’s CoreRawValue type. AnyTypeCopy cannot be added to non-typecopy types. Any protocol that derives from it can only be applied to typecopy types.  Type-copies are value types, and can only shadow structures, enumerations, other type-copies, tuples, and fixed-size arrays (if added).

The initializer used to conform to RawRepresentable is the type’s designated initializer. It’s the only one that can access “super.” Any other initializers added must be convenience and reference the designated one. (Published initializers are considered convenience.) The designated initializer can be fail-able to model subtypes. The designated initializer can pass on an altered value to model quotient types (or any other crazy mapping scheme); including being fail-able too (i.e. a quotient type of a subtype).

Of course, the designated initializer could do neither changes nor filtering:

typecopy MyResourceID: Int16, Hashable {
init(rawValue: RawValue) { super = rawValue }
publish Equatable
var hashValue: Int { get {return rawValue.hashValue & 0x01} }

Since every valid state of Int16 is also a valid state of MyResourceID, and without remapping, this type-copy is a trivial copy of its source. Besides downcasts (and cross-casts) never being fail-able, all sorts of other type-punning and related optimizations should be possible. (Upcasts are always trivial, even when the designated initializer isn’t.) We should be able to easily copy/type-pun between Array<MyResourceID> with Array<T>, where T is either Int16 or another type-copy that (eventually) trivially shadows Int16. (Of course, this applies to Unsafe*Pointer or fixed-size arrays (if added) too.) These optimizations can’t be done with non-trivial type copies, since downcasts (and the downcast parts of cross-casts) need to have their designated initializers called for each element.

1. Now we get to the question in the Subject. Before now, I just wanted the compiler to see the definition of the designated initializer to determine if the type-copy is trivial or not. Now I realize that may have problems. One is that it may not be easy for the compiler to check if a given initializer matches the form I gave in the “MyResourceID” example. Another is that if the user prints out a prototype summary of the type, with members but without their definitions, s/he would have no idea if the type-copy was trivial or not.

I’m wondering if we should define trivial-ness with a keyword added to the definition:

typecopy MyResourceID1 trivial: Int16, Hashable {
// Initializer not given
publish Equatable
var hashValue: Int { get{return rawValue.hashValue & 0x01} }

typecopy MyResourceID2: Int16, Hashable {
trivial init(rawValue: RawValue)  // Looks incomplete, but it’s actually the full thing
publish Equatable
var hashValue: Int { get{return rawValue.hashValue & 0x01} }

typecopy MyResourceID3: Int16, Hashable {
init(rawValue: RawValue) = trivial
publish Equatable
var hashValue: Int { get{return rawValue.hashValue & 0x01} }

I kind-of like the look of the second, but it may be confusing to (new) users since there’s no code block after the initializer declaration. My next-liked look is the first one, but I’m not sure where the “trivial” should go (before “typecopy”, after it, current spot, or before the original type’s name after the colon). The third choice looks the weirdest to me, but it’s probably the easiest parsing-wise. It does remind me of the C++ class built-in life-management overrides.

(Now completing the post, maybe option 3 isn’t the easiest to parse. We go from “init” then Code-Block to “init” then either Code-Block or “= trivial”.)

For actual use, I’m leaning on placing “trivial” right before the original type’s name but after the colon. What does everyone else think?

1a. If “trivial” is added to the “typecopy” line, should explicitly defining a designated initializer anyway be banned? I’m leaning towards yes.

2. If no initializers are declared, either directly or with a publish, then a designated initializer is automatically added. It would have the same form as the one I gave in the example. If “trivial” goes inside the initializer’s declaration instead of the “typecopy” line, then the initializer is declared trivial (if possible). If no direct initializers are given, but at least one is added through a “publish,” should an automatic designated initializer still be synthesized, or should the user be forced to declare one?

If we’re going to use “trivial” on the “typecopy” line and ban explicitly defining a designated initializer anyway, then the user shouldn’t (and can’t(!)) be forced to declare one. Otherwise, I think the user should be forced to manually declare the designated initializer if any others are either published or directly declared. It would get weird in one case: if there are no initializers directly declared or published, a protocol is published, and then that protocol is changed to add an initializer; suddenly the user would have to ensure a designated initializer in their code.

3. I finished all of the preceding text, and realized a new problem. There’s no way to determine if a given type-copy is trivial in code, in a meta-programming way. If there’s a way for a derived protocol to restrict how its parent’s members are expressed, then we could define a AnyTrivialTypeCopy as a sub-protocol, but only if we can update the “init?(rawValue: RawValue)” to always be “init(rawValue: RawValue)”. (AnyTrivialTypeCopy would be automatically added to type-copy types that are trivial. It can be manually added to parameters or protocols, but not types unless they already qualify.) Or we could have an “isTrivial” type-level constant Bool property. A separate type-traits helper, like the one for memory layout? Other ideas?

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

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171101/08f16789/attachment.html>

More information about the swift-evolution mailing list