How to Use Type Erasure with a Protocol Using Associated Type

How can I use Type Erasure with a protocol using associated type

To your actual question, the type eraser is straight-forward:

final class AnyClient<T: EndpointType>: ClientType {
let _request: (T) -> Void
func request(_ request: T) { _request(request) }

init<Client: ClientType>(_ client: Client) where Client.T == T {
_request = client.request
}
}

You'll need one of these _func/func pairs for each requirement in the protocol. You can use it this way:

let client = AnyClient(Client<ProfilesAPI>())

And then you can create a testing harness like:

class RecordingClient<T: EndpointType>: ClientType {
var requests: [T] = []
func request(_ request: T) -> Void {
requests.append(request)
print("recording: \(request.baseURL)")
}
}

And use that one instead:

let client = AnyClient(RecordingClient<ProfilesAPI>())

But I don't really recommend this approach if you can avoid it. Type erasers are a headache. Instead, I would look inside of Client, and extract the non-generic part into a ClientEngine protocol that doesn't require T. Then make that swappable when you construct the Client. Then you don't need type erasers, and you don't have to expose an extra protocol to the callers (just EndpointType).

For example, the engine part:

protocol ClientEngine: class {
func request(_ request: String) -> Void
}

class StandardClientEngine: ClientEngine {
func request(_ request: String) -> Void {
print(request)
}
}

The client that holds an engine. Notice how it uses a default parameter so that callers don't have to change anything.

class Client<T: EndpointType> {
let engine: ClientEngine
init(engine: ClientEngine = StandardClientEngine()) { self.engine = engine }

func request(_ request: T) -> Void {
engine.request(request.baseURL)
}
}

let client = Client<ProfilesAPI>()

And again, a recording version:

class RecordingClientEngine: ClientEngine {
var requests: [String] = []
func request(_ request: String) -> Void {
requests.append(request)
print("recording: \(request)")
}
}

let client = Client<ProfilesAPI>(engine: RecordingClientEngine())

How can you store an array of protocols with associated types in Swift without using Any?

PATs are usually complex and do not involve an easier solution. I would suggest you come up with some simpler design for your problem. The problem with your method is actually having PAT and nested protocols and trying to make them work together.

Even if you type erase Action to some type like AnyAction, these two different types DogAction and CatAction again produce different types and you cannot intermix them inside the array. What you can possibly do is have two type erasures, one for Action and other for StateType. When you wrap CatAction or DogAction inside AnyAction both of them would then wrap in type erase state to AnyState.

Here is one example you could approach this type erasing both the protocols,

protocol StateType { }

struct DogState: StateType { }

struct CatState: StateType { }

protocol Action {
associatedtype ST: StateType
func process() -> ST
}

struct DogAction: Action {
func process() -> DogState { return DogState() }
}

struct CatAction: Action {
func process() -> CatState { return CatState() }
}

struct AnyState: StateType {
let state: StateType

init(_ state: StateType) {
self.state = state
}
}

struct AnyAction: Action {
typealias ST = AnyState

let processor: () -> AnyState

init<T: Action>(_ a: T) {
self.processor = {
return AnyState(a.process())
}
}

func process() -> AnyState {
return processor()
}
}

let cat = AnyAction(CatAction())
let dog = AnyAction(DogAction())

let actions = [cat, dog]

actions.forEach { action in
action.process()
}

I still think that you should rethink your solution, this can get more complicated as your types increase.

Swift - Inherited Protocol Associated Type Erasure

Since Sub1 uses associated types, you cannot determine at runtime if a certain variable is of that type. Type erasers help to a certain degree, however it's hard to use multiple eraser types. My recommendation would be to overload the test method for every type you need to handle. This also adds more type safety to you code.

func test<S: Sub1, T>(_ s: S) where S.T == T

func test(_ s: Sub2)

The above solution won't work however for the scenario where you have a collection of Super elements, and you need to execute some actions based on the actual type. For this scenario a possible approach is to move the test method at the protocol level, and override in the child protocols.

protocol Super {
func test()
}

protocol Sub1: Super { associatedtype T }

protocol Sub2: Super {}

extension Sub1 {
func test() { ... do stuff for Sub1 }
}

extension Sub2 {
func test() { ... do stuff for Sub2 }
}

The downside is that conformers can override test, thus you'd loose the original implementation.

Swift Type Erasure attempt: Reference to invalid associated type

Your code doesn't compile because the associated type needs to be resolved at compile time by providing a concrete implementation for the Mover protocol.

What you can do, is to also erase the MoverType protocol:

