Fogmeister Fogmeister - 2 months ago 10
Swift Question

The type of a struct that conforms to a generic protocol with an associated type that is also a protocol

This is very hard to put into words but I've created a minimal example.

Here is a gist if you'd prefer... https://gist.github.com/anonymous/67d83fb2f286cf84539b58be96a971d3

The "data item" protocol

I have a protocol which defines

Sortable
objects with a property
number
like so.

protocol Sortable: Comparable {
var number: Int {get}

static func < (lhs:Self, rhs: Self) -> Bool
static func == (lhs:Self, rhs: Self) -> Bool
}

struct BasicSortable: Sortable {
let number: Int

static func < (lhs:BasicSortable, rhs: BasicSortable) -> Bool {
return lhs.number < rhs.number
}

static func == (lhs:BasicSortable, rhs: BasicSortable) -> Bool {
return lhs.number == rhs.number
}
}


The "worker" protocol

Then I have a protocol that can do something with these
Sortable
types. But because it has a Self requirement it needs to be defined as a protocol with an associated type and in the structs as generic property...

protocol Sorter {
associatedtype Item: Sortable

func sort(items: [Item]) -> [Item]
}

// Two different sorters
struct AscendingSorter<T:Sortable>: Sorter {
typealias Item = T

func sort(items: [T]) -> [T] {
return items.sorted()
}
}

struct DescendingSorter<T:Sortable>: Sorter {
typealias Item = T

func sort(items: [T]) -> [T] {
return items.sorted{$0 > $1}
}
}


The handler

Finally a struct that pulls everything together...

struct DataHandler<T: Sortable> {
let items: [T]
let sortedItems: [T]

init(unsortedItems: [T]) {
items = unsortedItems

let sorter = AscendingSorter<T>()
sortedItems = sorter.sort(items: unsortedItems)
}
}


Making it all work

This all works.

let array = [
BasicSortable(number: 1),
BasicSortable(number: 8),
BasicSortable(number: 13),
BasicSortable(number: 3),
BasicSortable(number: 4),
BasicSortable(number: 14),
BasicSortable(number: 5),
BasicSortable(number: 12),
BasicSortable(number: 3),
]

let handler = DataHandler(unsortedItems: array)

handler.sortedItems


This prints out the array of items in the correct order depending on what type of sorter I create in the Handler

The problem

What I'm trying to do now is to find a property declaration for this
sorter
struct that can take ANY Sorter type into it but everything I have tried in doing that has failed so far.

Is there a way to do this?

In the struct I'd like to have...

let sorter: SomeTypeHere


And then in the init set it like...

sorter = AscendingSorter()


but no combination I have tried in doing this has worked.

Thanks

Answer

You could use type erasure to implement your own AnySorter.

Starting with your own code from above:

protocol Sortable: Comparable {
    var number: Int {get}

    static func < (lhs:Self, rhs: Self) -> Bool
    static func == (lhs:Self, rhs: Self) -> Bool
}

struct BasicSortable: Sortable {
    let number: Int

    static func < (lhs:BasicSortable, rhs: BasicSortable) -> Bool {
        return lhs.number < rhs.number
    }

    static func == (lhs:BasicSortable, rhs: BasicSortable) -> Bool {
        return lhs.number == rhs.number
    }
}

protocol Sorter {
    associatedtype Item: Sortable

    func sort(items: [Item]) -> [Item]
}

Construct an AnySorter:

struct AnySorter<Item: Sortable>: Sorter {

    private let _sort: ([Item]) -> [Item]

    init<S: Sorter where S.Item == Item>(_ sorter: S) {
        _sort = sorter.sort
    }

    func sort(items: [Item]) -> [Item] {
        return _sort(items)
    }
}

Which you make use of e.g. as an argument to the initializer in your DataHandler:

struct DataHandler<T: Sortable> {
    let items: [T]
    let sortedItems: [T]

    init(unsortedItems: [T], sorter: AnySorter<T>) {
        items = unsortedItems
        sortedItems = sorter.sort(items: unsortedItems)
    }
}

Your handler can now be used with a type erased AnySorter applied to your Sortable types. E.g., for the two simple sorters you've supplied in your question:

struct AscendingSorter<T:Sortable>: Sorter {
    typealias Item = T

    func sort(items: [T]) -> [T] {
        return items.sorted()
    }
}

struct DescendingSorter<T:Sortable>: Sorter {
    typealias Item = T

    func sort(items: [T]) -> [T] {
        return items.sorted{$0 > $1}
    }
}

/* example usage */ 
extension Int: Sortable {
    var number: Int { return self }
} 

let arr = [1, 4, 2, 8, 3]

let dataHandlerDesc = DataHandler(unsortedItems: arr, sorter: AnySorter<Int>(DescendingSorter()))
print(dataHandlerDesc.sortedItems) // [8, 4, 3, 2, 1]

let dataHandlerAsc = DataHandler(unsortedItems: arr, sorter: AnySorter<Int>(AscendingSorter()))
print(dataHandlerAsc.sortedItems) // [1, 2, 3, 4, 8]

Edit addition to answer your comment:

Is it possible to take the input parameter and store it in a property? Would I just use AnySorter<T> as the type of the property?

Yes, you can keep a property in DataHandler with type AnySorter. E.g., for a contrived example, we can let sortedItems be a computed property that makes use of an AnySorter instance to sort a stored list of items (of course in reality we don't want to do this re-sorting for each call, but for this example only!):

struct DataHandler<T: Sortable> {
    let items: [T]
    var sortedItems: [T] { return sorter.sort(items: items) }
    var sorter: AnySorter<T>

    init(unsortedItems: [T], sorter: AnySorter<T>) {
        items = unsortedItems
        self.sorter = sorter
    }

    mutating func changeSorter(newSorter: AnySorter<T>) {
        sorter = newSorter
    }
}

/* example usage */ 
extension Int: Sortable {
    var number: Int { return self }
} 

let arr = [1, 4, 2, 8, 3]

var dataHandler = DataHandler(unsortedItems: arr, sorter: AnySorter<Int>(DescendingSorter()))
print(dataHandler.sortedItems) // [8, 4, 3, 2, 1]

dataHandler.changeSorter(newSorter: AnySorter<Int>(AscendingSorter()))
print(dataHandler.sortedItems) // [1, 2, 3, 4, 8]