Function argument that is a constrained protocol

I want a function with an argument that must be any implementation of protocol Collection, so long as it contains UInt8.

What I would have expected in other languages is something like:

func(path:Collection<UInt8>) -> UInt64

But I know that’s not how swift works. The following code does what I need:

func insert<C:Collection>(path: C) -> UInt64 where C.Element == UInt8 {

    // Do stuff here...
    
    return 0
}

But it's very verbose and it will make quite a few function definitions far more wordy than I’d like for easy readability. Is there a way to use ‘typealias’ to simplify this?

If not, are there plans to simplify this kind of function definition? Or will I always need that boilerplate?

You can use AnyCollection<UInt8>.

Regarding plans, there are plans of adding "Existentials": swift/GenericsManifesto.md at main · apple/swift · GitHub

1 Like

Just to avoid any sort of confusion, existential types have always been there (protocols for instance). The gist of the section dedicated to existentials is mainly about opening them, or, roughly said, introducing a type-safe mechanism for allowing values of protocols types that have associated types or Self constraints (currently we can only use them as generic constraints).

@AEC I don't know how you're supposed to convert any Collection of a specific element type to AnyCollection. Your approach, however, is correct and you'll have to cope with the verboseness for now: defining such a type alias would require opening the Collection existential, which then again isn't supported yet. Generic extensions, that would allow you to write the generic signature once on the extension itself, are unfortunately more in words than in deed for the time being.

1 Like

Thank you. I thought the suggestion of AnyCollection would be a wonderful solution because it would be terse, but you’re right, there’s no easy way to make the collection I’m passing an AnyCollection.

I’ve reverted to my original, more verbose, solution which works fine. I hope this is improved. I dislike boilerplate; it distracts from the actual semantic meaning of what I’m trying to do. Hopefully this is improved for Swift 5.

Thank you again!

Not sure what you meant by "no easy way" to construct an AnyCollection; you can pass any collection into its initializer, like so:

let anyCollection = AnyCollection([1, 2, 3])

It's not pretty, but it works. Maybe that was your point.

But yes, generally there's no perfect way around this. Generalized existentials will allow you to type something like the following:

typealias AnyCollection<E> = Any<Collection where .Element == E>

Then, your first example would work exactly like you would expect from other languages (after substituting AnyCollection for Collection). I wouldn't expect that feature to land in Swift 5 though.

Note that for the time being you could define an operator:

prefix operator ^
prefix func ^ <C: Collection, E>(c: C) -> AnyCollection<E> where C.Element == E {
    return AnyCollection(c)
}

func insert(path: AnyCollection<UInt8>) -> UInt64 {
    // Do something...
    return 0
}
insert(path: ^[0, 1, 2, 3, 4])
2 Likes

That’s really clever!

1 Like

Thanks for the suggestion! What kind of overhead is there is creating this wrapper? i.e. O(1) or O(N)?

Is this done with a wrapper struct on the stack, or is memory allocated for this?

1 Like

O(1). For a typical Collection type like Array or Set, the on-stack "value" will be promoted to the heap, but the underlying buffers that they point to will not be copied.

2 Likes