Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 8 additions & 2 deletions ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

## New features:

Adds a `splitPairwise` function for the List/Array/Seq/FSeq modules.
Adds new functions for the List/Array/Seq/FSeq modules:
- `splitPairwise`
- `split`
- `takeUntilIncluding`
- `skipUntilIncluding`

### InfiniteSeq
InfiniteSeq has been reworked. It is now iterable as a regular sequence. When dealing with infinite sequences, a hang should not be considered a recoverable error with programmatic mitigation (other than possibly with a global exception handler), rather it should be considered a bug needing a fix. Therefore, InfiniteSeq is no longer designed to return a Result in the event of a hang - it's meant to throw an exception instead. Functions like `InfiniteSeq.item` now either crash for a hang or return the item without Result. Existing Result-returning functions like `item'` or Option-returning functions like `tryItem` still exist but are marked deprecated, and will be removed in version 6.0. If you still need the functionality to programmatically recover from a hang, then switch to a `try ... with :? InfiniteSequenceEvaluationHung ->` block.
Expand All @@ -12,14 +16,16 @@ New functions include:
- `initUnbounded`: Create an "unsafe" InfiniteSeq that can hang if misused
- `isHungAfter`: apply a new upper bound to any InfiniteSeq
- `assume`: assume an existing seq is infinite
- `append`: prepend any seq to the front of an infinite seq
- `append`: prepend any seq to the front of an InfiniteSeq
- `item`: same as `Seq.item`, but safe for infinite sequences (barring a hang)
- `take`: same as `Seq.take`, but safe for infinite sequences (barring a hang)
- `takeWhile`: same as `Seq.takeWhile`, but safe for infinite sequences (barring a hang)
- `head`: same as `Seq.head`, but safe for infinite sequences (barring a hang)
- `uncons`: same as `Seq.uncons`, but safe for infinite sequences (barring a hang)
- `find`: same as `Seq.find`, but safe for infinite sequences (barring a hang)
- `skipUntilIncluding`: same as `Seq.skipUntilIncluding`
- `splitPairwise`: same as `Seq.splitPairwise`
- `split`: same as `Seq.split`

Also `Seq.isHungAfter` exists to take a potentially infinite seq that _isn't_ defined as an `InfiniteSeq` and apply an upper bound to consider the sequence hung if it produces more elements than some max number.

Expand Down
112 changes: 108 additions & 4 deletions SafetyFirst.Specs/ArraySpec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ module Splitting =
let toArrs xs = Seq.map Array.NonEmpty.toArray xs |> Array.ofSeq

[<Test>]

let ``returns what the documentation says`` () =

test
Expand Down Expand Up @@ -172,9 +171,9 @@ module Splitting =
@>

[<Test>]
let ``splits pairwise properly for multiple types of inputs`` () =
let ``splits pairwise properly for multiple types of inputs`` () =
let bigDiff i j = abs (i - j) > 5
test
test
<@
(Array.NonEmpty.splitPairwise (=) (Array.NonEmpty.singleton 0) |> toArrs) = [|[|0|]|]
&&
Expand All @@ -187,4 +186,109 @@ module Splitting =
&&
(Array.NonEmpty.splitPairwise (bigDiff) (Array.NonEmpty.create 1 [|2;12;13;23|]) |> toArrs)
= [|[|1;2|]; [|12;13|]; [|23|]|]
@>
@>

[<Test>]
let ``split returns what the documentation says`` () =
test
<@
(Array.split ((=) 100) [|1;2;3;100;100;4;100;5;6|] |> toArrs)
= [|[|1;2;3;100|];[|100|];[|4;100|];[|5;6|]|]
@>

[<Test>]
let ``split handles empty and single element arrays`` () =
test
<@
(Array.split ((=) 5) [||] |> toArrs) = [||]
&&
(Array.split ((=) 5) [|0|] |> toArrs) = [|[|0|]|]
&&
(Array.split ((=) 5) [|5|] |> toArrs) = [|[|5|]|]
&&
(Array.split ((=) 5) [|5;5|] |> toArrs) = [|[|5|]; [|5|]|]
@>

[<Test>]
let ``split splits properly for multiple types of inputs`` () =
test
<@
(Array.split ((=) 5) [|0|] |> toArrs) = [|[|0|]|]
&&
(Array.split ((=) 5) [|5|] |> toArrs) = [|[|5|]|]
&&
(Array.split ((=) 5) [|0;5|] |> toArrs) = [|[|0; 5|]|]
&&
(Array.split ((=) 5) [|5;5|] |> toArrs) = [|[|5|]; [|5|]|]
&&
(Array.split ((=) 5) [|5;0|] |> toArrs) = [|[|5|]; [|0|]|]
&&
(Array.split ((=) 5) [|5;0;0;5;5;0;5|] |> toArrs) = [|[|5|]; [|0;0;5|]; [|5|]; [|0;5|]|]
@>

module TakeUntilIncluding =
[<Test>]
let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () =
test
<@
Array.NonEmpty.takeUntilIncluding ((=) 3) (Array.NonEmpty.create 1 [|2;3;4;5|])
= Array.NonEmpty.create 1 [|2;3|]
&&
Array.NonEmpty.takeUntilIncluding ((=) 1) (Array.NonEmpty.create 1 [|2;3;4;5|])
= Array.NonEmpty.singleton 1
&&
Array.NonEmpty.takeUntilIncluding ((=) 99) (Array.NonEmpty.create 1 [|2;3|])
= Array.NonEmpty.create 1 [|2;3|]
@>

[<Test>]
let ``returns empty for empty input`` () =
test <@ Array.takeUntilIncluding (fun _ -> true) [||] = [||] @>

[<Test>]
let ``returns through the first matching element`` () =
test <@ Array.takeUntilIncluding ((=) 3) [|1;2;3;4;5|] = [|1;2;3|] @>

[<Test>]
let ``returns only the first element when it matches`` () =
test <@ Array.takeUntilIncluding ((=) 3) [|3;4;5|] = [|3|] @>

[<Test>]
let ``stops at the first match even when multiple elements match`` () =
test <@ Array.takeUntilIncluding ((=) 3) [|1;3;3;3|] = [|1;3|] @>

[<Test>]
let ``returns the full array when no element matches`` () =
test <@ Array.takeUntilIncluding ((=) 99) [|1;2;3|] = [|1;2;3|] @>

module SkipUntilIncluding =
[<Test>]
let ``returns empty for empty input`` () =
test <@ Array.skipUntilIncluding (fun _ -> true) [||] = [||] @>

[<Test>]
let ``returns elements after the first matching element`` () =
test <@ Array.skipUntilIncluding ((=) 3) [|1;2;3;4;5|] = [|4;5|] @>

[<Test>]
let ``returns elements after the first element when it matches`` () =
test <@ Array.skipUntilIncluding ((=) 3) [|3;4;5|] = [|4;5|] @>

[<Test>]
let ``stops skipping at the first match even when multiple elements match`` () =
test <@ Array.skipUntilIncluding ((=) 3) [|1;3;3;3|] = [|3;3|] @>

[<Test>]
let ``returns empty when the match is the last element`` () =
test <@ Array.skipUntilIncluding ((=) 3) [|1;2;3|] = [||] @>

[<Test>]
let ``returns empty when no element matches`` () =
test <@ Array.skipUntilIncluding ((=) 99) [|1;2;3|] = [||] @>

[<Test>]
let ``takeUntilIncluding and skipUntilIncluding partition the array`` () =
let xs = [|1;2;3;4;5|]
let taken = Array.takeUntilIncluding ((=) 3) xs
let skipped = Array.skipUntilIncluding ((=) 3) xs
test <@ Array.append taken skipped = xs @>
107 changes: 106 additions & 1 deletion SafetyFirst.Specs/FSeqSpec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ module Splitting =
List.ofSeq <| Seq.map FSeq.NonEmpty.toList xs

let toNonEmpty xs =
FSeq.NonEmpty.ofFSeq' (fseq xs) |> Result.expect
NonEmpty.assume (fseq xs)

[<Test>]
let ``returns what the documentation says`` () =
Expand Down Expand Up @@ -352,6 +352,111 @@ module Splitting =
&& Seq.toList neSegments.[1] = [1;2;3;4]
@>

[<Test>]
let ``split returns what the documentation says`` () =
test
<@
(FSeq.split ((=) 100) (fseq [1;2;3;100;100;4;100;5;6]) |> ofNonEmpty)
= [[1;2;3;100];[100];[4;100];[5;6]]
@>

[<Test>]
let ``split handles empty and single element sequences`` () =
test
<@
(FSeq.split ((=) 5) (fseq []) |> ofNonEmpty) = []
&&
(FSeq.split ((=) 5) (fseq [0]) |> ofNonEmpty) = [[0]]
&&
(FSeq.split ((=) 5) (fseq [5]) |> ofNonEmpty) = [[5]]
&&
(FSeq.split ((=) 5) (fseq [5;5]) |> ofNonEmpty) = [[5]; [5]]
@>

[<Test>]
let ``split splits properly for multiple types of inputs`` () =
test
<@
(FSeq.split ((=) 5) (fseq [0]) |> ofNonEmpty) = [[0]]
&&
(FSeq.split ((=) 5) (fseq [5]) |> ofNonEmpty) = [[5]]
&&
(FSeq.split ((=) 5) (fseq [0;5]) |> ofNonEmpty) = [[0; 5]]
&&
(FSeq.split ((=) 5) (fseq [5;5]) |> ofNonEmpty) = [[5]; [5]]
&&
(FSeq.split ((=) 5) (fseq [5;0]) |> ofNonEmpty) = [[5]; [0]]
&&
(FSeq.split ((=) 5) (fseq [5;0;0;5;5;0;5]) |> ofNonEmpty) = [[5]; [0;0;5]; [5]; [0;5]]
@>

module TakeUntilIncluding =
[<Test>]
let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () =
test
<@
FSeq.NonEmpty.takeUntilIncluding ((=) 3) (Splitting.toNonEmpty [1;2;3;4;5])
= Splitting.toNonEmpty [1;2;3]
&&
FSeq.NonEmpty.takeUntilIncluding ((=) 1) (Splitting.toNonEmpty [1;2;3;4;5])
= Splitting.toNonEmpty [1]
&&
FSeq.NonEmpty.takeUntilIncluding ((=) 99) (Splitting.toNonEmpty [1;2;3])
= Splitting.toNonEmpty [1;2;3]
@>

[<Test>]
let ``returns empty for empty input`` () =
test <@ FSeq.takeUntilIncluding (fun _ -> true) (fseq []) = fseq [] @>

[<Test>]
let ``returns through the first matching element`` () =
test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [1;2;3;4;5]) = fseq [1;2;3] @>

[<Test>]
let ``returns only the first element when it matches`` () =
test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [3;4;5]) = fseq [3] @>

[<Test>]
let ``stops at the first match even when multiple elements match`` () =
test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [1;3] @>

[<Test>]
let ``returns the full sequence when no element matches`` () =
test <@ FSeq.takeUntilIncluding ((=) 99) (fseq [1;2;3]) = fseq [1;2;3] @>

module SkipUntilIncluding =
[<Test>]
let ``returns empty for empty input`` () =
test <@ FSeq.skipUntilIncluding (fun _ -> true) (fseq []) = fseq [] @>

[<Test>]
let ``returns elements after the first matching element`` () =
test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [1;2;3;4;5]) = fseq [4;5] @>

[<Test>]
let ``returns elements after the first element when it matches`` () =
test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [3;4;5]) = fseq [4;5] @>

[<Test>]
let ``stops skipping at the first match even when multiple elements match`` () =
test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [3;3] @>

[<Test>]
let ``returns empty when the match is the last element`` () =
test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [1;2;3]) = fseq [] @>

[<Test>]
let ``returns empty when no element matches`` () =
test <@ FSeq.skipUntilIncluding ((=) 99) (fseq [1;2;3]) = fseq [] @>

[<Test>]
let ``takeUntilIncluding and skipUntilIncluding partition the sequence`` () =
let xs = fseq [1;2;3;4;5]
let taken = FSeq.takeUntilIncluding ((=) 3) xs
let skipped = FSeq.skipUntilIncluding ((=) 3) xs
test <@ FSeq.append taken skipped = xs @>

module SafeFunctions =
open SeqSpec

Expand Down
45 changes: 45 additions & 0 deletions SafetyFirst.Specs/InfiniteSeqSpec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ let ``skipping does not hang`` () =
illFormedList |> InfiniteSeq.skipWhile (fun i -> i < 10) |> take 5 |> Result.isError
&&
wellFormedList |> InfiniteSeq.skipWhile (always true) |> take 1 |> Result.isError
&&
wellFormedList |> InfiniteSeq.skipUntilIncluding (fun i -> i = 10) |> take 5 = Ok [11 .. 15]
&&
illFormedList |> InfiniteSeq.skipUntilIncluding (fun i -> i = 10) |> take 5 |> Result.isError
&&
wellFormedList |> InfiniteSeq.skipUntilIncluding (fun i -> i < 0) |> take 1 |> Result.isError
@>

[<Test>]
Expand Down Expand Up @@ -360,6 +366,45 @@ let ``splitPairwise inner segments can be consumed out of order`` () =
&& Seq.toList segments.[1] = [1;2;3;4]
@>

[<Test>]
let ``split returns what the documentation says`` () =
// Four 100s → four finite segments; everything after belongs to a 5th segment we don't read
let xs = InfiniteSeq.append [1;2;3;100;100;4;100;5;100;0] (InfiniteSeq.initBounded 100 id)
test
<@
InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 4 |> Seq.map Seq.toList |> Seq.toList
= [[1;2;3;100];[100];[4;100];[5;100]]
@>

[<Test>]
let ``split inner segments can be infinite`` () =
// initBounded 100 id yields 0..99 with no 100s, so the second segment is infinite
let xs = InfiniteSeq.append [1;100] (InfiniteSeq.initBounded 100 id)
let secondSeg = InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 2 |> Seq.toList |> List.item 1
test <@ secondSeg |> Seq.truncate 4 |> Seq.toList = [0;1;2;3] @>

[<Test>]
let ``split inner segments can be re-enumerated`` () =
let xs = InfiniteSeq.append [1;2;100;3] (InfiniteSeq.initBounded 100 id)
let firstSegment = InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 1 |> Seq.head
test
<@
Seq.toList firstSegment = [1;2;100]
&& Seq.toList firstSegment = [1;2;100]
@>

[<Test>]
let ``split inner segments can be consumed out of order`` () =
let xs = InfiniteSeq.append [1;2;3;100;100;4;100;5;100;0] (InfiniteSeq.initBounded 100 id)
let segments = InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 4 |> Seq.toArray
test
<@
Seq.toList segments.[2] = [4;100]
&& Seq.toList segments.[0] = [1;2;3;100]
&& Seq.toList segments.[3] = [5;100]
&& Seq.toList segments.[1] = [100]
@>

// [<Test>]
// let ``splits infinite sequences without hanging`` () =
// let alwaysFalse (_:int) = false
Expand Down
Loading
Loading