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

Review Range and Interval, resolve infinite loop issue #3408

Merged
merged 17 commits into from
Apr 20, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@
- [Update `Text.replace` to new API.][3393]
- [Add encoding support to `Text.bytes` and `Text.from_bytes`. Renamed and added
encoding to `File.read_text`. New `File.read` API.][3390]
- [Improved the `Range` type. Added a `down_to` counterpart to `up_to` and
`with_step` allowing to change the range step.][3408]

[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
Expand Down Expand Up @@ -170,6 +172,7 @@
[3392]: https://github.com/enso-org/enso/pull/3392
[3393]: https://github.com/enso-org/enso/pull/3393
[3390]: https://github.com/enso-org/enso/pull/3390
[3408]: https://github.com/enso-org/enso/pull/3408

#### Enso Compiler

Expand Down
32 changes: 16 additions & 16 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Interval.enso
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ export Standard.Base.Data.Interval.Bound
## Creates an interval that excludes both its bounds.

> Example
Create the bounds-exclusive range from 1 to 5.
Create the bounds-exclusive range from 0.1 to 0.5.

import Standard.Base.Data.Interval

example_exclusive = Interval.exclusive 1 5
exclusive : Any -> Any -> Interval
example_exclusive = Interval.exclusive 0.1 0.5
exclusive : Number -> Number -> Interval
exclusive start end = Interval (Bound.Exclusive start) (Bound.Exclusive end)

## Creates an interval that excludes its lower bound.
Expand All @@ -23,7 +23,7 @@ exclusive start end = Interval (Bound.Exclusive start) (Bound.Exclusive end)
import Standard.Base.Data.Interval

example_start_exclusive = Interval.start_exclusive 1 5
start_exclusive : Any -> Any -> Interval
start_exclusive : Number -> Number -> Interval
start_exclusive start end = Interval (Bound.Exclusive start) (Bound.Inclusive end)

## Creates an interval that excludes its upper bound.
Expand All @@ -34,44 +34,44 @@ start_exclusive start end = Interval (Bound.Exclusive start) (Bound.Inclusive en
import Standard.Base.Data.Interval

example_end_exclusive = Interval.end_exclusive 1 5
end_exclusive : Any -> Any -> Interval
end_exclusive : Number -> Number -> Interval
end_exclusive start end = Interval (Bound.Inclusive start) (Bound.Exclusive end)

## Creates an interval that includes both of its bounds.

> Example
Create the inclusive range from 1 to 5.
Create the inclusive range from 0 to 0.

import Standard.Base.Data.Interval

example_inclusive = Interval.inclusive 1 5
inclusive : Any -> Any -> Interval
example_inclusive = Interval.inclusive 0 0
inclusive : Number -> Number -> Interval
inclusive start end = Interval (Bound.Inclusive start) (Bound.Inclusive end)

## An interval type
## A type representing an interval over real numbers.
type Interval

## PRIVATE

A type representing an interval over orderable types.
A type representing an interval over real numbers.

Arguments:
- start: The start of the interval.
- end: The end of the interval.
type Interval start end
type Interval (start : Number) (end : Number)

## Checks if the interval contains `that`.

Arguments:
- that: The item to check if it is contained in the interval.

> Example
Checking if the interval 1 to 5 contains 7.
Checking if the interval 0.1 to 1 contains 0.33.

import Standard.Base.Data.Interval

example_contains = (Interval.inclusive 1 5) . contains 7
contains : Any -> Boolean
example_contains = (Interval.inclusive 0.1 1) . contains 0.33
contains : Number -> Boolean
contains that = if this.start.n > this.end.n then False else
case this.start of
Bound.Exclusive s -> (that > s) && case this.end of
Expand Down Expand Up @@ -101,11 +101,11 @@ type Interval
## Check if this interval is not empty.

> Example
Check if the interval from 0 to 1 is not empty.
Check if the interval from 0 to 0.001 is not empty.

import Standard.Base.Data.Interval

example_not_empty = Interval.inclusive 0 1 . not_empty
example_not_empty = Interval.inclusive 0 0.001 . not_empty
not_empty : Boolean
not_empty = this.is_empty.not

Original file line number Diff line number Diff line change
Expand Up @@ -183,20 +183,6 @@ Number.log base = this.ln / base.ln
Number.format : Text -> Text
Number.format fmt = String.format fmt this

## ALIAS Range

Creates a new right-exclusive range of integers from `this` to `n`.

Arguments:
- n: The end of the range.

> Example
Create a range containing the numbers 0, 1, 2, 3, 4.

0.up_to 5
Integer.up_to : Integer -> Range
Integer.up_to n = Range this n

## Checks equality of numbers, using an `epsilon` value.

Arguments:
Expand Down Expand Up @@ -322,3 +308,9 @@ Number.is_nan : Boolean
Number.is_nan = case this of
Decimal -> Double.isNaN this
_ -> False

## Returns the sign of the number.
Number.signum : Integer
Number.signum =
if this > 0 then 1 else
if this < 0 then -1 else 0
155 changes: 119 additions & 36 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Range.enso
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,42 @@ type Range
Arguments:
- start: The left boundary of the range. Its value is included.
- end: The right boundary of the range. Its value is excluded.
type Range start end
- step: The step between consecutive elements of the range. It must be
non-zero. Defaults to 1.
type Range (start : Integer) (end : Integer) (step : Integer = 1)

## Creates a copy of this range with a changed step.

> Example
Create a range of even numbers from 0 to 10 (exclusive).

0.up_to 10 . with_step 2 . to_vector == [0, 2, 4, 6, 8]

> Example
Create a a decreasing range of even numbers from 10 to 0 (exclusive).

10.down_to 0 . with_step 2 . to_vector == [10, 8, 6, 4, 2]
with_step : Integer -> Range
with_step new_step = case new_step of
Integer ->
if new_step == 0 then here.throw_zero_step_error else
if new_step < 0 then Error.throw (Illegal_Argument_Error "The step should be positive. A decreasing sequence will remain decreasing after updating it with positive step, as this operation only sets the magnitude without changing the sign.") else
Range this.start this.end this.step.signum*new_step
_ ->
Error.throw (Illegal_Argument_Error "Range step should be an integer.")

## Returns the last element that is included within the range or `Nothing`
if the range is empty.
last : Integer | Nothing
last = if this.is_empty then Nothing else case this.step > 0 of
True ->
diff = this.end - this.start
rem = diff % this.step
if rem == 0 then this.end - this.step else this.end - rem
False ->
diff = this.start - this.end
rem = diff % (-this.step)
if rem == 0 then this.end - this.step else this.end + rem

## Get the number of elements in the range.

Expand All @@ -18,7 +53,9 @@ type Range

0.up_to 100 . length
length : Number
length = this.end - this.start
length = case this.last of
jdunkerley marked this conversation as resolved.
Show resolved Hide resolved
Nothing -> 0
last -> ((last - this.start) . div this.step) + 1

## Checks if this range is empty.

Expand All @@ -27,7 +64,9 @@ type Range

0.up_to 100 . is_empty
is_empty : Boolean
is_empty = this.end <= this.start
is_empty = if this.step > 0 then this.end <= this.start else
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interval.inclusive 0 0 means this will throw an error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interval is not related to Range

if this.step < 0 then this.start <= this.end else
here.throw_zero_step_error

## Checks if this range is not empty.

Expand All @@ -50,7 +89,8 @@ type Range

1.up_to 10 . map (*2)
map : (Number -> Any) -> Vector Any
map function = this.to_vector.map function
map function =
Vector.new this.length (i -> function (this.start + i*this.step))

## Applies a function for each element in the range.

Expand All @@ -62,11 +102,13 @@ type Range
1.up_to 11 . each IO.println
each : (Number -> Any) -> Nothing
each function =
it start end = if start == end then Nothing else
function start
@Tail_Call it start+1 end
it this.start this.end
Nothing
go end_condition current =
if end_condition current this.end then Nothing else
function current
@Tail_Call go end_condition current+this.step
if this.step > 0 then go (>=) this.start else
if this.step < 0 then go (<=) this.start else
here.throw_zero_step_error

## Combines all the elements of the range, by iteratively applying the
passed function with next elements of the range.
Expand All @@ -77,22 +119,24 @@ type Range
an item.

In general, the result of
Range start end . fold init f
Range start end step . fold init f
is the same as
f (...(f (f init start) start+1)...) end-1
f (...(f (f init start) start+step)...) last

> Example
In the following example, we'll compute the sum of all integers less
than 100.
In the following example, we'll compute the sum of all even integers
less than 100.

0.up_to 100 . fold 0 (+)
0.up_to 100 . with_step 2 . fold 0 (+)
fold : Any -> (Any -> Number -> Any) -> Any
fold init function =
it acc start end = if start == end then acc else
new_acc = function acc start
@Tail_Call it new_acc start+1 end
res = it init this.start this.end
res
go end_condition acc current =
if end_condition current this.end then acc else
new_acc = function acc current
@Tail_Call go end_condition new_acc current+this.step
if this.step > 0 then go (>=) init this.start else
if this.step < 0 then go (<=) init this.start else
here.throw_zero_step_error

## Checks whether `predicate` is satisfied for all numbers in this range.

Expand All @@ -106,12 +150,7 @@ type Range

10.up_to 100 . all (> 5)
all : (Number -> Boolean) -> Boolean
all predicate =
it start end = if start==end then True else
r = predicate start
if r then (@Tail_Call it start+1 end) else False
res = it this.start this.end
res
all predicate = this . exists (predicate >> .not) . not

## Checks whether `predicate` is satisfied for any number in this range.

Expand Down Expand Up @@ -155,11 +194,13 @@ type Range
1.up_to 100 . find i->(i%2==0 && i%3==0 && i%5==0)
find : (Integer -> Boolean) -> Integer | Nothing
find predicate =
limit = this.end
go n = if (n >= limit) then Nothing else
if (predicate n) then n else
@Tail_Call go n+1
go this.start
go end_condition current =
if end_condition current this.end then Nothing else
if predicate current then current else
@Tail_Call go end_condition current+this.step
if this.step > 0 then go (>=) this.start else
if this.step < 0 then go (<=) this.start else
here.throw_zero_step_error

## Converts the range to a vector containing the numbers in the range.

Expand All @@ -168,17 +209,59 @@ type Range

1.up_to 6 . to_vector
to_vector : Vector.Vector
to_vector =
length = Math.max 0 (this.end - this.start)
Vector.new length (i -> i + this.start)
to_vector = this.map x->x

## Does the range contains the specified value
## Checks if the range contains the specified value.

> Example
Check if an index is in the range of a Vector

vec = ["A", "B", "C", "D", "E"]
0.up_to vec.length . contains 3
contains : Integer -> Boolean
contains value =
(this.start <= value) && (this.end > value)
contains value = case value of
Integer ->
if this.step > 0 then (value >= this.start) && (value < this.end) && (((value - this.start) % this.step) == 0) else
if this.step < 0 then (value <= this.start) && (value > this.end) && (((this.start - value) % (-this.step)) == 0) else
here.throw_zero_step_error
## In the future this will be handled by type-checking, but for now we
add this so that we avoid a confusing
`Range 0 10 . contains 3.0 == False` and get a type error for
decimals instead.
_ ->
Error.throw (Illegal_Argument_Error "`Range.contains` only accepts Integers.")

## ALIAS Range

Creates an increasing right-exclusive range of integers from `this` to `n`.

Arguments:
- n: The end of the range.

> Example
Create a range containing the numbers 0, 1, 2, 3, 4.

0.up_to 5
Integer.up_to : Integer -> Range
Integer.up_to n = case n of
Integer -> Range this n
_ -> Error.throw (Illegal_Argument_Error "Expected range end to be an Integer.")

## ALIAS Range

Creates a decreasing right-exclusive range of integers from `this` to `n`.

Arguments:
- n: The end of the range.

> Example
Create a range containing the numbers 5, 4, 3, 2, 1.

5.down_to 0
Integer.down_to : Integer -> Range
Integer.down_to n = case n of
Integer -> Range this n -1
_ -> Error.throw (Illegal_Argument_Error "Expected range end to be an Integer.")

## PRIVATE
throw_zero_step_error = Error.throw (Illegal_State_Error "A range with step = 0 is ill-formed.")
Original file line number Diff line number Diff line change
Expand Up @@ -1112,7 +1112,7 @@ Text.repeat count=1 =
Text.take : (Text_Sub_Range | Range) -> Text ! Index_Out_Of_Bounds_Error
Text.take range =
char_range = case range of
Range _ _ -> Span_Module.range_to_char_indices this range
Range _ _ _ -> Span_Module.range_to_char_indices this range
_ -> range.to_char_range this
Text_Utils.substring this char_range.start char_range.end

Expand Down Expand Up @@ -1152,7 +1152,7 @@ Text.take range =
Text.drop : (Text_Sub_Range | Range) -> Text ! Index_Out_Of_Bounds_Error
Text.drop range =
char_range = case range of
Range _ _ -> Span_Module.range_to_char_indices this range
Range _ _ _ -> Span_Module.range_to_char_indices this range
_ -> range.to_char_range this
if char_range.start == 0 then Text_Utils.drop_first this char_range.end else
prefix = Text_Utils.substring this 0 char_range.start
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ type Utf_16_Span
## PRIVATE
Utility function taking a range pointing at grapheme clusters and converting
to a range on the underlying code units.
range_to_char_indices : Text -> Range -> Range ! Index_Out_Of_Bounds_Error
range_to_char_indices text range =
range_to_char_indices : Text -> Range -> Range ! (Index_Out_Of_Bounds_Error | Illegal_Argument_Error)
range_to_char_indices text range = if range.step != 1 then Error.throw (Illegal_Argument_Error "Text indexing only supports ranges with step equal to 1.") else
len = text.length
start = if range.start < 0 then range.start + len else range.start
end = if range.end == Nothing then len else (if range.end < 0 then range.end + len else range.end)
Expand Down
Loading