The idea of <=>
is that it returns nil
when two objects are not comparable, otherwise less than, equal or greater than zero.
Right now <=>
is only defined if you include Comparable
, but, for example, <=>
in Ruby is defined for all objects, returning nil
if the two things are not comparable.
I think we should add Object#<=>
returning nil
, because you should be able to compare any two objects, and get nil
if they are not comparable.
Adding to this, Comparable
defines #>
, etc with a type restriction of self
, which adding <=>
to object would not help.
If the idea is to be able to compare any object with any other object that restriction should probably be removed. I would imagine that if <=>
returns nil
, then the comparison should also return false
?
EDIT: That would be the case if the type restriction is removed as they are setup like:
https://github.com/crystal-lang/crystal/blob/master/src/comparable.cr#L33
which would return false
since nil
if falsey.
~EDIT2: Ruby raises an exception, so maybe it's fine and if types might not be all comparable, just use output of <=>
directly~
Examples:
https://play.crystal-lang.org/#/r/8s3u
class Object
def <=>(other) : Nil; end
end
record ComparableMock, value : Int32 do
include Comparable(ComparableMock)
def <=>(other : self) : Int32?
@value <=> other.value
end
end
pp ComparableMock.new(1) > 10
Showing last frame. Use --error-trace for full trace.
error in line 13
Error: no overload matches 'ComparableMock#>' with type Int32
Overloads are:
- Comparable(T)#>(other : T)
https://play.crystal-lang.org/#/r/8s3w
class Object
def <=>(other) : Nil; end
end
pp 1 >= "foo"
Showing last frame. Use --error-trace for full trace.
error in line 5
Error: no overload matches 'Int32#>=' with type String
Overloads are:
- Int32#>=(other : Int8)
- Int32#>=(other : Int16)
- Int32#>=(other : Int32)
- Int32#>=(other : Int64)
- Int32#>=(other : Int128)
- Int32#>=(other : UInt8)
- Int32#>=(other : UInt16)
- Int32#>=(other : UInt32)
- Int32#>=(other : UInt64)
- Int32#>=(other : UInt128)
- Int32#>=(other : Float32)
- Int32#>=(other : Float64)
- Comparable(T)#>=(other : T)
@Blacksmoke16 The idea is that, if you want to compare two objects you should use <=>
. In your example, replace this:
pp ComparableMock.new(1) > 10
with this:
cmp = ComparableMock.new(1) <=> 10
pp cmp && cmp > 0
That is, the usual comparison operators >
, >=
, <
, <=
, etc., are type safe: you can't compare any two objects. They answer whether something is greater, lower, etc., than another object. The problem is, there isn't always an answer for that (if the objects are not comparable).
However, <=>
will answer how two objects compare: whether they are comparable at all, and if so, which one is bigger.
Check this in Ruby:
irb(main):001:0> a = [1, 2]
irb(main):002:0> b = [3, 2]
irb(main):003:0> a <=> b
=> -1
irb(main):004:0> a < b
Traceback (most recent call last):
4: from /usr/local/bin/irb:23:in `<main>'
3: from /usr/local/bin/irb:23:in `load'
2: from /usr/local/lib/ruby/gems/2.7.0/gems/irb-1.1.0/exe/irb:11:in `<top (required)>'
1: from (irb):4
NoMethodError (undefined method `<' for [1, 2]:Array)
How is it better to return nil instead of having a compile time error if two objects are not comparable?
How is it better to return nil instead of having a compile time error if two objects are not comparable?
Because you can't always know this. For example, floats are generally comparable, unless you have NaN. We can't detect at compile-time that you have NaN.
Another example: Location
in the compiler source code represents a location in code. That's filename with line and column. We can define that two locations are comparable if they are in the same file, then we compare line and column. However, two locations of different files are not comparable. The compiler can't know that at compile-time. We return nil
in this case.
Is there an actual practical benefit to this?
I don't see any real argument supporting this change, except for "it works on Ruby". But Ruby doesn't have strict typing, so it's not really comparable. And obviously, it's not really an argument unless Ruby shows an example where this is useful.
IMO the distinction is quite nice: the existence of <=>
method defines whether two types are comparable in theory and the actual return value nil
indicates whether the actual instances compare or not.
The argument for using Comparable
methods for type safety doesn't work because you can't use it with partially comparable types.
@Blacksmoke16 needed this so maybe he can explain his use case.
That said, I believe you should be able to compare any two objects with <=>. You generally don't use this method, you use >, <, etc., which are type safe.
You need to use <=>
to safely compare partially comparable types because >
etc. would raise.
Makes sense. Let's wait for @Blacksmoke16 to explain his use case.
You need to use
<=>
to safely compare partially comparable types because>
etc. would raise.
This is pretty much the reason. I'm working on a validation shard that includes constraints like GreaterThan
, or LessThanOrEqual
.
E.x.
@[Assert::GreaterThanOrEqual(value: 0)]
property age : Int32
However, since value
can be anything, and the property it gets assigned to can be anything; the constraint needs to be able to handle partially comparable types.
Currently without #8955, it just doesn't really work because there is no way to check if you can compare two objects without a lot of manual effort, or a compile error.
As a way to make handling these cases a bit easier, I created a helper module that allows comparing partial types, returning false
if they are not comparable, or are but fail.
module Compare
def self.gt(value1, value2) : Bool
compare(value1, value2) do |cmp|
cmp > 0
end
end
def self.gte(value1, value2) : Bool
compare(value1, value2) do |cmp|
cmp >= 0
end
end
def self.lt(value1, value2) : Bool
compare(value1, value2) do |cmp|
cmp < 0
end
end
def self.lte(value1, value2) : Bool
compare(value1, value2) do |cmp|
cmp <= 0
end
end
private def self.compare(value1 : _, value2 : _, & : Int32 -> Bool) : Bool
return false unless cmp = (value1 <=> value2)
yield cmp
end
end
Hm, for that use case I can't see why we need to relax. comparing Int32 with 0 works, right? And you do want a compile error if you, say, put a string.
And you do want a compile error if you, say, put a string.
Not quite, in this use case I think it would be fine to just return false
, I.e.
validator = AVD.validator
constraint = AVD::Constraints::GreaterThan.new value: 10
puts validator.validate 15, [constraint]
puts validator.validate "foo", [constraint]
# foo:
# This value should be greater than 10. (code: a221096d-d125-44e8-a865-4270379ac11a)
Possibly I could change the error message if they are not comparable. But either way, I guess the jist of my argument is:
There should be a way to _KNOW_ if you _CAN_ compare two types, without a compile time error.
Yes. We need responds_to? where you can pass types. Basically "does this compile?", then call it. But now I'm not sure we should go forward with this PR.
That would be one way. However, what's the reasoning for not wanting to move forward? Would it not be a better user experience to use <=>
to determine this, as the docs in Comparable
say that's the purpose of it?
nil if self and the other object are not comparable
This is more of a bug IMO as that currently doesn't happen.
1 <=> "foo" # => Error: no overload matches 'Int32#>' with type String
https://play.crystal-lang.org/#/r/8sa5
EDIT2: NVM on this one as it works fine if <=>
doesn't have self
as the restriction, but I would imagine it should return nil
if they did.
record ComparableMockOne, value : Int32 do
include Comparable(ComparableMockOne)
def <=>(other : self) : Int32?
@value <=> other.value
end
end
record ComparableMockTwo, value : Int32 do
include Comparable(ComparableMockTwo)
def <=>(other : self) : Int32?
@value <=> other.value
end
end
pp ComparableMockOne.new(1) <=> ComparableMockTwo.new(1) # => Error: no overload matches 'ComparableMockOne#<=>' with type ComparableMockTwo
It also says
Comparable uses #<=> to implement the conventional comparison operators (#<, #<=, #==, #>=, and #>). All of these return false when #<=> returns nil.
Note the last sentence. Currently it's impossible for the last sentence to be true since there is a type restriction on those comparison methods, which result in a compile error versus false
.
EDIT: ^, or maybe it should be reworded to make it clear "specific values of the same type". I.e. 1.0
and NaN
instead of totally separate types.
As every method in Crystal, <=>
as well as <
etc. only work with argument types that fit the restrictions in the method's signature. I don't see any reason why for <=>
that should be different or explicitly highlighted.
I don't know specifics about your use case, but when I use a framework for comparing things, I'd very much like to know at compile time if two values I want to compare can ever be compared at all (i.e. compile time error if the types are not comparable).
If you want to be able to compile code for comparing any values, even if the types could never compare at all, I think that should be the responsibility of the framework to implement that on top of the basic <=>
method. If that needs responds_to?
to support argument types, let's get on with #2549. It's a missing feature anyways.
I don't see any reason why for
<=>
that should be different or explicitly highlighted.
Then shouldn't #==
and #!=
also raise if you try to compare two incomparable types? I don't really see how comparing with those is any different than comparing with #>
, Et Al.
I think that should be the responsibility of the framework to implement that on top of the basic
<=>
method.
That's what I'm doing. But it requires #8955 to know if they are comparable or not.
If that needs
responds_to?
to support argument types
I'm assuming this would be be based on the runtime type of a variable? Otherwise how it would handle unions?
I don't know specifics about your use case
I'd very much like to know at compile time if two values I want to compare can ever be compared at all (i.e. compile time error if the types are not comparable).
I mean ideally I agree that would be the most "secure" way of doing it. I can probably do this in some contexts, like when using an annotation applied to a property; however there are some other contexts where the value is only known at runtime, which cannot be accounted for at compile time.
And quite frankly, I don't think the generic system could handle it in its current state, given the 3 bugs I ran into while trying to get this to a working state.
I'm assuming this would be be based on the runtime type of a variable? Otherwise how it would handle unions?
Of course. The behaviour of responds_to?
wouldn't change besides taking into account argument types.
So where does this leave #8955?
IMO given the current documentation of Comparable
I think it would be a better API than having to do like:
if value.responds_to? :<=>(typeof(expected)) # Or whatever the syntax of this will be
value > expected
end
which seems to be what is being suggested.
Wouldn't if value.is_a?(Comparable(T))
work?
No, because Comparable(T)
will only compare against T
. So it won't compile against other types.
In my opinion ==
, =~
, ===
and <=>
should work on all types. But that's just my opinion, of course (and how it's done in Ruby too).
I'm a little wary that it will cause some compile time "misses" like "a" <=> 32
(compiler could have caught that this is never going to be possible)? Or perhaps I misunderstand...
I'm a little wary that it will cause some compile time "misses" like
"a" <=> 32
(compiler could have caught that this is never going to be possible)? Or perhaps I misunderstand...
@rdp, that's the point. <=>
is a lower level comparison operator that _should_ return nil
if the two types are not comparable. In actual code you would want to use >=
etc to maintain compile time safety.
Ref https://crystal-lang.org/api/master/Comparable.html
Including types must provide an #<=> method, which compares the receiver against another object, returning:
a negative number if self is less than the other object
a positive number if self is greater than the other object
0 if self is equal to the other object
nil if self and the other object are not comparable
<=> is a lower level comparison operator that should return nil if the two types are not comparable. In actual code you would want to use >= etc to maintain compile time safety.
That's not true. #<=>
is already a high level comparison operator. It's directly used for example as sort operator. And you also can't use Comparable
methods for partially comparable types. You need to use #<=>
when you want to be able to handle nil
results from non-comparable instances.
But there's a huge difference between returning nil
because two values don't compare or returning nil
by default because their types are not even comparable at all. When those two cases are indistinguishable from each other, it means inevitably losing type safety.
And you also can't use
Comparable
methods for partially comparable types.
@straight-shoota am I misunderstanding sth or that's not true - see #6611
Well, you can but they don't convey the meaning of being non-comparable. They just return false
everywhere. And a > b == false
is different from a > b == nil
because the former implies a <= b == true
(which is also false
when a and b are not comparable).
It just seems strange to me that <=>
is a comparison operator, yet it cannot compare any two given objects. Am I missing something, or is there currently just not a way to compare two objects at runtime?
Like it doesn't seem that great of an experience to have to use .responds_to?
to determine if two types can be compared, then use <=>
to actually compare them. Especially considering the documentation of the method says it returns nil
if two types are not comparable.
When comparing random types doesn't make sense because their's simply no way to compare them, this is no different from +
being the addition operator, yet you can't sum any two objects because the method is only available for types where adding them makes any sense.
Am I missing something, or is there currently just not a way to compare two objects at runtime?
Not that I am aware of. We can certainly improve this. But I wouldn't sacrifice type safety on #<=>
for that. Maybe there can be a different solution. For example a separate method, defined on Object
?
Especially considering the documentation of the method says it returns nil if two types are not comparable.
Where do you get this from? I'm not aware of any such documentation. There's no generic documentation anyway, except for maybe the one in Comparable
but that clearly states "nil if the two objects are not comparable."
When comparing random types doesn't make sense because their's simply no way to compare them
Then I guess my next question is why is it fine for #==
and #!=
to work for every type but not <=>
, esp considering its the _comparison operator_.
Having this behavior for #+
makes sense. It's not the same thing as <=>
IMO.
For example a separate method, defined on
Object
?
That would work I guess, IMO it shouldn't be necessary tho.
nil if the two _objects_ are not comparable
Right. Does this not imply that you have two objects, and if they are not comparable, i.e. Int32
and String
it returns nil
?
Just like you can compare any two objects to determine whether they are equal or not, you can define a partial ordering of any two objects. Nil is returned if they are not comparable. Like ==, it should work on any two types. It makes sense in math, it should make sense in the language
I'm fine with this. As long as it's just <=>
, not all the comparison operators.
Does this not imply that you have two objects, and if they are not comparable, i.e. Int32 and String it returns nil?
No. The method is Comparable(T)#<=>(other : T)
so it only affects operands Comparable(T)
and T
.
Just like you can compare any two objects to determine whether they are equal or not, you can define a partial ordering of any two objects.
But you don't need to actually compare them when their respective types already make them non-comparable.
It makes sense in math, it should make sense in the language
No, it doesn't make sense in math. Math operators are only defined on specific domains. And comparison simply doesn't work for values that don't even share a common dimension to compare them on. That also affects typical properties like converse and transitivity.
As long as it's just <=>, not all the comparison operators.
How's that supposed to work when the specific operators defined by Comparable
depend on <=>
?
It'd help a lot more if I could understand the usecase. I still can't see a simple code example where this feature would help - accompanied with a motivating explanation.
I'm not convinced that if is_a? Comparable(typeof(whatever))
wouldn't work?
Can't the developer just create their own <=>
method overload?
edit: @Blacksmoke16 Would this work for you? https://play.crystal-lang.org/#/r/8u7r. Can just check for nil
now, no errors. I personally would do the typeof
route, though
@girng That might be an option, you can define Object#<=>(other)
in a shard. But this also affects other shard's code, so it might not a great idea.
Is the purpose of <=>
returning Nil so that Float NaN's can reject being
sorted? More?
On Mon, Apr 6, 2020 at 10:56 AM Johannes Müller notifications@github.com
wrote:
@girng https://github.com/girng That might be an option, you can define
Object#<=>(other) in a shard. But this also affects other shard's code,
so it might not a great idea.—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/crystal-lang/crystal/issues/8953#issuecomment-609913874,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AAADBUDI67YPGVW4CF2PA23RLICTLANCNFSM4LVFNDDQ
.
@rdp NaN values of Float types are one example for types with partial comparability, i.e. not all values can be compared. That's only a special case though. Most values are comparable (they're on the same number line), there are just a few exceptions. Other examples might have completele discrete groups of values. For a value type record Mony, amount : BigInt, currency : Currency
, comparison is only defined for values with the same currency property: 100 $
and 100 €
have no inherent order. But 100 $
and 200 $
are in the same dimension and comparing them is obviously possible.
There was a a special type for comparison defined only on subsets of a type's values, PartialComparable(T) which became obsolete with 0.28.0 when partial comparability was encoded in the return value nil
.
It is still possible to determine whether a comparison is defined over all values: !typeof(a <=> a).nilable?
.
Let's try with a better responds_to?
first.
Most helpful comment
Is there an actual practical benefit to this?
I don't see any real argument supporting this change, except for "it works on Ruby". But Ruby doesn't have strict typing, so it's not really comparable. And obviously, it's not really an argument unless Ruby shows an example where this is useful.
IMO the distinction is quite nice: the existence of
<=>
method defines whether two types are comparable in theory and the actual return valuenil
indicates whether the actual instances compare or not.The argument for using
Comparable
methods for type safety doesn't work because you can't use it with partially comparable types.