> On 9. Jun 2017, at 21:47, Matthew Johnson via swift-evolution 
> <swift-evolution@swift.org> wrote:
> 
> 
>> On Jun 9, 2017, at 2:39 PM, Xiaodi Wu <xiaodi...@gmail.com 
>> <mailto:xiaodi...@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



_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to