[swift-evolution] Pitch: Limit typealias extensions to the typealias

Karl Wagner razielim at gmail.com
Fri Jun 9 16:33:30 CDT 2017


> On 9. Jun 2017, at 21:47, Matthew Johnson via swift-evolution <swift-evolution at swift.org> wrote:
> 
> 
>> On Jun 9, 2017, at 2:39 PM, Xiaodi Wu <xiaodi.wu at gmail.com <mailto:xiaodi.wu at gmail.com>> wrote:
>> 
>> Interesting. So you’d want `newtype Foo = String` to start off with no members on Foo?
> 
> Yeah.  Previous discussions of newtype have usually led to discussion of ways to forward using a protocol-oriented approach.  Nothing has gotten too far, but it usually comes up that suppressing undesired members is important.
> 
> It is also important to have some way to distinguish between members with a parameter of the underlying type from members that should be treated by newtype as Self parameters.  The mechanism we have for doing that in Swift happens to be a protocol.

It’s important to note that you can create powerful quasi-subtype relationships using composition. This is going to be a little divergent, but this is WWDC week and everybody’s here, and its related to the topic of architecture with value-types.

The only practical difference between composition and subclassing is that you can’t add storage and have it carried along with the original “instance”. I’m not even sure that really makes sense for value types, which don’t have identity — the alternative, that a value is be composed of sub-values, makes more sense to me.

I’ve recently converted an entire project from using class hierarchies and stacks of protocols down to a handful of protocols and value-types. I cut the number of files in half, reduced duplicated logic, increased testability, maintainability, all kinds of good stuff. I’m pretty happy with how it turned out, so let me very briefly outline how I did it, to give a concrete example of the kinds of things you can do with composition.

The new data subsystem of this App is now entirely stateless - ultimately, if you look through all of the wrappers, we’re literally just passing around a handful of “let” variables (api endpoint, api key, login key, and a cache identifier), and the entire framework boils down to isolated islands of business logic written in extensions on those structs.

There are only two really important protocols: an ObjectStore (i.e. a cache) and an ObjectSource (i.e. the API). They have very generic methods, like “fetch”, “put” and “delete”. All of the business logic about which queries to run on the cache, and where to put the data, or how to query the correct data out of the API, is written in a collection of wrapper structs which you access to via a computed property (a kind of namespaced protocol-extension, e.g. myObjectStore.userInformation). Above the source and store (i.e. wrapping them) sits a Repository struct, which coordinates getting data from the ObjectStore, querying for that data from the ObjectSource if it doesn’t have anything (or if its expired), and returns a future (actually it’s a Reactive Observable, but any kind of future-like-object will do) encapsulating the operation. 

There’s lots you can do with value-types. For example, I created a wrapper for the top-level “Session” type which dynamically checks if the session belongs to a logged-in user. There is a separate repository for public data (e.g. store locations) and private data (e.g. purchase history), and we can model all of this separation really easily in the type-system with no cognitive or computational overhead.

/// An API session, which may or may not belong to a logged-in user.
///
struct Session {
    typealias Identity = (loginKey: String, cacheIdentifier: String)

    let configuration: SessionConfiguration // creates repository on behalf of the session, for unit-testing.
    let identity: Identity?
    let publicData: PublicDataRepository

    init(configuration: SessionConfiguration, identity: Identity) {
        self.configuration = configuration
        self.identity      = identity
        self.publicData    = configuration.makePublicRepository()
    }
}

/// A session which is dynamically known to belong to a logged-in user.
///
struct AuthenticatedSession {

    let base: Session // you can think of this like ‘super’
    let privateData: PrivateDataRepository

    init?(base: Session) {
        guard let identity = base.identity else { return nil }
        self.base   = base
        privateData = base.configuration.makePrivateRepository(for: identity)
    }
}

/* methods which do not require authentication */

extension Session { 
    func getStoreLocations() -> Future<[StoreLocation]> { … }
 }

/* methods which require a logged-in user */

extension AuthenticatedSession {  
    func buySomething(_: ThingToBuy) -> Future<PurchaseReceipt> { … }
}

When it comes to storage, AuthenticatedSession not having the same memory layout as Session means you can’t store one variable that could be either — unless you box it. You can use a protocol to create a semantically-meaningful box (e.g. PublicDataProvider, with one computed property which returns the public-only “Session”), or if you can’t be bothered, you could store it as Any and dynamic-cast to handle all the kinds of values you know how to work with.

It’s a simple model, but it works, its fast, and you can do lots with it if you know how to use it. I feel that sub-typing is really something that requires identity if you want any meaningful benefits over composition.

- Karl



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


More information about the swift-evolution mailing list