Skip to content

Commit

Permalink
Add error handling and negative indices to at
Browse files Browse the repository at this point in the history
Improve each performance ~2-3x
Improve filter performance ~50-60x
  • Loading branch information
radeusgd committed Jan 21, 2022
1 parent 107128a commit ac09fd9
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 78 deletions.
115 changes: 85 additions & 30 deletions distribution/lib/Standard/Base/0.2.32-SNAPSHOT/src/Data/Vector.enso
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,35 @@ type Vector
## Gets an element from the vector at a specified index (0-based).

Arguments:
- index: The location in the vector to get the element from.
- index: The location in the vector to get the element from. The index is
also allowed be negative, then the elements are indexed from the back
of the vector, i.e. -1 will correspond to the last element.

> Example
Get the second element of a vector.

[1, 2, 3].at 1
at : Number -> Any
at index = this.to_array.at index
[1, 2, 3].at 1 == 2

> Example
Get the last element of a vector.

[1, 2, 3].at -1 == 3
at : Number -> Any ! Index_Out_Of_Bounds_Error
at index =
actual_index = if index < 0 then this.length + index else index
if actual_index>=0 && actual_index<this.length then this.to_array.at actual_index else
Error.throw (Index_Out_Of_Bounds_Error index this.length)

## ADVANCED
UNSTABLE

An unsafe variant of the `at` operation. It only allows non-negative
indices and will panic with a raw Java exception on out-of-bounds access.
Thus it should only be used when the access is guaranteed to be within
bounds or with additional error handling.
unsafe_at : Number -> Any
unsafe_at index =
this.to_array.at index

## Combines all the elements of the vector, by iteratively applying the
passed function with next elements of the vector.
Expand Down Expand Up @@ -207,7 +228,7 @@ type Vector
exists predicate =
len = this.length
go idx found = if found || (idx >= len) then found else
@Tail_Call go idx+1 (predicate (this.at idx))
@Tail_Call go idx+1 (predicate (this.unsafe_at idx))
go 0 False

## Returns the first element of the vector that satisfies the predicate or
Expand All @@ -227,8 +248,8 @@ type Vector
len = this.length
go idx =
if (idx >= len) then Error.throw Nothing else
elem = this.at idx
if (predicate elem) then elem else
elem = this.unsafe_at idx
if predicate elem then elem else
@Tail_Call go idx+1
go 0

Expand Down Expand Up @@ -303,8 +324,10 @@ type Vector
[1, 2, 3, 4, 5].filter (> 3)
filter : (Any -> Boolean) -> Vector Any
filter predicate =
check acc ix = if predicate ix then acc + [ix] else acc
this.fold [] check
builder = here.new_builder
this.each elem->
if predicate elem then builder.append elem
builder.to_vector

## Applies a function to each element of the vector, returning the vector of
results.
Expand All @@ -319,7 +342,7 @@ type Vector
[1, 2, 3] . map +1
map : (Any -> Any) -> Vector Any
map function =
here.new this.length i-> function (this.at i)
here.new this.length i-> function (this.unsafe_at i)

## Applies a function to each element of the vector, returning the vector
that contains all results concatenated.
Expand Down Expand Up @@ -357,7 +380,7 @@ type Vector

[1, 2, 3].map_with_index (+)
map_with_index : (Integer -> Any -> Any) -> Vector Any
map_with_index function = here.new this.length i-> function i (this.at i)
map_with_index function = here.new this.length i-> function i (this.unsafe_at i)

## Applies a function to each element of the vector.

Expand All @@ -373,8 +396,8 @@ type Vector
[1, 2, 3, 4, 5] . each IO.println
each : (Any -> Any) -> Nothing
each f =
this.map f
Nothing
0.up_to this.length . each ix->
f (this.unsafe_at ix)

## Reverses the vector, returning a vector with the same elements, but in
the opposite order.
Expand All @@ -384,7 +407,7 @@ type Vector

[1, 2].reverse
reverse : Vector Any
reverse = here.new this.length (i -> this.at (this.length - (1 + i)))
reverse = here.new this.length (i -> this.unsafe_at (this.length - (1 + i)))

## Generates a human-readable text representation of the vector.

Expand All @@ -393,12 +416,44 @@ type Vector

[1, 2, 3].to_text == "[1, 2, 3]"
to_text : Text
to_text =
if this.length == 0 then "[]" else
if this.length == 1 then "[" + (this.at 0 . to_text) + "]" else
folder = str -> ix -> str + ", " + (this.at ix).to_text
tail_elems = 1.up_to this.length . fold "" folder
"[" + (this.at 0 . to_text) + tail_elems + "]"
to_text = this.generic_to_text "[" ", " "]"

## PRIVATE
A generic utility which concatenates textual representations of the
vector elements, separated by the `separator` and appends the provided
`prefix` and `suffix`.
generic_to_text : Text -> Text -> Text -> Text
generic_to_text prefix separator suffix =
if this.length == 0 then prefix+suffix else
folder = str -> ix -> str + separator + (this.unsafe_at ix).to_text
tail_elems = 1.up_to this.length . fold "" folder
prefix + (this.unsafe_at 0 . to_text) + tail_elems + suffix

