Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comparators support partial ordering #5778

Merged
merged 18 commits into from
Mar 7, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@
- [Use sbt runEngineDistribution][5609]
- [Update to GraalVM 22.3.1][5602]
- [Cache library bindings to optimize import/export resolution][5700]
- [Comparators support partial ordering][5778]

[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
Expand Down Expand Up @@ -698,6 +699,7 @@
[5609]: https://github.com/enso-org/enso/pull/5609
[5602]: https://github.com/enso-org/enso/pull/5602
[5700]: https://github.com/enso-org/enso/pull/5700
[5778]: https://github.com/enso-org/enso/pull/5778

# Enso 2.0.0-alpha.18 (2021-10-12)

Expand Down
22 changes: 15 additions & 7 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Any.enso
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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.Incomparable_Values
import project.Error.Common.No_Such_Conversion
import project.Error.Common.Type_Error
import project.Nothing.Nothing
Expand Down Expand Up @@ -119,11 +119,15 @@ type Any
True ->
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
res = eq_self.compare self that
case res of
Nothing -> False
_ -> Comparable.equals_builtin res Ordering.Equal
Akirathan marked this conversation as resolved.
Show resolved Hide resolved
False ->
res = eq_self.equals self that
case res of
Nothing -> False
_ -> res

## ALIAS Inequality

Expand Down Expand Up @@ -171,6 +175,7 @@ type Any
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Greater -> True
Nothing -> Error.throw Incomparable_Values.Error
_ -> False

## ALIAS Greater Than or Equal
Expand Down Expand Up @@ -203,6 +208,7 @@ type Any
Ordering.Less -> False
Ordering.Equal -> True
Ordering.Greater -> True
Nothing -> Error.throw Incomparable_Values.Error
Akirathan marked this conversation as resolved.
Show resolved Hide resolved

## ALIAS Less Than

Expand All @@ -227,6 +233,7 @@ type Any
assert_ordered_comparators self that <|
case (Comparable.from self).compare self that of
Ordering.Less -> True
Nothing -> Error.throw Incomparable_Values.Error
_ -> False

## ALIAS Less Than or Equal
Expand Down Expand Up @@ -259,6 +266,7 @@ type Any
Ordering.Less -> True
Ordering.Equal -> True
Ordering.Greater -> False
Nothing -> Error.throw Incomparable_Values.Error

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

Expand Down Expand Up @@ -456,5 +464,5 @@ 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
if Meta.is_same_object comp_this Incomparable || comp_this.is_ordered.not then Error.throw (Incomparable_Values.Error this that) else
action
Original file line number Diff line number Diff line change
Expand Up @@ -941,10 +941,7 @@ type Integer

parse_builtin text radix = @Builtin_Method "Integer.parse"

Comparable.from (that:Number) =
case that.is_nan of
True -> Incomparable
False -> Default_Ordered_Comparator
Comparable.from (_:Number) = Default_Ordered_Comparator
Akirathan marked this conversation as resolved.
Show resolved Hide resolved

## UNSTABLE

Expand Down
38 changes: 24 additions & 14 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Ordering.enso
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import project.Any.Any
import project.Data.Numbers.Decimal
import project.Data.Numbers.Integer
import project.Data.Numbers.Number
import project.Error.Common.Incomparable_Values
import project.Error.Common.Type_Error
import project.Error.Error
import project.Error.Incomparable_Values.Incomparable_Values
import project.Error.Unimplemented.Unimplemented
import project.Nothing
import project.Nothing.Nothing
import project.Meta
import project.Meta.Atom
import project.Panic.Panic
Expand All @@ -15,7 +15,7 @@ from project.Data.Boolean import all
## Provides custom ordering, equality check and hash code for types that need it.

The Enso runtime system offers default implementation of _equality_
as well as capability to _compute hash code_ (for use in `Map`) automatically.
as well as capability to _compute hash code_ (e.g., for use in `Map`) automatically.
Akirathan marked this conversation as resolved.
Show resolved Hide resolved
The default implementation is sufficient for most of the programming activities.
Especially when defining new type and its constructors, they get sensible
implementation of both functions.
Expand All @@ -27,30 +27,37 @@ from project.Data.Boolean import all
```
type Ordered_Comparator T
is_ordered = True
compare : T -> T -> Ordering
compare : T -> T -> (Ordering|Nothing)
hash : T -> Integer

type Unordered_Comparator T
is_ordered = False
equals : T -> T -> Boolean
equals : T -> T -> (Boolean|Nothing)
radeusgd marked this conversation as resolved.
Show resolved Hide resolved
hash : T -> Integer
```

Or `Incomparable` in case that the type `T` should not be compared at all.

Every type must provide exactly one comparator, i.e., any method of form
`Comparable.from (_:My_Type)` must always return the same comparator type.
Note that if there is an implicit `Default_Unordered_Comparator` assigned
for every type by default.

Note that there has to be `is_ordered` method defined which returns a Boolean
indicating that the comparator is ordered. This is currently needed as there is
no way to define interfaces in Enso.

An _unordered comparator_ has to implement both `equals` and `hash` to define
a _total_ custom equality. By _total_, we mean that every instance of the type
has to be either equal or not equal, which is represented by the type signature
of `equals` - just `Boolean` is returned without any errors thrown.
a _partial_ custom equality. By _partial_, we mean that every instance of the type
has to be either equal, not equal, or undefined, which is represented by the type signature
of `equals` - when a `Boolean` is returned, the two instances are either equal
or not equal, when `Nothing` is returned, the instances are incomparable.

An _ordered comparator_ has `compare` method instead of `equals` method, that is
expected to return `Ordering` signaling that an instance of the type is either
less than, equal to, or greater than the other instance. This relation is also
_total_, meaning that all the instances of the type are comparable.
less than, equal to, or greater than the other instance, or `Nothing` signaling
that the two instances are incomparable. This relation may also be _partial_, meaning
that some of the instances may be incomparable.

The runtime expects the following semantics for all the comparators:
- Hash consistency:
Expand All @@ -60,7 +67,7 @@ from project.Data.Boolean import all
- Symmetry: if x == y then y == x
- Reflexivity: x == x
- Transitivity: if x < y and y < z then x < z
- Antisymmetry (?): if x > y then y < x
- Antisymmetry: if x > y then y < x

Users are responsible for the compliance to the aforementioned semantics.
Should the semantics be violated, an unexpected behavior may be encountered, e.g.,
Expand Down Expand Up @@ -149,7 +156,7 @@ type Incomparable
type Default_Unordered_Comparator
is_ordered = False

equals : Any -> Any -> Boolean
equals : Any -> Any -> (Boolean|Nothing)
equals x y = Comparable.equals_builtin x y

hash : Any -> Integer
Expand All @@ -164,15 +171,18 @@ type Default_Ordered_Comparator
is_ordered = True

## Handles only primitive types, not atoms or vectors.
compare : Any -> Any -> Ordering
compare : Any -> Any -> (Ordering|Nothing)
compare x y =
case Comparable.less_than_builtin x y of
Nothing -> Nothing
True -> Ordering.Less
False ->
case Comparable.equals_builtin x y of
Nothing -> Nothing
True -> Ordering.Equal
False ->
case Comparable.less_than_builtin y x of
Nothing -> Nothing
True -> Ordering.Greater
False -> Panic.throw "Unreachable"
Akirathan marked this conversation as resolved.
Show resolved Hide resolved

Expand Down Expand Up @@ -201,7 +211,7 @@ type Ordering
if x < y then Ordering.Less else
if x == y then Ordering.Equal else
if x > y then Ordering.Greater else
Error.throw Incomparable_Values
Error.throw (Incomparable_Values.Error x y)

## Converts the ordering to the signed notion of ordering based on integers.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Text.Case_Sensitivity.Case_Sensitivity
import project.Data.Text.Text_Ordering.Text_Ordering
import project.Error.Incomparable_Values.Incomparable_Values
import project.Error.Common.Incomparable_Values
import project.Nothing.Nothing

from project.Data.Boolean import True, False
Expand All @@ -21,7 +21,7 @@ polyglot java import org.enso.base.ObjectComparator
Otherwise can support a custom fallback compare function.
new : Nothing | (Any -> Any -> Ordering) -> ObjectComparator
new custom_comparator=Nothing =
comparator_to_java cmp x y = Incomparable_Values.handle_errors (cmp x y . to_sign)
comparator_to_java cmp x y = cmp x y . to_sign

case custom_comparator of
Nothing -> ObjectComparator.getInstance (comparator_to_java Ordering.compare)
Expand Down
22 changes: 11 additions & 11 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Statistics.enso
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import project.Data.Ordering.Ordering
import project.Data.Ordering.Comparable
import project.Data.Range.Extensions
import project.Data.Vector.Vector
import project.Error.Common.Incomparable_Values
import project.Error.Error
import project.Error.Illegal_Argument.Illegal_Argument
import project.Error.Incomparable_Values.Incomparable_Values
import project.Meta
import project.Nothing.Nothing
import project.Runtime.Ref.Ref
Expand All @@ -20,13 +20,13 @@ from project.Data.Boolean import Boolean, True, False
from project.Data.Vector import Empty_Error
from project.Error.Common import Unsupported_Argument_Types

polyglot java import org.enso.base.CompareException
polyglot java import org.enso.base.statistics.Moments
polyglot java import org.enso.base.statistics.MomentStatistic
polyglot java import org.enso.base.statistics.CountMinMax
polyglot java import org.enso.base.statistics.CorrelationStatistics
polyglot java import org.enso.base.statistics.Rank

polyglot java import java.lang.ClassCastException
polyglot java import java.lang.NullPointerException

## Specifies how to handle ranking of equal values.
Expand Down Expand Up @@ -62,9 +62,10 @@ type Rank_Method

report_nullpointer caught_panic = Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)
handle_nullpointer = Panic.catch NullPointerException handler=report_nullpointer
handle_classcast = Panic.catch ClassCastException handler=(_ -> Error.throw Incomparable_Values)
handle_cmp_exc = Panic.catch CompareException handler=exc->
Error.throw (Incomparable_Values.Error exc.payload.getLeftOperand exc.payload.getRightOperand)

