Synthesized collection conformance for simple wrappers

There might be multiple occasions when we want to wrap a collection into a custom type, but otherwise leave the collection behavior unchanged. As an example, a task manager app could have a custom struct called TaskList which exactly mirrors the behavior of an array, the only difference being the declaration of a stored property:

struct TaskList {
    
    var date: Date
    var tasks: [Task]
    
}

extension TaskList: Collection {
    
    typealias Index = Array<Task>.Index
    typealias Element = Array<Task>.Element
    
    var startIndex: Array<Task>.Index {
        tasks.startIndex
    }
    
    var endIndex: Array<Task>.Index {
        tasks.endIndex
    }
    
    subscript(position: Array<Task>.Index) -> Array<Task>.Element {
        get {
            tasks[position]
        }
    }
    
    func index(after i: Array<Task>.Index) -> Array<Task>.Index {
        tasks.index(after: i)
    }
    
}

โ€” the current situation requires a lot of boilerplate code, and the volume of this code increases as we choose to support a more specialized protocol (e.g., moving from simply Collection to RandomAccessCollection would require implementing even more methods in the extension above).

Because Swift has recently introduced many convenience behaviors, like callable nominal types or synthesized comparable conformance for enums, I propose adding synthesized sequence/collection protocol conformance.

More specifically, a type can declare sequence/collection protocol conformance. If these two requirements are met:

  • the type declares only one stored property that conforms to the aforementioned collection/sequence protocol,
  • and the type of this property either inherits from the declared protocol or explicitly conforms to it,

then the compiler will synthesize the conformance analogously to the extension in the above example. The code will thus be reduced simply to

struct TaskList: Collection {
    
    var date: Date
    var tasks: [Task]
    
}
2 Likes

What if you have multiple arrays in the type - should the compiler simply throw an error or should there be a way to tell the compiler which property to use?

I have explicitly mentioned that this would only work if the type declares only one (stored) property that conforms to collection/sequence, so there will be no ambiguity :] So yes, if the type has several properties that are collections, one still would need to implement the methods by hand.

I encounter this situation a lot. There are literally too many times I just copy-paste Collection conformance code from one type to another. It will be really great to have a synthesized conformance.

I have a question regarding one aspect of how it works.

Collection-conforming types in the standard library don't have same Collection behaviour. For example, Set has read-only subscript, while Array has read-write subscript. How does the compiler tell if the custom type wants a read-only or read/write subscript? Does the compiler look at the Collection-conforming property within the custom type, and see how the property implemented the protocol, then copy it for the custom type? Or, does the compiler just simply delegate all the custom type's Collection conformance to its property?

In a situation like this, I think having the compiler try to infer the correct property to derive conformance from might be a bit too special-casey or magical. Consider a couple additional examples:

// This simple change looks like it should work, but it won't
// because String is also a Collection.
struct TaskList: Collection {
  var title: String
  var tasks: [Task]
}

And how would it interact with retroactive conformances?

// ModuleA
extension Date: Collection { ... }

// Module B

// Does this import cause this to stop synthesizing?
import ModuleA
struct TaskList: Collection {
  var date: Date
  var tasks: [Task]
}

But, this kind of situation does come up frequently (perhaps even more often in "new-type" constructs where a struct wraps another single value without any additional stored properties) and a more principled way to address it would be to make it explicit and clearโ€”that is, implement a generalized notion of "protocol forwarding" where you can say "implement this protocol by delegating to this property that is also a type that conforms to it".

This thread from @anandabits discusses the idea in more detail.

5 Likes

I'd love to see the topic of forwarding revisited someday. I never had a chance to put together my final thoughts on the topic as I didn't want to invest further effort after the Swift 3 proposal freeze happened. If anyone is interested in working on the compiler implementation side of this I could brush off my notes and put a new pitch together.

5 Likes

I agree that strings make this use-case somewhat unfortunate, because we usually don't work with strings as collections that explicitly. While I mention that fully automatic conformance would require that the type declares only one collection-conforming member, another way to disambiguate is to explicitly typealias the Element (or possibly any other) associated type:

struct TaskList: Collection {
  typealias Element = Array<Task>.Element
  var title: String
  var tasks: [Task]
}

โ€” this way it will be again possible to unambiguously refer to the property (here โ€” tasks) that the behavior is delegated to. The only caveat is that Task can happen to simply be a typealias of Character, but then we just report an error and require to implement manually. Same in the retroactive conformance case: if no further specification helps to resolve the correct property, we would require explicit conformance.

That way there is little more magical than more common synthesized Codable or Hashable conformances :]

Also, while protocol forwarding sounds like a neat feature, the proposed behavior here does not require introducing any new language syntax or major features, so perhaps it could be appealing by being relatively easy to implement.

I think this should be solved more generally and composable. Not specifically for collections, but for any situation where you want a type to conform to a protocol, and delegate the conformace in its entirety to a conforming child member.

I have no suggestions for syntax, but maybe some kind of annotation on a member, like so:

struct TaskList: Collection {
    var date: Date

    @implements(Collection)
    var tasks: [Task]
}

Wether we should annotate the member with a reference to the protocol, or annotate the outer type with a key path to the conforming member, or some other solution, I don't really care. But either way, by having a general solution for this problem, it opens the door for all kinds of other use cases.

In your example, you may want to have your type conform to Comparable and sort task lists by date. This way, you could forward Comparable-conformance to the date property.

10 Likes

