ceylon / ceylon-spec

DEPRECATED
Apache License 2.0
108 stars 34 forks source link

Ability to daisy-chain comparisons #84

Open FroMage opened 12 years ago

FroMage commented 12 years ago

The #1 biggest let down with comparison operators like <=> or Comparable.compareTo() in Java is that you can't easily daisy-chain them:

int compareTo(Other other){
 int result = firstName.compareTo(other.firstName);
 if(result != 0)
  return result;
 result = lastName.compareTo(other.lastName);
 if(result != 0)
  return result;
 result = age.compareTo(other.age);
 if(result != 0)
  return result;
 ...
}

When in Perl you'd write:

sub compareTo(){
 my ($this, $other) = @_;
 return $this->firstName <=> $other.firstName
  || $this->lastName <=> $other.lastName
  || $this->age <=> $other.age;
}

Because boolean operators like || return truthy values (-1 and 1 are truthy while 0 is falsy), and <=> returns a number;

We should find a way to support this somehow, in a style compatible with Ceylon, because writing comparators is a very frequent task.

gavinking commented 12 years ago

This would be doable if/when we have some kind of lazy evaluation.

Of course, we could generalize the || and && operators to some kind of Truthy<T> interface that is satisfied by both Boolean and Comparison. But I'm not sure if I like that idea.

FroMage commented 12 years ago

If we do that we'll have requests for Object foo = objA || objB;

gavinking commented 12 years ago

If we do that we'll have requests for Object foo = objA || objB;

We already have that, don't we? The ? operator.

FroMage commented 12 years ago

Doesn't matter that we do, people will want non-null values to be Truthy.

gavinking commented 12 years ago

Doesn't matter that we do, people will want non-null values to be Truthy.

:-(

FroMage commented 12 years ago

I'm arguing against myself here of course, I learn quick :)

gavinking commented 12 years ago

OK, so indeed there is a reasonable solution to this problem. It would be to change the definition of <=>, and I guess also of the compare() method of Comparable to evaluate to null when the values are equal. So you could write your example as:

Comparison? compare(Person other) {
    return firstName<=>other.firstName
            else lastName<=>other.lastName
            else age<=>other.age;
}

This is noticeably cleaner than either Java or Perl.

If we did this, I guess I would also be inclined to have <=> accept null operands, evaluating to null in the case that either operand is null.

gavinking commented 12 years ago

Actually if we didn't want to go with this <=> evaluating to null thing, we could add the following function

Comparison? unequal(Comparison comparison) { return comparison!=equal then comparison; }

Letting you write:

Comparison compare(Person other) {
    return unequal(firstName<=>other.firstName)
            else unequal(lastName<=>other.lastName)
            else unequal(age<=>other.age)
            else equal;
}

Which is more verbose but leaves intact the definition of Comparison.

gavinking commented 12 years ago

One thing that plays into this discussion is the question of partial orders, like what we have with Person. In that case, the set of outcomes of compare() should include "not comparable", i.e. null. Not quite sure how this consideration changes things.

gavinking commented 12 years ago

Hrm, why not introduce a <> operator which returns larger, smaller, or null, leaving the definition of <=> alone?

Comparison? compare(Person other) {
    return firstName<>other.firstName
        else lastName<>other.lastName
        else age<>other.age;
}

I like this. A lot.

FroMage commented 12 years ago

Look, honestly I can live with unequal, and I don't think we can spend a new operator out of operator budget for such a specific corner-case. I like Perl's daisy chaining, but I don't think it fits in Ceylon, as it is a consequence of truthy/fasly throughout the whole language. Having <=> return smaller/null/larger also feels weird, but perhaps @emmanuelbernard, @quintesse or @tombentley have a different opinion?

gavinking commented 12 years ago

I definitely don't think we should make <=> return null in the case of equal. I mean, the actual characters themselves look like they are saying "less than, equal, or greater than". And by the same token, I think it's totally natural to introduce <> to mean "less than, or greater than".

The way I look at it is you can chain comparisons in equals() using == and &&. It feels to me pretty natural to be able to do the same thing in compare() using <> and else.

tombentley commented 12 years ago

I agree there's a lot of boilerplate in writing a comparator. I don't find the whole null returning <> operator very intuitive though. I agree with @FroMage that it feels like we're catering for a quite specific use case here and I think people will get confused by having two very similar operators serving almost the same purpose.

It would be nice if there was some way we could generate a comparator just given the attributes which should be involved, (and if the compiler could optimize this for the common case of the attributes being known at compile time, even better). Even if the compiler couldn't do this, it should be provided by the IDE (if it's not already).

luolong commented 11 years ago

I think reasonable alternative would be to provide something akin to Google's ComparisonChain.

Then we can just write something like this:

shared actual Comparison compare( Other other ) {
    return ComparisonChain()
        .compare(this.name, other.name)
        .compare(this.age, other.age)
        .compare(this.methodName, other.methodName)
        .result();
}

It's just a raw idea. there is probably a more Ceylonic

BTW: I must admit treating result of <=> operation as a Truthy/Falsy value in the context of else statement does make a whole lot of sense.

sgalles commented 10 years ago

For all intents and purposes, in the meantime if someone stumbles upon this discussion and is looking for a temporary solution for basic use cases, this is a simple Ceylon class that mimics the new Java 8 Comparator#comparing : https://gist.github.com/sgalles/e5e8cfe63f4e1a6debf5 It allows to write :

value comparator = Comparing{
    byIncreasing(Person.lastName),
    byIncreasing(Person.firstName)
}.comparator;

(update : edited to use the second, more ceylonic version of the class)