Swift Type Inference and Protocols with Associated Types

I am trying to follow Swift's Type erasure discussed here -

Specifically the code below:

protocol Cacheable {
    associatedtype CacheType
    func decode(_ data:Data) ->CacheType?
    func encode()->Data?

}

extension String:Cacheable {
    func decode(_ data:Data)->String? {
        let string = String(data: data, encoding: .utf8)
        return string
    }
    func encode()->Data? {
        return data(using: .utf8)
    }
}

class AnyCacheable<T>:Cacheable {
    private let _encode:()->Data?
    private let _decode:(_ data:Data)->T?

    init<U:Cacheable>(_ cacheable:U) where U.CacheType == T {
        self._encode = cacheable.encode
        self._decode = cacheable.decode
    }

    func decode(_ data:Data)->T? {
        return _decode(data)
    }

    func encode() -> Data? {
        return _encode()
    }
}

It works perfectly fine if I create a new instance of AnyCacheable as -

let cacheable:AnyCacheable = AnyCacheable("Swift")

I don't understand why I don't have to explicitly specify the concrete type of 'T' like -

let cacheable:AnyCacheable = AnyCacheable<String>("Swift")

How does Swift infer the concrete type for 'T'? From the initializer -

init<U:Cacheable>(_ cacheable:U) where U.CacheType == T {
    self._encode = cacheable.encode
    self._decode = cacheable.decode
}

Swift can infer the type for 'U' from the initializer argument (in this case a String type). Since a 'where' statement implies a constraint how does Swift infer the type of 'T' to determine if it equals U.CacheType?

You have fixed U to be a specific type, String, because that's what you're passing in.*

Once you have that fixed, it is also fixed that T must be whatever that type's CacheType is, per the where clause.

Since there is only one possible thing it can be, the compiler will let you omit it.

*well, strictly speaking it could be any string literal convertible thing, but the rule is if it can be StringLiteralType, it will be.

1 Like

Thanks. So the compiler pretty much ignores the constraint check U.CacheType == T because like you said 'T' cannot be anything else once U.CacheType has been inferred as 'String' / 'StringLiteralType' correct?

It doesn't really ignore it – that constraint is fundamental to how your AnyCacheable type works. It is playing a role in fixing what T is. It means, for example, that you couldn't write this:

// presumably you have some other form of caching in mind in other circumstances
var cacheables: [AnyCacheable<Int>] = []
cacheables.append(AnyCacheable("Swift"))
// error: Cannot convert value of type 'AnyCacheable<String>' to expected argument type 'AnyCacheable<Int>'

Oh, absolutely. I actually meant to ask if that constraint really got morphed into an assignment. I couldn't understand how the compiler could possible evaluate that expression to a Bool value if the type for 'T' isn't explicitly passed and also couldn't be implicitly inferred from anywhere else. The only block of code that provides any sort of context for 'T' is that 'where' clause - where U.CacheType == T that itself needs a prior deduction of 'T' to a concrete type before any attempt to evaluate that expression can be made.

Type inference is beautiful in most cases but actually takes a whole lot longer in some situations like this (At least for me) to understand which makes me totally hate it (until I figure out what actually is happening behind the curtains) :-)

What you're seeing here is the result of associated type inference. The type of String's CacheType is inferred based on the requirement for decode from Cacheable and the definition of decode from String.

protocol Cacheable {
    func decode(_ data:Data) ->CacheType?
}

extension String : Cacheable {
    func decode(_ data:Data)->String?
}

By comparing the signatures the complier can determine that CacheType for String is String and then based on your constraint that U.CacheType == T, infer T has to be String in this instance since U == String (which is a result of your calling the initializer with a String).

Mark

1 Like