handle_classcast <| handle_nullpointer <|
handle_cmp_exc <| handle_nullpointer <|
java_ranks = Rank.rank input.to_array Comparator.new java_method
Vector.from_polyglot_array java_ranks

Expand Down Expand Up @@ -171,8 +172,8 @@ type Statistic
if skip_moments.not && result.is_nothing then report_error statistics else
statistics.map statistic-> case statistic of
Statistic.Count -> counter.count
Statistic.Minimum -> if counter.comparatorError then (Error.throw Incomparable_Values) else counter.minimum
Statistic.Maximum -> if counter.comparatorError then (Error.throw Incomparable_Values) else counter.maximum
Statistic.Minimum -> if counter.comparatorError then (Error.throw Incomparable_Values.Error) else counter.minimum
Statistic.Maximum -> if counter.comparatorError then (Error.throw Incomparable_Values.Error) else counter.maximum
Statistic.Covariance series -> calculate_correlation_statistics data series . covariance
Statistic.Pearson series -> calculate_correlation_statistics data series . pearsonCorrelation
Statistic.Spearman series -> calculate_spearman_rank data series
Expand Down Expand Up @@ -219,8 +220,8 @@ type Statistic

row = Panic.throw_wrapped_if_error <| statistics.map s-> case s of
Statistic.Count -> counter.count
Statistic.Minimum -> if counter.comparatorError then (Error.throw Incomparable_Values) else counter.minimum
Statistic.Maximum -> if counter.comparatorError then (Error.throw Incomparable_Values) else counter.maximum
Statistic.Minimum -> if counter.comparatorError then (Error.throw Incomparable_Values.Error) else counter.minimum
Statistic.Maximum -> if counter.comparatorError then (Error.throw Incomparable_Values.Error) else counter.maximum
_ -> if result.is_nothing then Error.throw (Illegal_Argument.Error ("Can only compute " + s.to_text + " on numerical data sets.")) else result.compute (to_moment_statistic s)
output.append row

