2

I am trying to pass a multiple tuples containing a KeyPath and a type of a sort order to a method that should do sorting.

I have this method:

extension Array {
    mutating func sort<T: Comparable>(by criteria: (path: KeyPath<Element, T>, order:OrderType)...) {
        
        criteria.forEach { path, order in
            //...
            sort { first, second in
            order.makeComparator()(
                first[keyPath: path],
                second[keyPath: path]
            )
        }
        }
    }
}

and I am using it like this:

var posts = BlogPost.examples
        
posts.sort(by:(path:\.pageViews, order: .asc), (path:\.sessionDuration, order: .desc))

Now, cause both pageViews and sessionDuration properties are integers, this will work.

But if I want to pass two properties of different types (say String and Int), I am getting this error:

Key path value type 'Int' cannot be converted to contextual type 'String'

Here is rest of the code, but I guess is not that relevant:

enum OrderType: String {
    case asc
    case desc
}

extension OrderType {
    func makeComparator<T: Comparable>() -> (T, T) -> Bool {
        switch self {
        case .asc:
            return (<)
        case .desc:
            return (>)
        }
    }
}

How should I define sort method so that I it accept heterogenous key paths?

Whirlwind
  • 14,286
  • 11
  • 68
  • 157
  • There are no variadic generics (yet), so you'll need to write a `AnyComparable` type eraser. See the one I wrote [here](https://stackoverflow.com/questions/68878549/class-anycomparable-inherits-from-class-anyequatable/68878884#68878884) for more info. – Sweeper Jan 07 '22 at 11:33
  • Not quite related to your question, but I don't think your implementation of `sort` is quite right... Wouldn't that just be equivalent to sorting by the last key path? – Sweeper Jan 07 '22 at 11:35
  • @Sweeper Yes, it only sorts the last parameter for now. I just copied as it is right now, but yeah, not related to a current problem. Cause I can't even pass what I want to pass :) – Whirlwind Jan 07 '22 at 11:38
  • @Sweeper Thanks, `AnyComparable` did the trick. – Whirlwind Jan 07 '22 at 12:04
  • Actually I just realised I have made a mistake in that answer. It is fixed now. I think I'll still post an answer here, since the `AnyComparable` there can be adapted quite a lot to fit your needs here. – Sweeper Jan 07 '22 at 12:11

1 Answers1

1

There are no variadic generics (yet), so you'll need to write a AnyComparable type eraser. This one is adapted from this post here.

struct AnyComparable: Equatable, Comparable {
    private let lessThan: (Any) -> Bool
    private let value: Any
    private let equals: (Any) -> Bool

    public static func == (lhs: AnyComparable, rhs: AnyComparable) -> Bool {
        lhs.equals(rhs.value) || rhs.equals(lhs.value)
    }
    
    public init<C: Comparable>(_ value: C) {
        self.value = value
        self.equals = { $0 as? C == value }
        self.lessThan = { ($0 as? C).map { value < $0 } ?? false }
    }

    public static func < (lhs: AnyComparable, rhs: AnyComparable) -> Bool {
        lhs.lessThan(rhs.value) || (rhs != lhs && !rhs.lessThan(lhs.value))
    }
}

With that, you can write your sort method signature as:

mutating func sort(by criteria: (path: KeyPath<Element, AnyComparable>, order:OrderType)...) {
    
    
}

To make it easier for us to pass key paths with the type AnyComparable in, we can make an extension:

extension Comparable {
    // this name might be too long, but I'm sure you can come up with a better name
    var anyComparable: AnyComparable {
        .init(self)
    }
}

Now we can do:

someArray.sort(by: (\.key1.anyComparable, .asc), (\.key2.anyComparable, .asc))
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Hm, ok, I didn't notice something is wrong, cause I didn't try sorting, but the general idea works for me. I tried this code, and sorting works nice. Thank you. – Whirlwind Jan 07 '22 at 12:32