struct AnyMover: MoverType {
private let mover: MoverType

init(_ mover: MoverType) {
self.mover = mover
}

func move() {
mover.move()
}
}

class AnyAnimal: Animal {
let mover: AnyMover

init<A: Animal>(animal: A) {
mover = AnyMover(animal.mover)
}
}

Use protocol with constrained associated type as property in Swift

To expand on my questions in the comments, looking at this code it looks like it would be exactly as flexible without adding AddressBookCellModelType or AddressBookViewModelType, and this would also get rid of the headaches, while still being generic over DataSourceCompatible.

// This protocol is fine and very useful for making reusable view controllers. Love it.
protocol DataSourceCompatible {
associatedtype CellModel
func cellModelForItem(at indexPath: IndexPath) -> CellModel
}

// No need for a protocol here. The struct is its own interface.
// This ensures value semantics, which were being lost behind the protocol
// (since a protocol does not promise value semantics)
struct AddressBookCellModel {
var name: String
var photo: UIImage?
var isInvited: Bool
}

// AddressBookViewModel conforms to DataSourceCompatible
// Its conformance sets CellModel to AddressBookCellModel without needing an extra protocol
class AddressBookViewModel: DataSourceCompatible {
let sectionedContacts: [[AddressBookCellModel]] = []
func cellModelForItem(at indexPath: IndexPath) -> AddressBookCellModel {
return sectionedContacts[indexPath.section][indexPath.row]
}
}

class AddressBookViewController: UIViewController {
private var viewModel: AddressBookViewModel!

func configure(viewModel: AddressBookViewModel) {
self.viewModel = viewModel
}
}

Doing it this way allows for a generic VC without introducing more pieces that required:

class DataSourceViewController<DataSource: DataSourceCompatible>: UIView {
private var viewModel: DataSource.CellModel!

func configure(viewModel: DataSource.CellModel) {
self.viewModel = viewModel
}
}

let vc = DataSourceViewController<AddressBookViewModel>()

Use Type Erasure return Generic Type in a function with Swift (Cannot convert return expression of type…)

EDIT to respond to the edit to the question:

createTwo doesn't work because you have the same misconception as I said in my original answer. createTwo decided on its own that F should be either String or Int, rather than "any type that conforms to Fooable".

For createOne, you have another common misconception. Generic classes are invariant. AnyFoo<String> is not a kind of AnyFoo<Fooable>. In fact, they are totally unrelated types! See here for more details.

Basically, what you are trying to do violates type safety, and you redesign your APIs and pick another different approach.


Original answer (for initial revision of question)

You seem to be having a common misconception of generics. Generic parameters are decided by the caller, not the callee.

In createOne, you are returning anyFoo, which is of type AnyFoo<Int>, not AnyFoo<P>. The method (callee) have decided, on its own, that P should be Int. This shouldn't happen, because the caller decides what generic parameters should be. If the callee is generic, it must be able to work with any type (within constraints). Anyway, P can't be Int here anyway, since P: FooProtocol.

Your createOne method should not be generic at all, as it only works with Int:

func createOne() -> AnyFoo<Int> {
let anyFoo = AnyFoo(p: FooImpClass())
return anyFoo
}

Swift: Is it possible to type erase a return type for use in a collection

Hopefully the following approach fits your needs. I did it only for getter just to simplify the demo, but the idea should be clear.

Note: used Xcode 11.2 / Swift 5.1 / Catalina

So here is your original entities

protocol Setting {
associatedtype Value
var value: Value { get }
}

struct ProjectSetting<T>: Setting {
let value: T
}

Now we need some helper protocols to hide your type differences, aka type erasers

private protocol TypeErasing {
var value: Any { get }
}

private struct TypeEraser<V: Setting>: TypeErasing {
let orinal: V
var value: Any {
return self.orinal.value
}
}

Now the core entity that wraps your concrete implementors holding different type values, but still allows to use those values and be stored in standard containers

struct AnySetting : Setting {
typealias Value = Any
private let eraser: TypeErasing
init<V>(_ setting: V) where V:Setting {
eraser = TypeEraser(orinal: setting)
}

var value: Any {
return eraser.value
}
}

Now testing your expectation

let settings = [AnySetting(ProjectSetting(value: 1)), AnySetting(ProjectSetting(value: "abc"))]

if let value = settings[0].value as? Int {
print("Stored value: \(value)")
}
if let value = settings[1].value as? String {
print("Stored value: \(value)")
}

PlayGround output


Stored value: 1
Stored value: abc


Related Topics



Leave a reply



Submit