Expand Down Expand Up @@ -342,13 +343,12 @@ compute_fold counter current value =
counter.increment

if counter.comparatorError.not then
value_comparator = Comparable.from value
if counter.minimum.is_nothing then counter.setMinimum value else
ordering = Incomparable_Values.handle_errors <| value_comparator.compare value counter.minimum
ordering = Ordering.compare value counter.minimum
if ordering.is_error then counter.failComparator else
if ordering == Ordering.Less then counter.setMinimum value
if counter.maximum.is_nothing then counter.setMaximum value else
ordering = Incomparable_Values.handle_errors <| value_comparator.compare value counter.maximum
ordering = Ordering.compare value counter.maximum
if ordering.is_error then counter.failComparator else
if ordering == Ordering.Greater then counter.setMaximum value

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import project.Data.Time.Duration.Duration
import project.Data.Ordering.Comparable
import project.Data.Ordering.Incomparable
import project.Data.Text.Text
import project.Error.Common.Incomparable_Values
import project.Error.Error
import project.Error.Illegal_Argument.Illegal_Argument
import project.Error.Incomparable_Values.Incomparable_Values
import project.Error.Time_Error.Time_Error
import project.Meta
import project.Nothing.Nothing
Expand Down
33 changes: 11 additions & 22 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import project.Data.Pair.Pair
import project.Data.Range.Range
import project.Data.Range.Extensions
import project.Data.Text.Text
import project.Error.Common.Incomparable_Values
import project.Error.Common.Index_Out_Of_Bounds
import project.Error.Common.No_Such_Method
import project.Error.Common.Not_Found
import project.Error.Common.Type_Error
import project.Error.Error
import project.Error.Illegal_Argument.Illegal_Argument
import project.Error.Incomparable_Values.Incomparable_Values
import project.Function.Function
import project.Math
import project.Nothing.Nothing
Expand Down Expand Up @@ -884,8 +884,8 @@ type Vector a
compare = if order == Sort_Direction.Ascending then comp_ascending else
comp_descending