## UNSTABLE
Generates a human-readable text representation of the vector, keeping its
length limited.

Arguments:
- max_entries: The maximum number of entries that are displayed. If the
vector contains more elements, the number of hidden elements is also
displayed.

TODO: a better name may be chosen

> Example
Convert a large vector of numbers to a short text.

(0.up_to 100).to_vector.short_display_text max_entries=2 == "[0, 1 and 98 more elements]"
short_display_text : Integer -> Text
short_display_text max_entries=10 =
# TODO should this be some specific error type? We may want a generic Illegal_Argument_Error for such situations
if max_entries < 1 then Error.throw "The `max_entries` parameter must be positive" else
prefix = this.take_start max_entries
if prefix.length == this.length then this.to_text else
remaining_count = this.length - prefix.length
remaining_text = if remaining_count == 1 then "and 1 more element" else
"and " + remaining_count.to_text + " more elements"
prefix.generic_to_text "[" ", " " "+remaining_text+"]"

## Checks whether this vector is equal to `that`.

Expand All @@ -414,7 +469,7 @@ type Vector
[1, 2, 3] == [2, 3, 4]
== : Vector -> Boolean
== that =
eq_at i = this.at i == that.at i
eq_at i = this.unsafe_at i == that.unsafe_at i
if this.length == that.length then 0.up_to this.length . all eq_at else False

## Concatenates two vectors, resulting in a new vector, containing all the
Expand All @@ -432,9 +487,9 @@ type Vector
this_len = this.length
arr = Array.new (this_len + that.length)
0.up_to this_len . each i->
arr.set_at i (this.at i)
arr.set_at i (this.unsafe_at i)
this.length.up_to arr.length . each i->
arr.set_at i (that.at i-this_len)
arr.set_at i (that.unsafe_at i-this_len)
Vector arr

## Add `element` to the beginning of `this` vector.
Expand Down Expand Up @@ -474,8 +529,8 @@ type Vector
join : String -> Text
join separator =
if this.length == 0 then "" else
if this.length == 1 then this.at 0 else
this.at 0 + (1.up_to this.length . fold "" acc-> i-> acc + separator + this.at i)
if this.length == 1 then this.unsafe_at 0 else
this.unsafe_at 0 + (1.up_to this.length . fold "" acc-> i-> acc + separator + this.unsafe_at i)

## Creates a new vector with the first `count` elements in `this` removed.

Expand All @@ -488,7 +543,7 @@ type Vector
[1, 2, 3, 4, 5].drop_start 1
drop_start : Integer -> Vector Any
drop_start count = if count >= this.length then here.new 0 (x -> x) else
here.new (this.length - count) (i -> this.at i+count)
here.new (this.length - count) (i -> this.unsafe_at i+count)

## Creates a new vector with the last `count` elements in `this` removed.

Expand Down Expand Up @@ -555,7 +610,7 @@ type Vector
zip : Vector Any -> (Any -> Any -> Any) -> Vector Any
zip that function=[_,_] =
len = Math.min this.length that.length
here.new len i-> function (this.at i) (that.at i)
here.new len i-> function (this.unsafe_at i) (that.unsafe_at i)

## Extend `this` vector to the length of `n` appending elements `elem` to
the end.
Expand Down Expand Up @@ -598,7 +653,7 @@ type Vector

[1, 2, 3, 4].head
head : Any ! Empty_Error
head = if this.length >= 1 then this.at 0 else Error.throw Empty_Error
head = if this.length >= 1 then this.unsafe_at 0 else Error.throw Empty_Error

## Get all elements in the vector except the first.

Expand Down Expand Up @@ -627,7 +682,7 @@ type Vector

[1, 2, 3, 4].last
last : Vector ! Empty_Error
last = if this.length >= 1 then (this.take_end 1).at 0 else
last = if this.length >= 1 then this.unsafe_at (this.length-1) else
Error.throw Empty_Error

## Get the first element from the vector, or an `Empty_Error` if the vector
Expand All @@ -650,7 +705,7 @@ type Vector

[1, 2, 3, 4].second
second : Vector ! Singleton_Error
second = if this.length >= 2 then this.at 1 else
second = if this.length >= 2 then this.unsafe_at 1 else
Error.throw (Singleton_Error this)

## Get all elements in the vector except the first.
Expand Down
53 changes: 5 additions & 48 deletions test/Benchmarks/src/Vector.enso
Original file line number Diff line number Diff line change
Expand Up @@ -6,68 +6,25 @@ polyglot java import java.util.Random
polyglot java import org.enso.base.Time_Utils



## Bench Utilities ============================================================

vector_size = 1000000
iter_size = 100
num_iterations = 10

make_sorted_ascending_vec : Integer -> Base.Vector.Vector
make_sorted_ascending_vec n = 0.up_to n+1 . to_vector

