As with most programming problems, the problem becomes vastly easier by choosing the right data model and data structures and leaning on existing algorithms than rolling your own.
Most modern mainstream programming languages already provide an algorithm for finding the maximum value in a collection, so we simply call that instead of writing our own. Typically, the only requirement this algorithm has is that our collection elements must define a total order among them.
So, all we need to do is define a total order among our Obj
s. And again, most modern programming languages already provide a protocol for us to implement.
Also, most modern mainstream programming languages provide some sort of product type data structure, like a tuple, which already implements the ordering protocol using a lexicographic ordering, which is exactly what we want: so, all we need is to provide an ordering key in the form of such a product type based on the state of Obj
.
Here is a (runnable) example in Ruby:
Obj = Data.define(:a, :b, :c) do
include Comparable
def <=>(other) = ordering_key <=> other.ordering_key
protected def ordering_key = [a, b, c.size]
end
o1 = Obj.new(1, 1, "")
o2 = Obj.new(1, 1, "9")
o3 = Obj.new(1, 1, "11")
[o1, o3, o2].max #=> #<data Obj a=1, b=1, c="11">
First, we mix the Comparable
mixin into our Obj
class:
This makes instances of Obj
comparable to each other and defines the methods Comparable#<
, Comparable#<=
, Comparable#==
, Comparable#>
, Comparable#>=
, Comparable#between?
, and Comparable#clamp
for us. The Comparable
protocol is very simple and only requires us to define one method: <=>
, the so-called spaceship operator.
At first glance, this doesn't help us much because in order to define the spaceship operator, we still need to implement all that logic. But actually, that is not true: in the implementation of the spaceship operator, we can forward the call to another object which already has the spaceship operator implemented in the way we want.
And luckily, one such object already exists: Array
s are ordered lexicographically, i.e., first by their first element, then by their second element, then their third, and so on. Which is exactly what we need: we want Obj
to be ordered first by a
, then by b
, and then by c
's length, so all we need is an Array
whose first element is a
, whose second element is b
, and whose third element is c.size
.
We could do this inline in our implementation of <=>
, but instead I have chosen to provide an attribute reader named ordering_key
.
Now that we have made sure that our Obj
s have a well-defined total ordering, we can use existing Enumerable
methods such as Enumerable#max
to find the maximum element in a collection.
Similarly, here is a (runnable) example in Scala:
final case class Obj(a: Int, b: Float, c: String) extends Ordered[Obj]:
private val orderingKey = (a, b, c.size)
override def compare(that: Obj) =
import Ordered.orderingToOrdered
orderingKey compare that.orderingKey
val o1 = Obj(1, 1, "")
val o2 = Obj(1, 1, "9")
val o3 = Obj(1, 1, "11")
Seq(o1, o3, o2).max //=> Obj(1, 1.0, 11)
It works the same way, mixing in the scala.math.Ordered
trait and implementing the Ordered.compare
abstract method by relying on Tuple
's implicit instance of Ordering
, and then using Iterable.max
to find the maximum of a Seq
uence of Obj
s.
And finally, this is what it looks like in Java:
import com.andrebreves.tuple.Tuple;
import com.andrebreves.tuple.Tuple3;
record Obj(int a, float b, String c) implements Comparable<Obj> {
private Tuple3<Integer, Float, Integer> orderingKey() {
return Tuple.of(a, b, c.length());
}
@Override public int compareTo(Obj o) {
return orderingKey().compareTo(o.orderingKey());
}
}
import java.util.stream.Stream;
var o1 = new Obj(1, 1, "");
var o2 = new Obj(1, 1, "9");
var o3 = new Obj(1, 1, "11");
var best = Stream.of(o1, o3, o2).max(Obj::compareTo);
System.out.println(best); // Optional[Obj[a=1, b=1.0, c=11]]
For some reason, I could not find a suitable tuple implementation in the Java SE JRE (seems like a weird oversight to me, considering how much stuff there is otherwise in there, but I digress), so I just randomly typed "Java Tuple" into Google and downloaded the first library I could find. There are others, all of them should work just fine.
Like with the other implementations, we define an ordering key, and then implement the Comparable
interface's compareTo
method by forwarding to the ordering key's implementation of compareTo
. Then, we use Stream
's max
method to find the best Obj
by satisfying max
's Comparator
requirement with a method reference to our compareTo
method.
Technically, we don't get our "best" Obj
but an Optional<Obj>
because there is no guarantee that there will be a "best" object – after all, there could be no object, in which case, there won't be a "best".
We have now achieved a couple of nice properties:
- The knowledge of how to compare itself to another object is now part of the object, not some external code.
- By forwarding the responsibility to the tuple, the comparison code is much cleaner.
- We make the collection responsible for finding the maximum, so we don't have to.
==
or!=
is most often a bad idea if the floats are the results of computations, because floating-point calculations result in approximate results, and even the smallest approximation might result in two floating-point numbers testing as unequal. GCC even has a standard warning option-Wfloat-equal
to produce a warning whenever floats are compared for equality.