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
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,12 +326,11 @@ if (ts = someOtherTimeSeries) then ...

#### `InfiniteSeq`

To go along with FiniteSeq, it's helpful to have a type for representing a sequence that's known to be infinite. Some functions are excluded from the InfiniteSeq module (like `fold`), while others are known to be safe for use (like `head`). An InfiniteSeq can be created with the `InfiniteSeq.init` function (which behaves exactly like `Seq.initInfinite`), and the functions in the `InfiniteSeq` module are safe to use for infinite sequences (well, as safe as you can be. You can certainly construct a sequence that will hang forever as soon as you try to do anything useful with it, e.g., the sequence:
To go along with FiniteSeq, it's helpful to have a type for representing a sequence that's known to be infinite. Some functions are excluded from the InfiniteSeq module (like `fold`), while others are known to be safe for use (like `head`). An InfiniteSeq can be created with the `InfiniteSeq.init` function, and the functions in the `InfiniteSeq` module are (mostly) safe to use for infinite sequences.

`InfiniteSeq.init (fun _ -> 0) |> InfiniteSeq.filter ((<>) 0)` will hang if you were to use any eager calculations with it, like `take` or `head` or `find`).

Note that from C#, you can use the InfiniteSeq module directly, but InfiniteSeq doesn't add much benefit if you're using LINQ style syntax. Since InfiniteSeq is an `IEnumerable`, you'll still see all of the regular LINQ extension methods, which will include all of the methods that should never be called on an infinite sequence.
Working with infinite sequences has inherent risks that your program will hang, especially in the presence of a bug. E.g., you can easily construct a sequence that will hang forever as soon as you try to do anything useful with it, such as the sequence: `Seq.initInfinite (fun _ -> 0) |> Seq.filter ((<>) 0)` which will hang if you were to use any eager calculations with it, like `take` or `head` or `find`. InfiniteSeq tries to make this a little easier to deal with by encouraging you to provide an "upper bound" when calling `InfiniteSeq.init` - a number of elements such that if you've produced that many you can be sure that the application has hung. If this upper bound is crossed when working with an `InfiniteSeq`, then an exception is thrown which is much easier to deal with than a true hang. You can still create an unbounded InfiniteSeq with `InfiniteSeq.initUnbounded`, but then a program hang is possible.

Note that from C#, you can use the InfiniteSeq module directly, but InfiniteSeq doesn't add much benefit if you're using LINQ style syntax. Since InfiniteSeq is an `IEnumerable`, you'll still see all of the regular LINQ extension methods, which will include all of the methods that should never be called on an infinite sequence. For C#, the most useful way to work with infinite sequences is probably with the `Seq.isHungAfter` function, which provides the same exception instead of a hang for some upper bound, but doesn't use the `InfiniteSeq` type.

#### `NonEmpty`

Expand Down
40 changes: 39 additions & 1 deletion ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,45 @@

## New features:

- Adds a `splitPairwise` function for the List/Array/Seq/FSeq modules.
Adds a `splitPairwise` function for the List/Array/Seq/FSeq modules.

### 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.

New functions include:
- `initBounded`: Same as `init` but without the need for the `MaxElements` union case
- `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
- `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)
- `splitPairwise`: same as `Seq.splitPairwise`

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.

## Deprecations:

Existing Result-returning functions like `item'` or Option-returning functions like `tryItem` in the InfiniteSeq module 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. These include:

- `item'`
- `itemSafe`
- `tryItem`
- `take'`
- `takeSafe`
- `tryTake`
- `takeWhile'`
- `tryTakeWhile`
- `head'`
- `tryHead`
- `uncons'`
- `tryUncons`
- `find'`
- `tryFind`

# Version 5.3.0

Expand Down
124 changes: 121 additions & 3 deletions SafetyFirst.Specs/InfiniteSeqSpec.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module SafetyFirst.Specs.InfiniteSeqSpec

#nowarn "44"

open NUnit.Framework
open Swensen.Unquote

