A more understandable IComparable
"Any fool can write code that a computer can understand. Good programmers write code that humans can understand." - Martin Fowler
In the .NET framework the IComparable<T> interface is used to compare values:
public interface IComparable<T> {
int CompareTo(T other);
}
The comparison is done with the CompareTo method. It returns zero if the current object is equal to the passed in parameter; a negative number if it's less than the parameter; and a positive number if it's greater than it. That's a very non-intuitive return value, reminiscent of the C strcmp function. I know that this scheme is already ingrained in our brains, but it creates code that's not directly decipherable:
static int BinarySearch<T>(IList<T> list, T target) where T : IComparable<T> {
if (list == null) throw new ArgumentNullException("list");
int lower = 0;
int upper = list.Count - 1;
while (lower <= upper) {
int middle = lower + (upper - lower) / 2;
int compare = target.CompareTo(list[middle]);
if (compare == 0) return middle;
if (compare < 0) upper = middle - 1;
else lower = middle + 1;
}
return -1;
}
...
int[] mySortedArray = ...
int indexOfTheAnswer = BinarySearch(mySortedArray, 42);
(Note: this is just an example, there're already binary searches implementations in classes Array and List<T>.)
Wouldn't it be nicer to use the comparison operators instead of the clumsy CompareTo method? C# doesn't support extension operators, so in order to provide the operators to any IComparable<T>, a wrapper type must be used:
public class ComparableWithOperators<T> where T : IComparable<T> {
T _comparable;
public ComparableWithOperators(T comparable) {
if (comparable == null) throw new ArgumentNullException("comparable");
_comparable = comparable;
}
public static implicit operator ComparableWithOperators<T>(T comparable) {
return new ComparableWithOperators<T>(comparable);
}
public static bool operator <(ComparableWithOperators<T> left, ComparableWithOperators<T> right) {
return left._comparable.CompareTo(right._comparable) < 0;
}
public static bool operator >(ComparableWithOperators<T> left, ComparableWithOperators<T> right) {
return left._comparable.CompareTo(right._comparable) > 0;
}
public static bool operator <=(ComparableWithOperators<T> left, ComparableWithOperators<T> right) {
return left._comparable.CompareTo(right._comparable) <= 0;
}
public static bool operator >=(ComparableWithOperators<T> left, ComparableWithOperators<T> right) {
return left._comparable.CompareTo(right._comparable) >= 0;
}
public static bool operator ==(ComparableWithOperators<T> left, ComparableWithOperators<T> right) {
return left._comparable.CompareTo(right._comparable) == 0;
}
public static bool operator !=(ComparableWithOperators<T> left, ComparableWithOperators<T> right) {
return left._comparable.CompareTo(right._comparable) != 0;
}
...
}
This class implements the comparison operators for itself, and since there's also an implicit cast operator from a T, which is constrained to be an IComparable<T>, it can also compare itself to any IComparable<T>. So, the binary search example can now be changed to this:
static int BinarySearch<T>(IList<T> list, ComparableWithOperators<T> target) where T : IComparable<T> {
if (list == null) throw new ArgumentNullException("list");
int lower = 0;
int upper = list.Count - 1;
while (lower <= upper) {
int middle = lower + (upper - lower) / 2;
var candidate = list[middle];
if (target == candidate) return middle;
if (target < candidate) upper = middle - 1;
else lower = middle + 1;
}
return -1;
}
...
int[] mySortedArray = ...
int indexOfTheAnswer = BinarySearch<int>(mySortedArray, 42);
But now the caller must provide the type parameter to the method, because the compiler's type inference can't figure it out. Also, sometimes you already have an IComparable<T>, and to use the operators in the same context, you'd need to convert it to the wrapper class:
IComparable<T> comparable = ... ComparableWithOperators<T> comparableWithOperators = comparable;
To make this statement read better, a converter extension method can be used:
public static class ComparableExtensions {
public static ComparableWithOperators<T> WithOperators<T>(this T self) where T : IComparable<T> {
if (self == null) throw new ArgumentNullException();
return self;
}
}
Now the statement will read:
IComparable<T> comparable = ... var comparableWithOperators = comparable.WithOperators();
And the binary search can be implemented like this:
static int BinarySearch<T>(IList<T> list, T target) where T : IComparable<T> {
if (list == null) throw new ArgumentNullException("list");
int lower = 0;
int upper = list.Count - 1;
while (lower <= upper) {
int middle = lower + (upper - lower) / 2;
var candidate = list[middle].WithOperators();
if (target == candidate) return middle;
if (target < candidate) upper = middle - 1;
else lower = middle + 1;
}
return -1;
}
This solution also alleviates the requirements for new classes that implement IComparable<T>. They don't need to provide the comparison operators by themselves anymore, which would add a lot of boilerplate code. The same logic can be used to tackle the IEquatable<T> interface as well.
Note that F# has a (meta) generic type constraint, named comparison, which gives us the desired syntax out of the box:
let BinarySearch<'a when 'a : comparison>(list: IList<'a>, target: 'a) =
if list = null then raise(ArgumentNullException("list"))
let rec Loop(lower, upper) =
if (lower <= upper) then
let middle = lower + (upper - lower) / 2
let candidate = list.[middle]
if (candidate = target) then middle
elif (target < candidate) then Loop(lower, middle - 1)
else Loop(middle + 1, upper)
else -1
Loop(0, list.Count - 1)
Comments
Post a Comment