How to handle multiple generic protocols in Swift? - swift

I'm trying to use two generic protocols that relate to each other as:
protocol PersistableData {}
protocol DataStore: class {
associatedtype DataType: PersistableData
func save(data: DataType, with key: String)
func retreive(from key: String) -> DataType?
}
protocol PersistentDataModel {
// Swift infers that DataType: PersistableData as DataType == DataStoreType.DataType: PersistableData
// Setting it explicitly makes the compiler fail
associatedtype DataType
associatedtype DataStoreType: DataStore where DataStoreType.DataType == DataType
}
extension String: PersistableData {}
protocol StringDataStore: DataStore {
associatedtype DataType = String
}
class Test: PersistentDataModel {
typealias DataType = String
typealias DataStoreType = StringDataStore
}
However Xcode fails to compile saying that Type 'Test' does not conform to protocol 'PersistentDataModel' and suggesting that Possibly intended match 'DataStoreType' (aka 'StringDataStore') does not conform to 'DataStore' while StringDataStore is defined as conforming to DataStore
I've read a few good resources about generic protocols including SO and this Medium post, but I could not find where the issue is.

This happening because your typealias for associatedtype should have concretion, not abstraction.
Thus, for your case, StringDataStore should be a class, not protocol.
protocol PersistableData {}
protocol DataStore: class {
associatedtype DataType: PersistableData
func save(data: DataType, with key: String)
func retreive(from key: String) -> DataType?
}
protocol PersistentDataModel {
// Swift infers that DataType: PersistableData as DataType == DataStoreType.DataType: PersistableData
// Setting it explicitly makes the compiler fail
associatedtype DataType
associatedtype DataStoreType: DataStore where DataStoreType.DataType == DataType
}
extension String: PersistableData {}
class StringDataStore: DataStore {
typealias DataType = String
func save(data: String, with key: String) {
//
}
func retreive(from key: String) -> String? {
return nil
}
}
class Test: PersistentDataModel {
typealias DataType = String
typealias DataStoreType = StringDataStore
}
However, you can keep using protocols and solve it by using additional generics conditions in your Test class:
class Test<T: StringDataStore>: PersistentDataModel where T.DataType == String {
typealias DataStoreType = T
typealias DataType = T.DataType
}
Using this you can tell to compiler that concrete type will be passed to Test somewhere else.
Like this:
class ConcreteStringDataStore: StringDataStore {
func save(data: String, with key: String) {
//
}
func retreive(from key: String) -> String? {
return nil
}
}
let test = Test<ConcreteStringDataStore>()

Related

Swift: Constrain a generic variable to be a class or a protocol

As KeyPath can't be casted to super types in Swift, I want to write a type-erased version, which represents any KeyPath whose value could be casted to a specific protocol or super type:
public struct PropertyGetter<Root, ValueType> {
private let keyPath: PartialKeyPath<Root>
public init<T: ValueType>(_ keyPath: KeyPath<Root, T>) {
self.keyPath = keyPath
}
public func get(_ instance: Root) -> ValueType {
return instance[keyPath: self.keyPath] as! ValueType
}
}
The compiler rightfully complains that
type 'T' constrained to non-protocol, non-class type 'ValueType.Type'
as ValueType could potentially be a struct type.
So how do we properly constrain this? That is enforcing that
ValueType is either a class type or a protocol, i.e subclassable / implementable
T is constrained to conform to ValueType, i.e x as! ValueType must be guranteed to work, where x: T
Notice that it is indeed possible to write such a type-erasing struct, when the Protocol type is fixed. E.g a class which only accepts KeyPaths pointing to CustomStringConvertible members:
public struct CustomStringConvertibleGetter<Root> {
private let keyPath: PartialKeyPath<Root>
public init<T: CustomStringConvertible>(_ keyPath: KeyPath<Root, T>) {
self.keyPath = keyPath
}
public func get(_ instance: Root) -> CustomStringConvertible {
return instance[keyPath: self.keyPath] as! CustomStringConvertible
}
}
let getter1 = CustomStringConvertibleGetter(\SomeClass.someString) // works
let getter2 = CustomStringConvertibleGetter(\SomeClass.nonConformingMember) // will throw an error at compile time

Genericize method to encode a `Codable` protocol

