[swift-evolution] Proposal: Automatic Wrapper Synthesis / "deriving"

plx plxswift at icloud.com
Sat Dec 5 09:57:37 CST 2015


I definitely got off on the wrong foot by using `deriving`; what I was trying to ask for is more of a stronger typealias (perhaps `newtype`-esque is the right analogy?), which — at least for my purposes — would *ideally* have the following capabilities / rules:

- when Y is a wrapper around an X, Y has only a single, “synthesized" stored property (of type X)
- the user can control the name of the stored property by one of these mechanisms (which one is used is moot to me):
  - override the name of the synthesized stored property
  - indicate another name and have an accessor auto-generated
- every protocol adopted by X *can* be auto-synthesized for Y 
  - but the user has to explicitly request each specific protocol be synthesized…
  - …because it’s very important that it be possible to *not* adopt every one of X’s protocols
- the user can optionally provide some predicate (of type (X) -> Bool), which then:
  - is included as an assert/precondition inside a synthesized `init(_ x: X)` 
  - is perhaps/can-be used inside some synthesized `init?(_ x: X)` (this is trickier at present, and might be better as a static func)
  - is ideally available on some methods like `func isInvariantSatisfied() -> Bool; static func isWrappableValue(x: X) -> Bool` (e.g. for debugging/testing)
  - …are essentially for documentation and some very modest runtime verification
- there’s some standard/principled way to do type-erasure "back down to X” as-needed

Only supporting single-value-wrappers should make the needed syntheses essentially trivial as far as I can tell; the only obviously non-trivial aspect is that, in practice, it’d seem likely that you’d want to occasionally “override” the typealiases from X with wrappers thereof; e.g. if you had a type Y wrapping Set<Foo>, you might want Y’s `Index` to be a wrapper around `SetIndex<Foo>` and *not just* `SetIndex<Foo>`; this doesn’t seem overly-complicated semantically but it isn’t “trivial”, either, and would perhaps need additional syntax support to read nicely in-use.

Here are a few concrete example motivating the above requests:

## UI-Level Section/Item Indices

It’s a nice — albeit minor — improvement to work with Int-wrappers like `SectionIndex` and `ItemIndex` rather than as primitive, interchangeable `Int` everywhere. It’s particularly handy if trying to write reasonably-generic implementations of the common UI-level datasource/delegate protocols.

For this:

- it’s nice to have the wrapped value accessible as `index` (as opposed to e.g. `wrappedValue` or `storage`, etc.)
- it’s nice to *not* wind up with the arithmetic operations from `Int`:
  - accidental use of e.g. `*` or `+` or `-` (etc.) on `SectionIndex` or `ItemIndex` is *probably* an error!
- it’s nice to *not* wind up as `IntegerLiteralConvertible`:
  - hard-coded literal indices are rarely advisable in application code!
  - being `IntegerLiteralConvertible` reintroduces transposition-error risk!
- it’s nice to have the constructors assert that the wrapped indices aren’t:
  - == NSNotFound
  - negative 

…and although this can be done by hand, it’s an example of a use that *could* be synthesized.

## URL Segregation

It can be moderately useful to have a bunch of lightweight URL-wrapper types to avoid transposition errors and also to ensure appropriate handling (e.g. using the appropriate URL session); picture, say, `IconImageURL`, `HeroImageURL`, `DetailImageURL`, `APIEndpointURL` (etc.).

If you *did* go this route, some standard way of doing "type-erasure” down to the underlying type can be useful:

    class ActiveTransferMonitor {
      private var activeTransfers: [URL:TransferMetrics] = [:]

      func didBeginTransfer<T:ValueWrapperType where T.WrappedValue: URL>(transferURL: T) {
        self.activeTransfers[transferURL.URL] = TransferMetrics()
      }
    }

…but this is the example I feel least strongly about (it’s just here to motivate the “standard type-erasure mechanism” aspect).

## Bluetooth

For Bluetooth LE, you have peripherals, which have *services*, which contain *characteristics*, which can have associated *descriptors*. Each service, characteristic, or descriptor is identified by a unique identifier, which is modeled in CoreBluetooth as instances of the `CBUUID` class.

It’s handy here for correctness to have distinct wrapper types, let’s call them `ServiceUUID`, `CharacteristicUUID`, and `DescriptorUUID`, both to guard against simple transposition errors and to make some internal APIs more self-documenting.

