Help with protocol conformance for 'contains'

I've been trying to write a single function where previously I had 2. The original 2 functions had these prototypes:

typealias CategoryID = Int16
let someOtherSetOfStrings = Set<String>(.....)

func categories<S:StringProtocol>(for searchString: S) -> Set<CategoryID>

and

func categories(for stringSet: Set<String>) -> Set<CategoryID>

The implementation of both functions looks identical and both of them make use of their respective 'for' parameter's 'contains(...)' function

e.g:

someOtherSetOfStrings.allSatisfy({searchString.contains($0)})

and

someOtherSetOfStrings.allSatisfy({stringSet.contains($0)})

I was hoping that 'contains' for StringProtocol and Set both came from a single protocol and that I could then use that as my 'for' parameter's type, but they don't. StringProtocol has its own version:

func contains<T>(_ other: T) -> Bool where T : StringProtocol

while Set gets its from SetAlgebra (I think):

func contains(_ member: Self.Element) -> Bool

So I tried creating my own protocol explicitly:

protocol SupportsContains {
func contains<T>(_ other: T) -> Bool where T : StringProtocol
}

and changing my function to:

func categories(for containable: SupportsContains) -> Set<CategoryID> {
...
someOtherSetOfStrings.allSatisfy({containable.contains($0)})
}

extension Set: SupportsContains where Element == String {
    @inlinable
    func contains<T>(_ other: T) -> Bool where T : StringProtocol {
        contains(String(other))
    }
}

But I can't make StringProtocol itself conform to SupportsContains so can never call my 'categories' function with a StringProtocol type.

I know that I could conform String to SupportsContains and ensure I do categories(for: String(someSubstring)), but there are times I just want to pass in a Substring directly.

I am sure there is a solution, but I'm not yet seeing it.

Thanks

Only String and Substring can conform to StringProtocol. As you point out, you cannot retroactively make one protocol refine another. The solution here is to conform both of the concrete types to your custom protocol.

1 Like

Thanks - That's what I went with in the end.

@gutley I realize that you probably already found something that works, but I just wanted to share this really nice option that we came up with, in case it could be helpful.

Here's what it lets you do:

struct Widget {
    var tags: Tags = [.blue, .green]
}

let widget = Widget()

switch widget.tags {
case Contains(.blue):
    print("blue")
default:
    print("default")
}

Supporting source-code:

public protocol Option: RawRepresentable, Hashable, CaseIterable {}

/// Arbitrary, limited, assignable, non-exclusive unit of reference.
public enum Tag: String, Option {
    case red
    case green
    case blue
}

public typealias Tags = Set<Tag>

extension Set where Element: Option {
    public var rawValue: Int {
        var rawValue = 0
        for (index, element) in Element.allCases.enumerated() {
            if self.contains(element) {
                rawValue |= (1 << index)
            }
        }
        
        return rawValue
    }
}

extension Set where Element == Tag {
    /// A default set of tags that contains no tags.
    public static var `default`: Self { [] }
}

/// Pattern matching function to allow `Contains(Tag)` syntax in `switch` statements.
///
/// - Parameters:
///   - pattern: A `Tags` container.
///   - value: a `Tag` we're trying to match (e.g. the subject of a `switch`).
/// - Returns: `Bool` of whether the pattern matches.
public func ~= <C: Container>(pattern: C, value: Set<C.T>) -> Bool where C.T == Tag {
    return value.contains(pattern.item)
}

/// A container used to enhance matching syntax in `switch` statements.
public protocol Container {
    associatedtype T
    /// Contents of the container.
    var item: T { get }
    
    /// Create a container that contains `item`.
    /// - Parameter item: the contents.
    init(_ item: T)
}

/// Implementation of `Container` protocol.
public struct Contains<T>: Container {
    public let item: T
    public init(_ item: T) {
        self.item = item
    }
}
```