Expand Down Expand Up @@ -233,17 +235,133 @@ let ``zipping an infinite sequence with a finite sequence does not hang`` () =
@>

[<Test>]
let ``scanning does not hang`` () =
test
let ``scanning does not hang`` () =
test
<@
InfiniteSeq.scan (+) 1 wellFormedList |> take 6 = Ok [1; 1; 2; 4; 7; 11]
&&
InfiniteSeq.scan (+) 1 illFormedList |> take 6 |> Result.isError
@>

[<Test>]
let ``initUnbounded creates an infinite sequence without a max elements guard`` () =
let xs = InfiniteSeq.initUnbounded id
test <@ xs |> InfiniteSeq.take 5 |> Seq.toList = [0..4] @>

[<Test>]
let ``assume wraps an existing sequence as infinite`` () =
let xs = InfiniteSeq.assume (Seq.initInfinite id)
test <@ xs |> InfiniteSeq.take 5 |> Seq.toList = [0..4] @>

[<Test>]
let ``append prepends a finite sequence to an infinite sequence`` () =
test
<@
InfiniteSeq.append [10; 20; 30] wellFormedList |> InfiniteSeq.take 5 |> Seq.toList = [10; 20; 30; 0; 1]
&&
InfiniteSeq.append [10; 20; 30] illFormedList |> take 4 |> Result.isError
@>

[<Test>]
let ``item returns the element at a given index`` () =
test <@ wellFormedList |> InfiniteSeq.item (NaturalInt.assume 42) = 42 @>

[<Test>]
let ``take returns the first N elements`` () =
test <@ wellFormedList |> InfiniteSeq.take 5 |> Seq.toList = [0..4] @>

[<Test>]
let ``takeWhile returns elements while the predicate holds`` () =
test <@ wellFormedList |> InfiniteSeq.takeWhile (fun i -> i < 5) |> Seq.toList = [0..4] @>

[<Test>]
let ``head returns the first element`` () =
test <@ wellFormedList |> InfiniteSeq.head = 0 @>

[<Test>]
let ``uncons returns the head and tail of the sequence`` () =
let h, t = InfiniteSeq.uncons wellFormedList
test <@ h = 0 && take 3 t = Ok [1..3] @>

[<Test>]
let ``find returns the first element satisfying the predicate`` () =
test <@ InfiniteSeq.find ((=) 42) wellFormedList = 42 @>

[<Test>]
let ``chunksOf divides the sequence into fixed-size chunks`` () =
test
<@
wellFormedList |> InfiniteSeq.chunksOf (PositiveInt.assume 3) |> take 2
= Ok (List.map NonEmpty.assume [ [|0;1;2|]; [|3;4;5|] ])
&&
illFormedList |> InfiniteSeq.chunksOf (PositiveInt.assume 3) |> take 2 |> Result.isError
@>


[<Test>]
let ``item throws when the sequence hangs`` () =
raises<InfiniteSequenceEvaluationHung> <@ illFormedList |> InfiniteSeq.item (NaturalInt.assume 1) @>

[<Test>]
let ``take throws when the sequence hangs`` () =
raises<InfiniteSequenceEvaluationHung> <@ illFormedList |> InfiniteSeq.take 1 |> Seq.toList @>

[<Test>]
let ``takeWhile throws when the sequence hangs`` () =
raises<InfiniteSequenceEvaluationHung> <@ illFormedList |> InfiniteSeq.takeWhile (always true) |> Seq.toList @>

[<Test>]
let ``head throws when the sequence hangs`` () =
raises<InfiniteSequenceEvaluationHung> <@ illFormedList |> InfiniteSeq.head @>

[<Test>]
let ``uncons throws when the sequence hangs`` () =
raises<InfiniteSequenceEvaluationHung> <@ illFormedList |> InfiniteSeq.uncons @>

[<Test>]
let ``find throws when the sequence hangs`` () =
raises<InfiniteSequenceEvaluationHung> <@ InfiniteSeq.find ((=) -1) wellFormedList @>


