4

As an exercise in learning Swift, I'm trying to create minimum and maximum extension methods on Array using a shared method called comparest. Everything is tested and works well, except for one thing: I cannot use the minimum function without a parameter. I've specified a default parameter value of nil, but Apple's Swift compiler does not like it when I call minimum without this parameter.

Here are my method definitions. They compile just fine.

extension Array {

  func comparest<C: Comparable>(comparator: T -> C, _ op: (C, C) -> Bool) -> T? {
     var min = self.first
     for elem in self {
         if op(comparator(elem), comparator(min!)) {
            min = elem
         }
     }
     return min
  }                     

  func minimum<C: Comparable>(var _ comparator: (T -> C)? = nil) -> T? {
     if comparator == nil {
         comparator = { elem -> C in elem as C }
     }
     return self.comparest(comparator!, { $0 < $1 }) 
  }

}

(If you're wondering why I say comparest(comparator!, { $0 < $1 }) instead of comparest(comparator!, <), see my related question.)

If I say

var array = [3, 4, 1, 9]
var min = array.minimum({ $0 })

everything works well. However, I designed this method so that the parameter passed to minimum could be omitted, in which case the array element itself is used as the comparison value, e.g.

var array = [3, 4, 1, 9]
var min = array.minimum()

The compiler hates this. It says "Cannot convert the expression's type () to Int?". I'm clueless here. Any thoughts?

(As a quick aside, the reason for the parameter is if you want to compare object by, say, the value of a property, e.g., routes.map( { $0 as MKRoute }).minimum({ $0.distance }) will give you the route with the minimum distance.)

Community
  • 1
  • 1
Gregory Higley
  • 15,923
  • 9
  • 67
  • 96

3 Answers3

1

The elements in an array don't have to be Comparable, which means you can't write that function for all possible Arrays.

Unfortunately Swift doesn't allow you to extend just a subset of Arrays. In other for your function to work, the compiler needs extra type information to infer the type C.

Others have struggled with this same issue:

How can we create a generic Array Extension that sums Number types in Swift?

Instead of using an extension, you should be able to use minimum as a global function.

Community
  • 1
  • 1
ColinE
  • 68,894
  • 15
  • 164
  • 232
  • OK, after reading the linked question and thinking about the type system, I understand what's happening, but I have to confess I don't like it much. Swift's generics and extensions feel a little bit broken to me. Maybe 'incomplete' is the best word. Even C# does a much better job here, allowing extension methods to be specialized on constrained generic types. Oh well, perhaps the ability to write something like `extension Array` will come as the language matures. – Gregory Higley Oct 07 '14 at 06:01
  • 1
    @GregoryHigley I completely agree. I really want to be able to extend `Array`, not 'all' arrays. – ColinE Oct 07 '14 at 11:03
  • By the way, `minimum` as a global function feels ugly to me. I see that Apple has provided a `contains` function that is also global, but of course the only reason this exists is because of how the current extension system works. If protocols could be extended instead of just concrete types, `contains` would be an extension method. Swift has a few speed bumps, it seems. :) – Gregory Higley Oct 07 '14 at 21:09
1

I think the compiler cannot infer what type C is, since the caller expression isn't saying C is what. So, the Swift compiler as its current behavior substitutes Void (= ()) for C. (It should be reported as an error for reducing mistakes, I'd say)

findall
  • 2,176
  • 2
  • 17
  • 21
0

While I won't change the accepted answer, I thought I'd post for posterity my working solution. Although I didn't introduce a new protocol or monoid as suggested by Gabrielle Petronella, it did give me the idea of giving more "evidence" to the compiler to help it along.

This solves the default closure parameter problem by overloading the methods, especially because it turns out that each method needs a different return type.

Here's what I came up with:

extension Array
{
    func comparest<C: Comparable>(comparable: T -> C, _ op: (C, C) -> Bool) -> T? {
        var est = self.first
        if count > 1 {
            for elem in self[1..<count] {
                if op(comparable(elem), comparable(est!)) {
                    est = elem
                }
            }
        }
        return est
    }

    func comparest<C: Comparable>(op: (C, C) -> Bool) -> C? {
        var array = map({ $0 as C })
        var est = array.first
        if count > 1 {
            for elem in array[1..<count] {
                if op(elem, est!) {
                    est = elem
                }
            }
        }
        return est
    }

    func minimum<C: Comparable>(comparable: T -> C) -> T? {
        return comparest(comparable) { $0 < $1 }
    }

    func minimum<C: Comparable>() -> C? {
        return comparest() { $0 < $1 }
    }

    func maximum<C: Comparable>(comparable: T -> C) -> T? {
        return comparest(comparable) { $0 > $1 }
    }

    func maximum<C: Comparable>() -> C? {
        return comparest() { $0 > $1 }
    }
}

Sadly, in all cases, type inference is not possible. For instance,

var x = [5, 9, 1, 3].minimum()

This will fail because the type system does not know that T (effectively) = C. To fix it, you have to provide the appropriate type:

var x: Int? = [5, 9, 1, 3].minimum()

This works! My next task is to tighten things up, probably with a tuple, so that the internal comparable closure is only called once per element. Lastly, the single-parameter comparest could probably be reimplemented with reduce.

Community
  • 1
  • 1
Gregory Higley
  • 15,923
  • 9
  • 67
  • 96