It’s handy here for readability to have the wrapped UUID be accessible as, e.g. `.UUID`, and not as some generic `.wrappedValue`:
  
    extension CBPeripheral {
      
      func locatePrimaryService(serviceIdentifier: ServiceUUID) -> CBService? {
        return self.services?.firstElementSatisfying() {
          $0.UUID.isEqual(serviceIdentifier.UUID)
        }
      }
  
    }

At present, the easiest way to get these all setup was as-follows:

- create a protocol like `BluetoothEntityUUIDType` that:
  - also adopts the protocols it should
  - has as many default implementations as possible
- create a struct for `ServiceUUID` that:
  - wraps a single CBUUID instance 
  - conforms to `BluetoothEntityUUIDType`
  - manually adds any missing conformances / logic
- create files for `CharacteristicUUID` and `DescriptorUUID`
  - copy-and-paste the definition of `ServiceUUID` into the `CharacteristicUUID` file
  - find-and-replace `Service` with `Characteristic`
  - copy-and-paste the definition of `ServiceUUID` into the `DescriptorUUID` file
  - find-and-replace `Service` with `Descriptor`
  - edit the `init` (etc.) for `DescriptorUUID ` to add asserts that the passed-in `CBUUID` is one of 6 allowed, pre-defined CBUUIDs applicable to CBDescriptor

…it works, it’s not too terrible as a one-off, but it’s also tedious and runs the risk of implementation drift (especially due to `CBDescriptor` having validation logic the others don’t) in the event that the initial copy-paste-edit cycle needs to be repeated.

Likewise, there’s small things that are nice utilities to tack-onto the various identifier types (utilities to check if they are some well-known ID) that risk getting accidentally blown away if the copy-paste-edit cycle gets repeated; these *can* be defined in other files, but that in turn *can* require “unnatural” choice of visibility (in some cases).

## Closing Comments

For anyone who read this far, thanks for taking the time. I hope the concrete examples helps shed some light on both the motivation and the intended scope for this feature request.

The common theme in the examples is that in the individual use, the amount of wrapping is small enough that:

- it can be done by hand
- there are not enough wrappers to really justify writing a code-gen tool…
- …especially since (in practice) many of the wrappers benefit from small “customization” here-and-there
- there are readability-and-correctness benefits to having the wrappers available

…but I think having wrapper-synthesis — or a functional equivalent — as an easily-accessible part of the language would open the door more broadly to writing in this style, in ways which aren't always cost-or-time-effective today.

Finally, as I keep mentioning, it’s entirely possible that improvements to the type system will make protocols + default method implementations able to get this features’ benefits without requiring any additional support from the language or tooling; I’d be happy-enough with that outcome also.

Thanks again for the time and feedback. If there’s enough community interest I can prepare a formal proposal, otherwise I’ve said everything I have to say on this idea.

## PS: Product-Type Handling

I honestly think that handling product types with deriving is hard-enough it’s not really worth trying to do; there’s certainly some limited value in being able to synthesize (component-wise) `Equatable` and so on, but once you get past the very foundational protocols like `Equatable` / `Comparable` / `Hashable` / `CustomStringConvertible` / `CustomDebugStringConvertible` you start hitting tricky problems it’s not clear .

EG: how do you synthesize a CollectionType implementation for, e.g., a product of two `CollectionType` types? Sure, you can safely bet the type of the synthesized Element is going to be the product of the component elements, but how do you plan to synthesize the implementation of a proper `ForwardIndexType` from the product of the component collections' `ForwardIndexType`s? There’s a lot of ways to do this, with different resulting iteration ordering and different performance/resource tradeoffs, and it’s hard to do auto-synthesis here that doesn’t either just “pick a winner” (and leave you hanging if you wanted a different implementation) or wind up introducing a mini-language to let you indicate which variant it should synthesize.

Moreover, note that even for, e.g., `Hashable`, there are actually at least a couple common strategies I often see.

One is the `xor`-everything strategy, except:

- it’s IMHO a good idea to rotate some of the constituent hashes to avoid accidentally hashing a lot of stuff to zero
- you don’t always want to hash *everything*
- you may want/need to hash transformed/normalized forms of constituent values, not the “raw” values