import Foundation
protocol ProtocolA: Codable {
var var1: String { get }
}
struct Type1: ProtocolA {
var var1: String
var var2: Int
}
struct Type2: ProtocolA {
var var1: String
var var2: Bool
}
func encode<T: ProtocolA & Encodable>(object: T) throws -> Data {
return try JSONEncoder().encode(object as! T.Type)
}
Putting the above in a playground results in error: argument type 'T.Type' does not conform to expected type 'Encodable'
Why does this happen when I am saying that T has to conform to Encodable?
return try JSONEncoder().encode(object as! T.Type)
This means to convert object to the metatype of T. The type of the type. For example, 1 is an Int. But "Int" itself has a type, which is Int.Type, which is a metatype. Metatypes do not conform to Encodable.
You meant:
return try JSONEncoder().encode(object as! T)
But you really just meant:
return try JSONEncoder().encode(object)
since object is always of type T. That's its explicit type from the function signature. Since you also don't rely on ProtocolA for this algorithm, this all boils down to:
func encode<T: Encodable>(object: T) throws -> Data {
return try JSONEncoder().encode(object)
}
Your compiler is actually complaining about the type cast your doing at the end which is converting object to the metatype of T, in your example either Type1.Type or Type2.Type.
From the encoding perspective, what the compiler needs to know is the model confirms to Encodable which is implicit in the T: Codable statement.
import Foundation
protocol ProtocolA: Codable {
var var1: String { get }
}
struct Type1: ProtocolA {
var var1: String
var var2: Int
}
struct Type2: ProtocolA {
var var1: String
var var2: Bool
}
func encode<T: Codable>(object: T) throws -> Data {
return try JSONEncoder().encode(object)
}
let type2 = Type2(var1: "test1", var2: true)
print(type2)

Swift Associated type constraints

I have two protocols with each defining an associated type. One of the protocols needs to define a variable of typed the other protocol where they both have the same type for associated type. Is it possible to somehow infer the type of associated type?
protocol A {
associatedtype AModel
var b: B { get }
}
protocol B {
associatedtype BModel
func doAnother(anotherModel: BModel)
}
Here is what I tried with no success
protocol A {
associatedtype AModel
associatedtype TypedB = B where B.BModel == AModel
var another: TypedB { get }
}
protocol B {
associatedtype BModel
func doAnother(anotherModel: BModel)
}
Please find the following working playground example. You need to use the associated type's name, not the constraining protocol's name. The reason for this is described here.
import Foundation
protocol A {
associatedtype AModel
associatedtype TypedB: B where TypedB.BModel == AModel
var another: TypedB { get }
}
protocol B {
associatedtype BModel
func doAnother(anotherModel: BModel)
}
// compiles
struct One: B {
typealias BModel = String
func doAnother(anotherModel: String) {}
}
struct Second: A {
typealias AModel = String
typealias TypedB = One
var another: One
}
// does not compile
struct Third: B {
typealias BModel = Int
func doAnother(anotherModel: Int) {}
}
struct Fourth: A { // A' requires the types 'Fourth.AModel' (aka 'String') and 'Third.BModel' (aka 'Int') be equivalent
typealias AModel = String
typealias TypedB = Third
var another: Third
}

Using protocol with typealias as a property