Incomparable_Values.handle_errors <|
new_vec_arr = self.to_array.sort compare
new_vec_arr = self.to_array.sort compare
if new_vec_arr.is_error then Error.throw new_vec_arr else
Vector.from_polyglot_array new_vec_arr

## UNSTABLE
Expand All @@ -912,25 +912,14 @@ type Vector a
[Pair 1 "a", Pair 2 "b", Pair 1 "c"] . distinct (on = _.first) == [Pair 1 "a", Pair 2 "b"]
distinct : (Any -> Any) -> Vector Any
distinct self (on = x->x) =
## TODO [JD] This is based on the Map class until a HashMap is available.

Current implementation allows for a consistent distinct with the Enso `==` operator.
Using the Map, is less efficient than a HashMap based as requires searching the tree (O(Log N)).
A HashMap would allow for a O(1) operation to confirm if an item has been seen already.
There is no native HashMap within Enso at present, and the polyglot approach does not produce
consistent results with the `==` operator.

More details on the HashCode / HashMap ticket https://www.pivotaltracker.com/story/show/181027272.

Incomparable_Values.handle_errors <|
builder = Vector.new_builder
result = self.fold Map.empty existing->
item->
key = on item
if (existing.get key False) then existing else
builder.append item
existing.insert key True
if result.is_error then result else builder.to_vector
builder = Vector.new_builder
result = self.fold Map.empty existing->
item->
key = on item
if (existing.get key False) then existing else
builder.append item
existing.insert key True
if result.is_error then result else builder.to_vector

## UNSTABLE
Returns the Vector as a Vector.
Expand Down
Loading