[<Test>]
let ``splitPairwise returns what the documentation says`` () =
let xs = InfiniteSeq.append [0;1;1;2;3;4;4;4;5;0] (InfiniteSeq.initBounded 100 id)
test
<@
InfiniteSeq.splitPairwise (=) xs |> InfiniteSeq.take 4 |> Seq.map Seq.toList |> Seq.toList
= [[0;1];[1;2;3;4];[4];[4;5;0]]
@>

[<Test>]
let ``splitPairwise inner segments can be infinite`` () =
let xs = InfiniteSeq.append [5;5] (InfiniteSeq.initBounded 100 id)
let secondSeg = InfiniteSeq.splitPairwise (=) xs |> InfiniteSeq.take 2 |> Seq.toList |> List.item 1
test <@ secondSeg |> Seq.truncate 4 |> Seq.toList = [5;0;1;2] @>

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

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

// [<Test>]
// let ``splits infinite sequences without hanging`` () =
// let ``splits infinite sequences without hanging`` () =
// let alwaysFalse (_:int) = false
// test
// <@
Expand Down
22 changes: 20 additions & 2 deletions SafetyFirst.Specs/SeqSpec.fs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,24 @@ let ``Safe Seq functions always produce the same output as unsafe versions for a
alwaysProduceSameOutputForSeq2ExceptNonEmpty Seq.chunkBySize' Seq.chunkBySize
alwaysProduceSameOutputForSeq2ExceptNonEmpty Seq.windowed' Seq.windowed

[<Test>]
let ``isHungAfter allows elements below the limit`` () =
test <@ Seq.initInfinite id |> Seq.isHungAfter 10 |> Seq.take 10 |> Seq.toList = [0..9] @>

[<Test>]
let ``isHungAfter throws when the limit is exceeded`` () =
raises<InfiniteSequenceEvaluationHung>
<@ Seq.initInfinite id |> Seq.isHungAfter 10 |> Seq.take 11 |> Seq.toList @>

[<Test>]
let ``isHungAfter works with finite sequences that stay within the limit`` () =
test <@ [1..5] |> Seq.isHungAfter 10 |> Seq.toList = [1..5] @>

[<Test>]
let ``isHungAfter throws for finite sequences that exceed the limit`` () =
raises<InfiniteSequenceEvaluationHung>
<@ [1..11] |> Seq.isHungAfter 10 |> Seq.toList @>

module Splitting =
let toLists (xs:seq<#seq<_>>) =
Seq.toList <| Seq.map Seq.toList xs
Expand All @@ -208,7 +226,7 @@ module Splitting =

[<Test>]
let ``works with infinite lists`` () =
let infinite = Seq.append [0;1;1;2;3;4;4;4;5;0] (Seq.initInfinite id)
let infinite = Seq.append [0;1;1;2;3;4;4;4;5;0] (InfiniteSeq.initBounded 3000 id)
let neInfinite = NonEmpty.assume infinite
test
<@
Expand All @@ -227,7 +245,7 @@ module Splitting =
let ``inner segments can be infinite`` () =
// [5; 5; 0; 1; 2; 3; ...]: one split at (5,5), then an infinite segment [5; 0; 1; 2; 3; ...]
// with no equal adjacent pairs, so it never splits again
let infinite = Seq.collect id [ seq [5; 5]; (Seq.initInfinite id |> Seq.truncate 3000); seq { yield failwith "evaluation hung" } ]
let infinite = Seq.append [5; 5] (InfiniteSeq.initBounded 3000 id)
let neInfinite = NonEmpty.assume infinite

test <@ (Seq.splitPairwise (=) infinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [5; 0; 1; 2] @>
Expand Down
3 changes: 3 additions & 0 deletions SafetyFirst/ErrorTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ namespace SafetyFirst

open System

type InfiniteSequenceEvaluationHung (msg:string) =
inherit Exception (msg)

type SeqIsEmpty = SeqIsEmpty of string with
override this.ToString() = let (SeqIsEmpty s) = this in s
type NotEnoughElements = NotEnoughElements of string with
Expand Down
Loading
Loading