Hi guys, newbie question about kotlin comparison. ...
# announcements
a
Hi guys, newbie question about kotlin comparison. I want to create a data class that has some fields. And I want to make this comparable. I know I can implement Comparable interface. Is there facility in kotlin to implement the compareTo function? I see there is a compareValuesBy I can use but it wanted to avoid to do compareValuesBy for every field. What am I missing?
s
You'll have to implement the compareTo function manually. The compiler cannot guess how you want to combine multiple values of [-1, 0, 1] (one for each field) into one result of [-1, 0, 1].
a
that makes sense, but say I have a data class like this:
Copy code
data class User(id: String, name: String, age: Int): Comparable<User>
is there a way to leverage the default comparison field by field?
as in, I would like to specify the list of fields on which to execute the comparison, and just do the default comparison
s
You could use reflection to implement a function that takes a KClass as a generic type param, two instances of that KClass and an (ordered) list of KProperty-s of that KClass (through vararg). Iterate over these properties,
get
them for each of the two instances and handle the result [-1, 0, 1].
a
gotcha, was looking at https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.comparisons/compare-values-by.html to see whether that could solve this but it seems it does not 😕
v
Besides that your constructor declaration is invalid, why not simply
Copy code
data class User(val id: String, val name: String, val age: Int) : Comparable<User> {
    override fun compareTo(other: User) =
            compareBy(User::id, User::name, User::age)
                    .compare(this, other)
}
?
👍 1
Or maybe better
Copy code
data class User(val id: String, val name: String, val age: Int) : Comparable<User> {
    override fun compareTo(other: User) = comparator.compare(this, other)
    companion object {
        private val comparator = compareBy(User::id, User::name, User::age)
    }
}
s
Yup, I didn't realize a KProperty could be coerced to
(T) -> Comparable<*>
, @Vampire's answer looks great.
v
Well, if it wouldn't work, you could still do
compareBy<User>({ it.id }, { it.name }, { it.age })
n
It's a shame that data classes don't simply provide this out of the box
given they provide == and hash, ordering makes sense as well, allowing them to be used in ordered collections as easily as hashed ones
a
yeah well it makes sense in a way
v
Well, suggest it to JetBrains if you think so ;-)
a
@Vampire what if one field is a map? that becomes a bit messier isn’t it?
n
Why does it make sense?
a
say
Copy code
data class User(val id: String, val name: String, val age: Int, val metadata: Map<String, String>)
n
Sorry, I thought you are saying that it makes sense that they don't, but mayb eI misunderstood
v
A map is not comparable, is it?
So you have to extract the value to compare yourself with the lambda version or leave out the field, whatever
a
what do you mean by “extract the value to compare yourself with the lambda version”?
v
Probably one of the reasons data classes cannot do this by default, because it would only work with only comparable fields
n
Well, the logical thing to do is to simply make it non comparable if it has a comparable field
you have the same issue with equality, in principle
the difference is that in kotlin, == seems to always be defined for a type
which is pretty bad
v
Copy code
data class User(val id: String, val name: String, val age: Int, val metadata: Map<String, String>) : Comparable<User> {
    override fun compareTo(other: User) = comparator.compare(this, other)
    companion object {
        private val comparator = compareBy(
            User::id,
            User::name,
            User::age,
            { generate a comparable value out of the map here }
        )
    }
}
n
looks like kotlin silently falls back to identity based equality
v
Well, the logical thing to do is to simply make it non comparable if it has a comparable field
That doesn't make sense
you have the same issue with equality, in principle
No, because everything can calculate equality, but not everything is comparable
looks like kotlin silently falls back to identity based equality
As does Java and any other similar language.
n
Why doesn't it make sense?
sorry, I obviously meant "if it has a non-comparable field"
v
Well, read it again, you probably forgot a "non" 😄
n
And yes, I'm aware that Java does that. Doesn't make it a good idea though.
Although I can understand why that would cause Kotlin to do it.
v
Of course it is a great idea. Why should an instance not be equal to itself? o_O
That would be a very bad idea
n
Err, no 🙂
It's a pretty bad idea for the same operator to silently and without telling you, end up meaning totally different things for different types
If you want to check object identity, have a separate operator for it
s
Like
===
?
n
It doesn't really matter how you spell it, but sure
python uses
is
for this, for example, though
is
is already something much more useful obviously
v
There is a separate operator for identity, it is called
is
n
Ah I didn't know kotlin's
is
can be used for that
it can't
just tested
v
Ah, no, sorry, it is tripple equals, so
===
s
yup, that’s what I suggested 🙂
n
Excellent, so Kotlin already has this 🙂 So don't let == become identity 🙂. @streetsofboston i didn't realize you were saying it's already in the language.
v
But still, if you want to check equality and have a type that does not override
equals
, why would anyone think it is a good idea to return
false
if you compare something to itself?
s
a == b
is short for
a.equals(b)
n
err, nobody is suggesting that
v
You did
n
Nope 🙂
I suggest making it not compile
which is how it works in languages with stronger type systems, typically
v
That makes even less sense tbh
But that's just my opinion
You are free to suggest it to JetBrains for a future version of the language. 😉
n
It won't happen because Kotlin is tied so closely to Java, and that's how it works in Java.
It's both Kotlin's greatest strength and its greatest weakness 🙂
this won't change any more than type erasure will, and they both make about equally as much sense 🙂
v
Btw. @streetsofboston to be correct,
a == b
is
a?.equals(b) ?: (b === null)
😉
n
that's ok though, not the end of the world
one thing you might consider is that following your logic
why is it that comparing unrelated types fails to compile, instead of returning false?
v
You can also compile Kotlin to JavaScript and Native code
why is it that comparing unrelated types fails to compile, instead of returning false?
is it?
n
yes, it does fail to compile
which is a big improvement over Java's .equals, and surprisingly, scala's ==
s
This
"hello" == 5
fails to compile, since it will always return false. But if you upcast the instances of both sides to
Any
, it will compile and return false
v
Well, it will always be
false
, so where is the point in comparing them?
I guess even upcasting one side should be sufficient
n
Sure. But, I'm just following your own reasoning to its logical conclusion (and the conclusion of Java-like languages).
s
Copy code
"hello" == 5 // Error
"hello" as Any == 5 as Any // Compiles fine
n
if you want to compare by identity, why not force people to write === so it's clearer to read?
Etc
v
Because it is not up to the user to decide what defines equality of two instances, but the creator of that class
n
In Kotlin, if you have a type without == in a dataclass, then your dataclass is now, semantically, silently no longer a value type
v
If he doesn't decide for any logical equality, at least an instance is equal to itself
Which makes perfect sense
n
It's not "equal" to itself; it is itself. He hasn't defined equality at all. There are important ideas in equality, that relate to value semantics; this is a programming language independent concept
For any type claiming to be
data class
for example, I'd expect that constructing it twice with identical arguments, should give me two equal instances. That's what a well behaved value type looks like.
v
Please don't waste my time anymore, I have work to do. If you think it is bad, suggest to change it. I simply do not agree to your reasoning and you are too into your opinion to change mind either
n
🤷 you literally could have said nothing and the convo would have ended, you preferred to be rude for some reason. I'd suggest exposure to languages outside Java and Kotlin, if I'm not an effective vehicle for changing minds 🙂
s
Kotlin is not a functional programming language, where you’d need to define the
Eq
typeclass for a type to be able to check for equality… Instead, it chose to provide the
equals
method on the top-most object
Any
that can be overridden and its default impl is
this === that
. Different choices were made.
n
@streetsofboston I don't really think this has anything to do with functional programming?
v
I was not rude, just definite, as I have work to do for which I actually get paid. It is more rude to harvest foreign threads with only loosely related discussions. 😉
n
@Vampire you could have been doing that work in the time you typed the last two messages 🙂
v
Well, I'm too polite to just turn around and go, but at least tell my opposite. But now I'm really ignoring anymore messages by you here, or I'm actually really getting rude.
@Andrea Giuliano hope you didn't miss my answer to your last question in all this unrelated chatter. In case you did, here it is: https://kotlinlang.slack.com/archives/C0922A726/p1601558923246900?thread_ts=1601556289.241000&amp;cid=C0922A726
n
@Vampire 3
@streetsofboston If you want non functional examples, neither Swift nor C++ would allow == to silently resolve to identity semantics
if the actual reason is related to Java interop, or even perhaps because it would cause other issues that would be confusing for Java devs, as always, that's always a consideration in the context of Kotlin. But, it's a pretty hard-to-defend choice in a vacuum.
a
@Vampire thanks a lot for the repost. Yeah I was that. I was trying to convert some old jjava class that also compares a Map<String, String> as field . To do that the old (sorry to call it ugly) compare method does the following with the map
Copy code
public int compare(Map<String, String> first, Map<String, String> second) {
        Iterator<Map.Entry<String, String>> firstIterator = first.entrySet().iterator();
        Iterator<Map.Entry<String, String>> secondIterator = second.entrySet().iterator();
        while (true) {
            Boolean firstHasNext = firstIterator.hasNext();
            Boolean secondHasNext = secondIterator.hasNext();
            int result = firstHasNext.compareTo(secondHasNext);
            if (result != 0 || !firstHasNext) {
                return result;
            }
            // we have two entries to compare
            Map.Entry<String, String> n1 = firstIterator.next();
            Map.Entry<String, String> n2 = secondIterator.next();
            result = n1.getKey().compareTo(n2.getKey());
            if (result != 0) {
                return result;
            }
            result = n1.getValue().compareTo(n2.getValue());
            if (result != 0) {
                return result;
            }
        }
    }