That's not quite the case, though. Codable and Hashable synthesis are based on the characteristic that synthesis is possible if every stored property in the type conforms to those protocols. In that situation, a general protocol-forwarding solution wouldn't work because there are multiple properties whose values need to be considered and you're not merely forwarding, but rather projecting the operations onto the children and then sequencing and/or composing the results. It's the last part that can't be easily expressed in a general manner.

In this case, you're asking a type to conform to a protocol based on the compiler identifying exactly one stored property that conforms to that protocol and delegate its operations directly to it.

In your example where you introduce a typealias to disambiguate which Collection you actually mean, you've introduced what is probably the same amount of additional syntax as one would using a hypothetical protocol forwarding feature, but the intent is expressed much less clearly. One has to take a somewhat circuitous route of reasoning to understand that TaskList is getting its Collection conformance from tasks because its associated type happens to match that of that particular property.

It also falls apart if the type can't disambiguate either, as you said:

struct NamedCharacterSet: Collection {
  // Nothing we can say here to make this work
  typealias Element = Array<Character>.Element
  var name: String
  var characters: [Character]
}

Protocol forwarding would make the solution much clearer, because the relevant property would be directly tagged in some fashion to say "this is where the conformance should come from", and it eliminates these edge cases where you would require the user to implement it manually. The code would exactly express the intended semantics.

There's a recent thread about trade-offs for special case conformances vs. general solutions that is relevant to this discussion.

In this case I don't think special casing this in the compiler in the interest of expediency would be the right choice, and since Collection is not just a single protocol but a hierarchy of protocols with a large API surface and subtle semantic requirements, I suspect you may be overstating the ease of correctly implementing it. Protocol forwarding would get you a solution for your specific use case while providing so much additional value for other use cases that any design/implementation time would be far better spent there, IMO.

Yes, I see the robustness of your arguments. Any chance you know where I could read about the decision (or rather, the reasoning behind it) to freeze the protocol-forwarding proposal? I can't find neither any mention of it in the thread that you've previously linked nor the page in the proposal repo.

Seems like there is already a "private" @_implements attribute in Swift, that allow a conforming type to implement a protocol requirement through a different name. It can e.g. be used if two protocols have identically named requirements, but with different semantics.

Or if you simply don't want to add a computed property to simply forward a call:

struct Task: Identifiable {
    @_implements(Identifiable, id)
    var name: String
    var due: Date?
}

Here, name assumes the role of id. Maybe this attribute could be made public (non-underscored) and also offer a one-argument version which simply forwards every protocol requirement to the annotated member.

4 Likes

I believe what @anandabits was referring to there was a more general freeze on large new-feature proposals in general around that point in the Swift 3 timeline, not to a freeze of that specific proposal.

In any case, that time has passed, and I don't think there is anything else that would block the proposal from moving forward, at least not process-wise. It just needs someone(s) to refine the design (taking into account changes in Swift that have occurred since it was written) and to provide the implementation that is now required for proposals.

Yep, thatโ€™s what I was referring to.

I would be happy to help refine the design and did have a direction in mind after the discussion thread. Iโ€™m not able to work on implementation though, so I would need a collaborator to make it worth the effort to invest time into updating the design and writing a new pitch. :slight_smile:

I like the idea of something like @impliments above. I run into this when I want to create a restricted String type. It would be nice to be able to default all string protocol definitions to the internal string and still be able to override a few key behaviors like initialization.

On problem I do see though is trying to plug all the holes. In the case of gating init, what happens when a new init method is added to the protocol and now there is a backdoor to creating your type without validation.

1 Like

I tend to use forwarder protocols to eliminate this boilerplate:

public protocol CollectionWrapper: Collection {
    associatedtype Wrapped: Collection where Wrapped.Index == Index, Wrapped.Element == Element
    var wrapped: Wrapped { get set }
}
extension CollectionWrapper {
    public var startIndex: Index { wrapped.startIndex }
    public var endIndex: Index { wrapped.endIndex }
    public subscript(idx: Index) -> Element {
        return wrapped[idx]
    }
    public func index(after idx: Index) -> Index {
        return wrapped.index(after: idx)
    }
}

public struct ArrayWrapper<T>: CollectionWrapper {
    public typealias Index = Array<T>.Index
    public typealias Element = Array<T>.Element
    public var wrapped: Array<T>
}

let wrapped = ArrayWrapper(wrapped: [2,4,6,8,10])
wrapped.forEach { print($0) } // prints: 2, 4, 8, 10

The good thing is that it's explicit and scales to any protocol. The not-so-great thing is that all of the implementation details are required to be public.

It would be nice if I could write something like this instead, so the CollectionWrapper protocol is not exposed publicly, but since Collection is of course public, we could expose the default implementations which complete that conformance:

internal protocol CollectionWrapper {
    associatedtype Wrapped: Collection
    var wrapped: Wrapped { get set }
}

extension CollectionWrapper where Self: Collection, Self.Index == Wrapped.Index, Self.Element == Wrapped.Element {
    // error: cannot declare a public property in an extension with internal requirements
    public var startIndex: Index { wrapped.startIndex }
    public var endIndex: Index { wrapped.endIndex }
    public subscript(idx: Index) -> Element {
        return wrapped[idx]
    }
    public func index(after idx: Index) -> Index {
        return wrapped.index(after: idx)
    }
}

public struct ArrayWrapper<T>: Collection {
    public typealias Index = Int
    public typealias Element = T
    internal var wrapped: Array<T>
}
extension ArrayWrapper: CollectionWrapper {}
6 Likes
Terms of Service

Privacy Policy

Cookie Policy