Skip to content

Commit

Permalink
Add Comparator conversion for all types (#4067)
Browse files Browse the repository at this point in the history
Add `Comparator` type class emulation for all types. Migrate all the types in stdlib to this new `Comparator` API. The main documentation is in `Ordering.enso`.

Fixes these pivotals:
- https://www.pivotaltracker.com/story/show/183945328
- https://www.pivotaltracker.com/story/show/183958734
- https://www.pivotaltracker.com/story/show/184380208

# Important Notes
- The new Comparator API forces users to specify both `equals` and `hash` methods on their custom comparators.
- All the `compare_to` overrides were replaced by definition of a custom _ordered_ comparator.
- All the call sites of `x.compare_to y` method were replaced with `Ordering.compare x y`.
- `Ordering.compare` is essentially a shortcut for `Comparable.from x . compare x y`.
- The default comparator for `Any` is `Default_Unordered_Comparator`, which just forwards to the builtin `EqualsNode` and `HashCodeNode` nodes.
- For `x`, one can get its hash with `Comparable.from x . hash x`.
- This makes `hash` as _hidden_ as possible. There are no other public methods to get a hash code of an object.
- Comparing `x` and `y` can be done either by `Ordering.compare x y` or `Comparable.from x . compare x y` instead of `x.compare_to y`.
  • Loading branch information
Akirathan authored Feb 10, 2023
1 parent 4f70bcc commit 1f8511d
Show file tree
Hide file tree
Showing 93 changed files with 2,002 additions and 825 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@
- [Optimize Atom storage layouts][3862]
- [Make instance methods callable like statics for builtin types][4077]
- [Convert large longs to doubles, safely, for host calls][4099]
- [Consistent ordering with comparators](4067)
- [Profile engine startup][4110]
- [Report type of polyglot values][4111]
- [Engine can now recover from serialization failures][5591]
Expand Down Expand Up @@ -649,6 +650,7 @@
[4056]: https://github.com/enso-org/enso/pull/4056
[4077]: https://github.com/enso-org/enso/pull/4077
[4099]: https://github.com/enso-org/enso/pull/4099
[4067]: https://github.com/enso-org/enso/pull/4067
[4110]: https://github.com/enso-org/enso/pull/4110
[4111]: https://github.com/enso-org/enso/pull/4111
[5591]: https://github.com/enso-org/enso/pull/5591
Expand Down
87 changes: 63 additions & 24 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import project.Data.Ordering.Ordering
import project.Data.Pair.Pair
import project.Data.Range.Extensions
import project.Data.Text.Text
import project.Error.Error
import project.Error.Incomparable_Values.Incomparable_Values
import project.Error.Common.No_Such_Conversion
import project.Error.Common.Type_Error
import project.Nothing.Nothing
import project.Meta
import project.Panic.Panic

from project.Data.Boolean import Boolean, True, False
from project.Data.Ordering import all

## Any is the universal top-type, with all other types being subsumed by it.

Expand Down Expand Up @@ -101,7 +105,21 @@ type Any
a = 7 * 21
a == 147
== : Any -> Boolean
== self that = @Builtin_Method "Any.=="
== self that =
# If there is No_Such_Conversion, then `self` and `that` are probably
# host or polyglot values, so we just compare them with the default comparator.
eq_self = Panic.catch No_Such_Conversion (Comparable.from self) _-> Default_Unordered_Comparator
eq_that = Panic.catch No_Such_Conversion (Comparable.from that) _-> Default_Unordered_Comparator
if Meta.is_same_object eq_self Incomparable then False else
similar_type = Meta.is_same_object eq_self eq_that
if similar_type.not then False else
case eq_self.is_ordered of
True ->
# Comparable.equals_builtin is a hack how to directly access EqualsNode from the
# engine, so that we don't end up in an infinite recursion here (which would happen
# if we would compare with `eq_self == eq_that`).
Comparable.equals_builtin (eq_self.compare self that) Ordering.Equal
False -> eq_self.equals self that

## ALIAS Inequality

Expand Down Expand Up @@ -133,12 +151,8 @@ type Any
Arguments:
- that: The value to compare `self` against.

To have `>` defined, a type must define `compare_to`, returning an Ordering.

! Implementing Greater Than
Many types can admit a definition of greater than that is more efficient
than the generic one given here. When implementing this for your own types
please ensure that it is semantically equivalent to using `.compare_to`.
To have `>` properly defined, a type must have an associated ordered comparator.
See `Ordering.enso` for information how comparators work.

> Example
Checking if the variable `a` is greater than `147`.
Expand All @@ -148,8 +162,12 @@ type Any
example_greater =
a = 7 * 28
a > 147
> : Any -> Boolean
> self that = self.compare_to that == Ordering.Greater
> : Any -> Boolean ! Incomparable_Values
> self that =
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Greater -> True
_ -> False

## ALIAS Greater Than or Equal

Expand All @@ -174,10 +192,13 @@ type Any
example_greater_eq =
a = 6 * 21
a >= 147
>= : Any -> Boolean
>= : Any -> Boolean ! Incomparable_Values
>= self that =
ordering = self.compare_to that
(ordering == Ordering.Greater) || (ordering == Ordering.Equal)
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> False
Ordering.Equal -> True
Ordering.Greater -> True

## ALIAS Less Than

Expand All @@ -186,12 +207,8 @@ type Any
Arguments:
- that: The value to compare `self` against.

To have `<` defined, a type must define `compare_to`, returning an Ordering.

! Implementing Less Than
Many types can admit a definition of less than that is more efficient than
the generic one given here. When implementing this for your own types
please ensure that it is semantically equivalent to using `.compare_to`.
To have `<` properly defined, a type must have an associated ordered comparator.
See `Ordering.enso` for information how comparators work.

> Example
Checking if the variable `a` is less than `147`.
Expand All @@ -201,8 +218,12 @@ type Any
example_less =
a = 7 * 21
a < 147
< : Any -> Boolean
< self that = self.compare_to that == Ordering.Less
< : Any -> Boolean ! Incomparable_Values
< self that =
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> True
_ -> False

## ALIAS Less Than or Equal

Expand All @@ -227,10 +248,13 @@ type Any
example_less_eq =
a = 7 * 21
a < 147
<= : Any -> Boolean
<= : Any -> Boolean ! Incomparable_Values
<= self that =
ordering = self.compare_to that
(ordering == Ordering.Less) || (ordering == Ordering.Equal)
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> True
Ordering.Equal -> True
Ordering.Greater -> False

## Checks if the type is an instance of `Nothing`.

Expand Down Expand Up @@ -396,3 +420,18 @@ type Any
(+1 >> *2) 2
>> : (Any -> Any) -> (Any -> Any) -> Any -> Any
>> self ~that = x -> that (self x)


## PRIVATE
Checks if both comparators of the given objects are both of same type and ordered.
If they are not of same type, a `Type_Error` is thrown.
If the comparators are either `Incomparable`, or unordered, `False` is returned.
assert_ordered_comparators : Any -> (Any -> Any) -> Any ! (Type_Error | Incomparable_Values)
assert_ordered_comparators this that ~action =
comp_this = Comparable.from this
comp_that = Comparable.from that
if (Meta.is_same_object comp_this comp_that).not then Error.throw (Type_Error.Error (Meta.type_of comp_this) comp_that "comp_that") else
if Meta.is_same_object comp_this Incomparable || comp_this.is_ordered.not then Error.throw Incomparable_Values else
action


Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ type Array

[3,2,1].to_array.sort
sort : (Any -> Any -> Ordering) -> Array
sort self comparator=(_.compare_to _) =
sort self comparator=(Ordering.compare _ _) =
self.sort_builtin comparator

## Identity.
Expand Down
18 changes: 5 additions & 13 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Boolean.enso
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import project.Any.Any
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Nothing.Nothing

from project.Data.Boolean.Boolean import True, False
Expand Down Expand Up @@ -55,19 +57,6 @@ type Boolean
not : Boolean
not self = @Builtin_Method "Boolean.not"

## Compares the two operands to determine the ordering of this with
respect to that.

Arguments:
- that: The operand to order this with respect to.

> Example
Computing the ordering of True and False

True.compare_to False
compare_to : Boolean -> Ordering
compare_to self that = @Builtin_Method "Boolean.compare_to"

## The if-then-else control flow operator that executes one of two branches
based on a conditional.

Expand Down Expand Up @@ -100,3 +89,6 @@ type Boolean
if (27 % 3) == 0 then IO.println "Fizz"
if_then : Any -> Any | Nothing
if_then self ~on_true = @Builtin_Method "Boolean.if_then"


Comparable.from (_:Boolean) = Default_Ordered_Comparator
39 changes: 25 additions & 14 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json.enso
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import project.Warning.Warning
from project.Metadata.Widget import Single_Choice
from project.Metadata.Choice import Option
import project.Metadata.Display

# We need to import conversion methods from Ordering, therefore, we import all
from project.Data.Ordering import all
from project.Data.Boolean import Boolean, True, False

## Methods for serializing from and to JSON.
Expand Down Expand Up @@ -174,19 +175,6 @@ type JS_Object
to_json : Text
to_json self = self.to_text

## Checks if this JS_Object is equal to another JS_Object.

Arguments:
- that: The map to compare `self` to.
== : JS_Object -> Boolean
== self that = case that of
_ : JS_Object ->
self_keys = self.field_names
that_keys = that.field_names
self_keys.length == that_keys.length && self_keys.all key->
(self.get key == that.at key).catch No_Such_Key.Error _->False
_ -> False

## UNSTABLE

Transform the vector into text for displaying as part of its default
Expand All @@ -195,6 +183,29 @@ type JS_Object
to_default_visualization_data self =
render self

## PRIVATE
type JS_Object_Comparator
is_ordered : Boolean
is_ordered = False

equals : JS_Object -> JS_Object -> Boolean
equals obj1 obj2 =
obj1_keys = obj1.field_names
obj2_keys = obj2.field_names
obj1_keys.length == obj2_keys.length && obj1_keys.all key->
(obj1.get key == obj2.at key).catch No_Such_Key.Error _->False

hash : JS_Object -> Integer
hash obj =
values_hashes = obj.field_names.map field_name->
val = obj.get field_name
Comparable.from val . hash val
# Return sum, as we don't care about ordering of field names
values_hashes.fold 0 (+)

Comparable.from (_:JS_Object) = JS_Object_Comparator


## PRIVATE
Render the JS_Object to Text with truncated depth.
render object depth=0 max_depth=5 max_length=100 = case object of
Expand Down
30 changes: 4 additions & 26 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Ordering.Default_Ordered_Comparator
import project.Data.Text.Text
import project.Data.Locale.Locale
import project.Error.Common.Arithmetic_Error
Expand Down Expand Up @@ -529,19 +531,6 @@ type Decimal
ceil : Integer
ceil self = @Builtin_Method "Decimal.ceil"

## Compares the two operands to determine the ordering of this with
respect to that.

Arguments:
- that: The operand to order this with respect to.

> Example
Computing the ordering of 1.732 and 4 (Less).

1.732.compare_to 4
compare_to : Number -> Ordering
compare_to self that = @Builtin_Method "Decimal.compare_to"

## Computes the nearest integer below this decimal.

This method provides a means of converting a Decimal to an Integer.
Expand Down Expand Up @@ -768,19 +757,6 @@ type Integer
ceil : Integer
ceil self = @Builtin_Method "Integer.ceil"

## Compares the two operands to determine the ordering of this with
respect to that.

Arguments:
- that: The operand to order this with respect to.

> Example
Computing the ordering of 1 and 4 (Less).

1.compare_to 4
compare_to : Number -> Ordering
compare_to self that = @Builtin_Method "Integer.compare_to"

## Computes the integer division of this by that.

Arguments:
Expand Down Expand Up @@ -964,6 +940,8 @@ type Integer

parse_builtin text radix = @Builtin_Method "Integer.parse"

Comparable.from (_:Number) = Default_Ordered_Comparator

## UNSTABLE

A syntax error when parsing a double.
Expand Down
Loading

0 comments on commit 1f8511d

Please sign in to comment.