Another is the “forward to a good identifier” strategy, e.g.:

- you have a type like `struct ContentMetadata { let identifier: ContentIdentifier; let metadata: [String:MetadataItem] }`
- a good `hashValue` here can be had by simply forwarding the hashValue for `identifier`

…and at least IMHO, in a perfect world, if you *were* to introduce automatic synthesis of `Hashable` for product types, I’d want to be able to indicate which implementation-strategy should be used.

Which, honestly, seems hard to do without baking in special handling for Hashable, and by extension without each synthesis-supporting protocol getting special treatment (and thus leaving “third party” protocols a bit hamstrung in terms of being availble-for-synthesis themselves).

Even mere `Comparable` has similar amounts of fiddly detail — you probably want lexicographic, but in what ordering? do you want any of the constituent types' orderings flipped? — and so on and so forth.

> On Dec 5, 2015, at 12:46 AM, Harlan Haskins <harlan at harlanhaskins.com> wrote:
> 
> I feel like, if we implement automatic derivation for structs that wrap one value, we can just as easily implement automatic derivation for all product types for which the children conform to the protocols in question, and there’s a provided implementation for derivation by combining values.
> 
> Consider Hashable. A very common implementation of hashValue is xor-ing all the members’ hash values together.
> We could actually implement this right now given Swift’s reflection system (if we were able to conditionally cast to Hashable or any protocol with a Self requirement).
> 
> Consider this:
> 
> struct HashableDerivable deriving Hashable {
>     let string: String // because String is already Hashable
>     let integer: Int   // and Int is Hashable
>     // then HashableDerivable is trivially Hashable.
> }
> 
> /// This implementation is absolutely derivable at compile time.
> extension HashableDerivable: Hashable {
>     var hashValue: Int {
>         return string.hashValue ^ integer.hashValue
>     }
> }
> func ==(lhs: HashableDerivable, rhs: HashableDerivable) -> Bool {
>     return lhs.string == rhs.string && lhs.integer == rhs.integer
> }
> 
> // one can also use Reflection to derive this at runtime
> 
> extension Mirror {
>     func canDeriveHashable() -> Bool {
>         if self.subjectType is Hashable { return true } // this is currently a compiler error
>         for child in self.children {
>             let mirror = Mirror(reflecting: child)
>             if !mirror.canDeriveHashable() { return false }
>         }
>         return true
>     }
>     func deriveHashValue() -> Int {
>         if !self.canDeriveHashable() { fatalError("Boy, I wish this didn't have to happen at runtime.") }
>         guard let firstChild = self.children.first as? Hashable /* also an error */ else { fatalError("no children") }
>         return self.children.dropFirst().reduce(firstChild.hashValue) { (hash, _: (_: String?, value: Any)) -> T in
>             return hash ^ (value as! Hashable).hashValue
>         }
>     }
> }
> 
> Of course, this is something that can be done at compile time, which would make protocol conformance really, really simple.
> 
> We already do this, using the Mirror API, for CustomStringConvertible.
>> > On Dec 4, 2015, at 4:26 PM, John McCall <rjmccall at apple.com <https://lists.swift.org/mailman/listinfo/swift-evolution>> wrote:
>> > 
>> >> On Dec 4, 2015, at 1:19 PM, plx <plxswift at icloud.com <https://lists.swift.org/mailman/listinfo/swift-evolution>> wrote:
>> >> # A `wrapper` / `deriving` Construct
>> >> 
>> >> I'm sure a construct along these lines has been requested numerous times and is hopefully already on the roadmap.
>> >> 
>> >> The point of this email is to put out a reasonably-*concrete* sketch as a way of soliciting community feedback on the specifics of how such a construct might look-and-work within Swift; hopefully I’ve gone far-enough to be interesting, but not too much further than that.
>> >> 
>> >> ## Design Sketch
>> >> 
>> >> It ought to be possible to write something like this:
>> >> 
>> >>   // an example:
>> >>   struct SectionIndex
>> >>     wrapping Int
>> >>     as index
>> >>     satisfying precondition { $0 >= 0 }
>> >>     deriving Equatable, Comparable, Hashable {
>> >>     // declaration can continue in here
>> >>   }
>> >> 
>> >> ...which, when compiled, would be "expanded" along these lines:
>> >> 
>> >>   struct SectionIndex {
>> >> 
>> >>     // would have been `wrappedValue` w/out the `as index` clause
>> >>     let index: Int
>> >> 
>> >>     init(_ index: Int) {
>> >>       precondition(index >= 0) 
>> >>       // ^ would have been assert(index >= 0) 
>> >>       //   had we used `satisfying { $0 >= 0 }`,
>> >>       //   and omitted entirely had we omitted a `satisfying` clause
>> >>       self.index = index
>> >>     }
>> >> 
>> >>   }
>> >> 
>> >>   extension SectionIndex : Equatable {
>> >>   }
>> >> 
>> >>   // synthesized unless explicitly written-out
>> >>   func ==(lhs: SectionIndex, rhs: SectionIndex) -> Bool {
>> >>     return lhs.index == rhs.index
>> >>   }
>> >> 
>> >>   // same for Comparable, Hashable, all done in the obvious way    
>> >> 
>> >>   // there’s a lot of utility in synthesizing something like this,
>> >>   //  I can expand on it if necessary:
>> >>   extension SectionIndex: ValueWrapperType {
>> >>     typealias WrappedType = Int
>> >>   }
>> >> 
>> >> ...where each method/init/subscript/etc in the derived protocols gets synthesized at compile-time, if not explicitly implemented; similarly, if not explicitly-declared, the derived protocols' typealiases can be synthesized in obvious ways, and it seems acceptable to simply fail to compile (and inform the user of the need to make an explicit-declaration) in cases where such synthesis is impossible.
>> >> 
>> >> I think this enough to sketch the way the feature would look and how it would work. 
>> > 
>> > I’m not sure what work is being done by “wrapping X as Y” here; it seems like just another way of expressing a stored property.
>> > 
>> > I think we’re all interested in a “deriving” proposal.  However, the key problem that a serious proposal would have to address is not picking the syntax, but describing how derivation would actually work.  We’d prefer not to just hard-code rules in the compiler for specific protocols.
>> > 
>> > For example, derivation presumably involves recursively invoking the given operation on each of the stored properties (what does “on” mean? which parameters are changed, and which are passed through?) and then merging the results (how?).
>> > 
>> > John.
>> 
>> Apologies for leaving too much out.
>> 
>> I meant to propose that the `deriving` in this place would enforce the wrapper type only wrapped a single stored value, warranting the distinct syntax; I seem to have edited-out both an explicit statement that this assumed a single-stored-property and to have omitted a comment in the `//declaration can continue in here` that no additional stored-properties could be declared (analogous to the rules current applied within extensions).
>> 
>> Yes, constraining a `deriving` construct to only support wrappers containing a single stored property would, on the one hand, be somewhat limiting, but on the other hand it would seemingly allow trivial solutions to the issues you bring up:
>> 
>> - `on` is unambiguous as there’s only one thing it can be “on"
>> - there’s no ordering-of-operations to have to worry about
>> - there’s no merging-of-results to have to worry about
>> - i’m guessing there’s no parameters needing to getting changed (but I’m not 100% on what you mean here)
>> - there’s no associated-type incoherency to worry about (unless user error introduces it)
>> 
>> …there’s least one tricky case (if you want the wrapper to replace one of the wrapped type’s typealiases with a wrapper).
>> 
>> …and at least for me, there’s enough value in that simplified wrapper-synthesis / deriving-type construct to take the time to check community interest. 
>> 
>> Thanks for taking the time to read and send feedback.
>> 
>> PS:
>> 
>> On the other hand, if this becomes writable:
>> 
>>     protocol WrapperType {
>>        typealias WrappedValue
>>        var wrappedValue: { get }
>>     }
>> 
>>     extension WrapperType : Equatable where WrappedValue: Equatable {
>>     }
>> 
>>     func ==<W:WrapperType where W.WrappedValue:Equatable>(lhs: W, rhs: W) -> Bool {
>>       return lhs.wrappedValue == rhs.wrappedValue
>>     }
>> 
>> …etc., then it’s possible (albeit moderately unpleasant) to just write suitable glue logic out longhand on an as-needed basis (and with the caveat that all types wrapping T would potentially adopt all of T’s protocols even when potentially undesirable).
> 

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20151205/8da52390/attachment-0001.html>


More information about the swift-evolution mailing list