make_partially_sorted_vec : Integer -> Base.Vector.Vector
make_partially_sorted_vec n =
random_gen = Random.new n
direction = Ref.new Sort_Order.Ascending
last_num = Ref.new 0
run_length = Ref.new 0
Base.Vector.fill n <|
case (Ref.get run_length) == 0 of
True ->
new_direction = if random_gen.nextDouble > 0 then Sort_Order.Ascending else
Sort_Order.Descending
Ref.put direction new_direction
Ref.put run_length ((random_gen.nextLong % (n / 10).floor) - 1)
num = random_gen.nextInt
Ref.put last_num num
num
False ->
change = random_gen.nextInt.abs % n
num = case Ref.get direction of
Sort_Order.Ascending ->
num = (Ref.get last_num) + change
Ref.put last_num num
num
Sort_Order.Descending ->
num = (Ref.get last_num) - change
Ref.put last_num num
num
Ref.put run_length ((Ref.get run_length) - 1)
num

make_random_vec : Integer -> Base.Vector.Vector
make_random_vec n =
random_gen = Random.new n
Base.Vector.fill n random_gen.nextLong



# The Benchmarks ==============================================================

main =
sorted_vec = here.make_sorted_ascending_vec here.vector_size
partially_sorted_vec = here.make_partially_sorted_vec here.vector_size
random_vec = here.make_random_vec here.vector_size
projection = x -> x % 10
comparator = l -> r -> r.compare_to l

Bench.measure (sorted_vec.sort) "Already Sorted" here.iter_size here.num_iterations
Bench.measure (sorted_vec.sort order=Sort_Order.Descending) "Sorted in Opposite Order" here.iter_size here.num_iterations
Bench.measure (partially_sorted_vec.sort) "Sorted Runs Ascending" here.iter_size here.num_iterations
Bench.measure (partially_sorted_vec.sort order=Sort_Order.Descending) "Sorted Runs Descending" here.iter_size here.num_iterations
Bench.measure (random_vec.sort) "Random Elements Ascending" here.iter_size here.num_iterations
Bench.measure (random_vec.sort order=Sort_Order.Descending) "Random Elements Descending" here.iter_size here.num_iterations
Bench.measure (random_vec.sort on=projection) "Sorting with a Custom Projection" here.iter_size here.num_iterations
Bench.measure (random_vec.sort by=comparator) "Sorting with a Custom Comparison" here.iter_size here.num_iterations
Bench.measure (random_vec.filter (x -> x % 3 == 1)) "Filter" here.iter_size here.num_iterations

stateful_fun x =
s = State.get Number
State.put s+x
Bench.measure (State.run Number 0 <| random_vec.each stateful_fun) "Each" here.iter_size here.num_iterations
22 changes: 22 additions & 0 deletions test/Tests/src/Data/Vector_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,18 @@ spec = Test.group "Vectors" <|
built . should_equal [1, 2, 3, 4, 5]

Test.specify "should allow accessing elements" <|
[1,2,3].at 0 . should_equal 1
[1,2,3].at 2 . should_equal 3

Test.specify "should allow accessing elements with negative indices" <|
[1,2,3].at -1 . should_equal 3
[1,2,3].at -2 . should_equal 2
[1,2,3].at -3 . should_equal 1

Test.specify "should return a dataflow error when accessing elements out of bounds" <|
[1,2,3].at -4 . should_fail_with Vector.Index_Out_Of_Bounds_Error
[1,2,3].at 3 . should_fail_with Vector.Index_Out_Of_Bounds_Error

Test.specify "should have a well-defined length" <|
[1,2,3].length . should_equal 3

Expand Down Expand Up @@ -105,6 +115,18 @@ spec = Test.group "Vectors" <|
[].to_text.should_equal "[]"
[1,2,3].to_text.should_equal "[1, 2, 3]"
[Nothing].to_text.should_equal "[Nothing]"
['a'].to_text . should_equal "['a']"

Test.specify "should allow to generate a short text representation for display" <|
[].short_display_text max_entries=3 . should_equal "[]"
[1].short_display_text max_entries=3 . should_equal "[1]"
[1, 2].short_display_text max_entries=3 . should_equal "[1, 2]"
[1, 2, 3].short_display_text max_entries=3 . should_equal "[1, 2, 3]"
[1, 2, 3, 4].short_display_text max_entries=3 . should_equal "[1, 2, 3 and 1 more element]"
[1, 2, 3, 4, 5, 6].short_display_text max_entries=3 . should_equal "[1, 2, 3 and 3 more elements]"
(0.up_to 100).to_vector.short_display_text max_entries=2 . should_equal "[0, 1 and 98 more elements]"

[].short_display_text max_entries=0 . catch . should_equal "The `max_entries` parameter must be positive"

Test.specify "should define equality" <|
[1,2,3]==[1,2] . should_be_false
Expand Down

0 comments on commit ac09fd9

Please sign in to comment.