I have a protocol with a typealias:
protocol Archivable {
typealias DataType
func save(data: DataType, withNewName newName: String) throws
func load(fromFileName fileName: String) throws -> DataType
}
and a class that conforms to that protocol:
class Archiver: Archivable {
typealias DataType = Int
func save(data: DataType, withNewName newName: String) throws {
//saving
}
func load(fromFileName fileName: String) throws -> DataType {
//loading
}
}
and I would like to use Archivable as a property in another class like:
class TestClass {
let arciver: Archivable = Archiver() //error here: Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requiments
}
but it fails with
Protocol 'Archivable' can only be used as a generic constraint because it has Self or associated type requiments
My goal is that TestClass should only see Archiver as Archiveable, so if I want to change the saving/loading mechanism, I just have to create a new class that conforms to Archivable as set it as the property in TestClass, but I don't know if this is poosible, and if so, then how.
And I would like to avoid using AnyObject instead of DataType.
Depending on what you are actually trying to do, this can work using type erasure. If you follow the instructions in the link R Menke posted in the comments, you can achieve what you are trying to do. Since your property in TestClass seems to be a let, I'm going to assume you already know the type of DataType at compile time. First you need to setup a type erased Archivable class like so:
class AnyArchiver<T>: Archivable {
private let _save: ((T, String) throws -> Void)
private let _load: (String throws -> T)
init<U: Archivable where U.DataType == T>(_ archiver: U) {
_save = archiver.save
_load = archiver.load
}
func save(data: T, withNewName newName: String) throws {
try _save(data, newName)
}
func load(fromFileName fileName: String) throws -> T {
return try _load(fileName)
}
}
Much like Swift's AnySequence, you'll be able to wrap your Archiver in this class in your TestClass like so:
class TestClass {
let archiver = AnyArchiver(Archiver())
}
Through type inference, Swift will type TestClass' archiver let constant as an AnyArchiver<Int>. Doing it this way will make sure you don't have to create a dozen protocols to define what DataType is like StringArchiver, ArrayArchiver, IntArchiver, etc. Instead, you can opt in to defining your variables with generics like this:
let intArchiver: AnyArchiver<Int>
let stringArchiver: AnyArchiver<String>
let modelArchiver: AnyArchiver<Model>
rather than duplicating code like this:
protocol IntArchivable: Archivable {
func save(data: Int, withNewName newName: String) throws
func load(fromFileName fileName: String) throws -> Int
}
protocol StringArchivable: Archivable {
func save(data: String, withNewName newName: String) throws
func load(fromFileName fileName: String) throws -> String
}
protocol ModelArchivable: Archivable {
func save(data: Model, withNewName newName: String) throws
func load(fromFileName fileName: String) throws -> Model
}
let intArchiver: IntArchivable
let stringArchiver: StringArchivable
let modelArchiver: ModelArchivable
I wrote a post on this that goes into even more detail in case you run into any problems with this approach. I hope this helps!
When you try to declare and assign archiver:
let archiver: Archivable = Archiver()
it must have concrete type.
Archivable is not concrete type because it's protocol with associated type.
From "The Swift Programming Language (Swift 2)" book:
An associated type gives a placeholder name (or alias) to a type that
is used as part of the protocol. The actual type to use for that
associated type is not specified until the protocol is adopted.
So you need to declare protocol that inherits from Archivable and specifies associated type:
protocol IntArchivable: Archivable {
func save(data: Int, withNewName newName: String) throws
func load(fromFileName fileName: String) throws -> Int
}
And then you can adopt this protocol:
class Archiver: IntArchivable {
func save(data: Int, withNewName newName: String) throws {
//saving
}
func load(fromFileName fileName: String) throws -> Int {
//loading
}
}
There are no truly generic protocols in Swift now so you can not declare archiver like this:
let archiver: Archivable<Int> = Archiver()
But the thing is that you do not need to do so and I explain why.
From "The Swift Programming Language (Swift 2)" book:
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.
So basically when you want to declare archiver as Archivable<Int> you mean that you don't want some piece of code using archiver to know about its concrete class and to have access to its other methods, properties, etc.
It's obvious that this piece of code should be wrapped in separate class, method or function and archiver should be passed there as parameter and this class, method or function will be generic.
In your case TestClass can be generic if you pass archivable via initializer parameter:
class TestClass<T, A: Archivable where A.DataType == T> {
private let archivable: A
init(archivable: A) {
self.archivable = archivable
}
func test(data: T) {
try? archivable.save(data, withNewName: "Hello")
}
}
or it can have generic method that accepts archivable as parameter:
class TestClass {
func test<T, A: Archivable where A.DataType == T>(data: T, archivable: A) {
try? archivable.save(data, withNewName: "Hello")
}
}
Hector gives a more complex though ultimately better solution above but I thought I'd post an alternative take on the answer anyway. It is simpler but probably less flexible in the long term.
typealias DataType = Int
protocol Archivable {
var data: DataType { get set }
func save(data: DataType, withNewName newName: String) throws
func load(fromFileName fileName: String) throws -> DataType
}
class Archiver: Archivable {
var data:DataType = 0
func save(data: DataType, withNewName newName: String) throws {
//saving
}
func load(fromFileName fileName: String) throws -> DataType {
return data
}
}
class TestClass {
let arciver: Archivable = Archiver()
}

Swift protocol extension implementing another protocol with shared associated type

Consider the following:
protocol Foo {
typealias A
func hello() -> A
}
protocol FooBar: Foo {
func hi() -> A
}
extension FooBar {
func hello() -> A {
return hi()
}
}
class FooBarClass: FooBar {
typealias A = String
func hi() -> String {
return "hello world"
}
}
This code compiles. But if I comment out explicit definition of associated type typealias A = String, then for some reason, swiftc fails to infer the type.
I'm sensing this has to do with two protocols sharing the same associated type but without a direct assertion through, for example, type parameterization (maybe associated type is not powerful/mature enough?), which makes it ambiguous for type inference.
I'm not sure if this is a bug / immaturity of the language, or maybe, I'm missing some nuances in protocol extension which rightfully lead to this behaviour.
Can someone shed some light on this?
look at this example
protocol Foo {
typealias A
func hello() -> A
}
protocol FooBar: Foo {
typealias B
func hi() -> B
}
extension FooBar {
func hello() -> B {
return hi()
}
}
class FooBarClass: FooBar {
//typealias A = String
func hi() -> String {
return "hello world"
}
}
with generics
class FooBarClass<T>: FooBar {
var t: T?
func hi() -> T? {
return t
}
}
let fbc: FooBarClass<Int> = FooBarClass()
fbc.t = 10
fbc.hello() // 10
fbc.hi() // 10
Providing explicit values for associated types in a protocol is required for conformance to said protocol. This can be accomplished by hard coding a type, as you've done with typealias A = String, or using a parameterized type as you mentioned, such as below:
class FooBarClass<T>: FooBar {
typealias A = T
...
}
Swift will not infer your associated type from an implemented method of the protocol, as there could be ambiguity with multiple methods with mismatching types. This is why the typealias must be explicitly resolved in your implementing class.