I’m still trying to figure out a way to make that looks nicer with the kotlin snippet you suggested which I believe is very elegant
as an update, I managed to create a keyAndThenValueComparator this way
Copy code
private val keyComparator =
            compareBy<Map.Entry<String, String>> { it.key }

        private val keyThenValueComparator =
            keyComparator.thenComparator { a, b -> compareValues(a.value, b.value) }
my only missing bit there is how to replicate the check done in the snippet above on the sizing, anyone has any suggestion?
v
I think you could use for example this, at least if the metadata is not too big as this solution will build one string from all metadata:
Copy code
data class User(
        val id: String,
        val name: String,
        val age: Int,
        val metadata: Map<String, String>
) : Comparable<User> {
    override fun compareTo(other: User) = comparator.compare(this, other)

    companion object {
        private val comparator = compareBy(
                User::id,
                User::name,
                User::age,
                { user ->
                    user
                            .metadata
                            .entries
                            .flatMap { it.toPair().toList()}
                            .joinToString("\u0000")
                }
        )
    }
}
A maybe better version that does not have this problem (but untested as also was the other examples):
Copy code
data class User(
        val id: String,
        val name: String,
        val age: Int,
        val metadata: Map<String, String>
) : Comparable<User> {
    override fun compareTo(other: User) = comparator.compare(this, other)

    companion object {
        private val entryComparator: Comparator<Entry<String, String>> = compareBy(
                { it.key },
                { it.value }
        )

        private val comparator = compareBy(
                User::id,
                User::name,
                User::age,
        ).thenBy(Comparator { entries1, entries2 ->
            entries1.asSequence()
                    .zip(entries2.asSequence())
                    .map { (entry1, entry2) -> entryComparator.compare(entry1, entry2) }
                    .filter { it != 0 }
                    .firstOrNull()
                    ?: (entries2.size - entries1.size)
        }) {
            it.metadata.entries
        }
    }
}
👍 1
a
I’ll play with this last guy want to write a bunch of test for it, will let you know 🙂 thanks for your help @Vampire
v
Yep, should definitely be tested, as I said, this was just out of my mind without any trying at all 😄
👍 1
a
it seems it works pretty well, I’ll try to see if I can polish it a tiny bit to make it simpler, if I find a better option will paste it here
👌 1
f
@Vampire the decision to link equality and identity was a bad one but not by JetBrains but rather by the Java language designers. But there's nothing that can be done about this today...
v
@Fleshgrinder up to now this is just an opinion that you share and I don't. Do you have any objective arguments why you think this is the case?
f
The problem with this implicit approach is that you as a developer always have to keep it in mind because it is something implicit. You can also not tell from just looking at the code, e.g. review, if the other developer wanted equality or identity. Neither can you tell if the struct actually has equality defined or is implicitly using identity. @streetsofboston explained it perfectly above, well, leaving out the functional. Having an explicit equals implementation, like we do for comparable, would also allow you to target only types that define proper equality. Same goes for hashCode and toString. I'm certain there are much more in depth explanations that go deeper. Have a look at Rust that was designed with the latest learning in language design with it's
Eq
,
PartialEq
,
Debug
,
Display
,
Hash
, ...
v
That doesn't really convince me. It is not implicitly defined, it is explicitly defined in
Object
and
Any
and inherited by subclasses. Almost all you said is true for any inherited behavior, so to solve generically it you would have to cancel out inheritance completely. I still don't see why it should be a good idea to not have something equal to itself.
f
This is not about something not being equal to itself, that's what identity provides and that is already provided through the
===
operator and totally sensible to be there by default since this only has to do with memory effects. Allowing this to be overwritten would actually be unsound. No, this is about equality and what equality means. In equality something is not necessarily equal to itself (e.g.
NaN
and
NaN
).
v
You cannot really argue with floating point numbers and expect me to swallow that. :-D Floating point numbers and equality are always their own story. And yes, I'm talking about equality to itself, not about identity. Of course something is identical to itself. But why should something not be equal to itself? To me that makes no sense at all.
f
It's the classical example where equality doesn't hold. If you do not accept it than that's fine for me. No need to convince you. 🙂
v
What, floating point numbers? They are always a special thing when it comes to equality. That doesn't make them meaningful when it comes to whether an object should be equal to itself by default.
f
Math is what's underlying programming and a real number is just an object as any other. Of course one can hand wave and just put it in a corner but it doesn't change the fact. 😉 https://en.wikipedia.org/wiki/Partial_equivalence_relation Rust has an example with books and ISBNs https://doc.rust-lang.org/std/cmp/trait.PartialEq.html#how-can-i-implement-partialeq other classical examples usually deal with time, is
00:00 +00:00
the same as
01:00 +01:00
or
00:00 +24:00
?
a
@Vampire I’ve tested it and all seems good. The only mistake was on the subtraction when the lists have not the same size. Correct snippet below. Thanks again a lot that looks much better than what I had before 😊
Copy code
data class User(
         val id: String,
         val name: String,
         val age: Int,
         val metadata: Map<String, String>
 ) : Comparable<User> {
     override fun compareTo(other: User) = comparator.compare(this, other)
 
     companion object {
         private val entryComparator: Comparator<Entry<String, String>> = compareBy(
                 { it.key },
                 { it.value }
         )
 
         private val comparator = compareBy(
                 User::id,
                 User::name,
                 User::age,
         ).thenBy(Comparator { entries1, entries2 ->
             entries1.asSequence()
                     .zip(entries2.asSequence())
                     .map { (entry1, entry2) -> entryComparator.compare(entry1, entry2) }
                     .filter { it != 0 }
                     .firstOrNull()
                     ?: (entries1.size - entries2.size)
         }) {
             it.metadata.entries
         }
     }
 }
v
Yeah, can happen on untested code. :-D Glad you like it.
@Fleshgrinder
other classical examples usually deal with time, is 00`:00 +00:00 t`he same as 01`:00 +01:00 o`r 00`:00 +24:00?`
That's totally not the question though. The point is, why should
00:00 +00:00
not be equal to
00:00 +00:00
f
With identity two instances that contain the same data are not the same. Try it.
v
No don't need to try it, because that is not what I said, do you listen at all? I'm talking about comparing an instance to itself for equality. What reason could justify that an instance is not equal to itself? A developer can also add a more fine-grained equals semantic if he develops a classes where it makes sense. But an instance should always be equal to itself.
f
I'm listening. 🙂 The discussion started out with you stating that it's fine that the default
equals
implementation uses identity:
> looks like kotlin silently falls back to identity based equality
As does Java and any other similar language.
@Nir didn't agree with you at which point you got aggressive but @streetsofboston jumped in and also agreed that this behavior is not sensible, so did I later. I think the situation is very clear, have a nice day.
v
I'm not aggressive, I'm just curious about your reasoning, but unfortunately none of you had any besides personal preference and that's fine. (If you would not blame the language designers of wrong decisions just because you don't agree personally that is) Have a nice day too