diff --git a/README.md b/README.md index af3aa53..0cf605b 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 316b51e..5d48541 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -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 diff --git a/SafetyFirst.Specs/InfiniteSeqSpec.fs b/SafetyFirst.Specs/InfiniteSeqSpec.fs index 4a470c8..9a2d205 100644 --- a/SafetyFirst.Specs/InfiniteSeqSpec.fs +++ b/SafetyFirst.Specs/InfiniteSeqSpec.fs @@ -1,5 +1,7 @@ module SafetyFirst.Specs.InfiniteSeqSpec +#nowarn "44" + open NUnit.Framework open Swensen.Unquote @@ -233,17 +235,133 @@ let ``zipping an infinite sequence with a finite sequence does not hang`` () = @> [] -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 @> +[] +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] @> + +[] +let ``assume wraps an existing sequence as infinite`` () = + let xs = InfiniteSeq.assume (Seq.initInfinite id) + test <@ xs |> InfiniteSeq.take 5 |> Seq.toList = [0..4] @> + +[] +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 + @> + +[] +let ``item returns the element at a given index`` () = + test <@ wellFormedList |> InfiniteSeq.item (NaturalInt.assume 42) = 42 @> + +[] +let ``take returns the first N elements`` () = + test <@ wellFormedList |> InfiniteSeq.take 5 |> Seq.toList = [0..4] @> + +[] +let ``takeWhile returns elements while the predicate holds`` () = + test <@ wellFormedList |> InfiniteSeq.takeWhile (fun i -> i < 5) |> Seq.toList = [0..4] @> + +[] +let ``head returns the first element`` () = + test <@ wellFormedList |> InfiniteSeq.head = 0 @> + +[] +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] @> + +[] +let ``find returns the first element satisfying the predicate`` () = + test <@ InfiniteSeq.find ((=) 42) wellFormedList = 42 @> + +[] +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 + @> + + +[] +let ``item throws when the sequence hangs`` () = + raises <@ illFormedList |> InfiniteSeq.item (NaturalInt.assume 1) @> + +[] +let ``take throws when the sequence hangs`` () = + raises <@ illFormedList |> InfiniteSeq.take 1 |> Seq.toList @> + +[] +let ``takeWhile throws when the sequence hangs`` () = + raises <@ illFormedList |> InfiniteSeq.takeWhile (always true) |> Seq.toList @> + +[] +let ``head throws when the sequence hangs`` () = + raises <@ illFormedList |> InfiniteSeq.head @> + +[] +let ``uncons throws when the sequence hangs`` () = + raises <@ illFormedList |> InfiniteSeq.uncons @> + +[] +let ``find throws when the sequence hangs`` () = + raises <@ InfiniteSeq.find ((=) -1) wellFormedList @> + + +[] +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]] + @> + +[] +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] @> + +[] +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] + @> + +[] +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] + @> // [] -// let ``splits infinite sequences without hanging`` () = +// let ``splits infinite sequences without hanging`` () = // let alwaysFalse (_:int) = false // test // <@ diff --git a/SafetyFirst.Specs/SeqSpec.fs b/SafetyFirst.Specs/SeqSpec.fs index 04cb676..3444b9e 100644 --- a/SafetyFirst.Specs/SeqSpec.fs +++ b/SafetyFirst.Specs/SeqSpec.fs @@ -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 +[] +let ``isHungAfter allows elements below the limit`` () = + test <@ Seq.initInfinite id |> Seq.isHungAfter 10 |> Seq.take 10 |> Seq.toList = [0..9] @> + +[] +let ``isHungAfter throws when the limit is exceeded`` () = + raises + <@ Seq.initInfinite id |> Seq.isHungAfter 10 |> Seq.take 11 |> Seq.toList @> + +[] +let ``isHungAfter works with finite sequences that stay within the limit`` () = + test <@ [1..5] |> Seq.isHungAfter 10 |> Seq.toList = [1..5] @> + +[] +let ``isHungAfter throws for finite sequences that exceed the limit`` () = + raises + <@ [1..11] |> Seq.isHungAfter 10 |> Seq.toList @> + module Splitting = let toLists (xs:seq<#seq<_>>) = Seq.toList <| Seq.map Seq.toList xs @@ -208,7 +226,7 @@ module Splitting = [] 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 <@ @@ -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] @> diff --git a/SafetyFirst/ErrorTypes.fs b/SafetyFirst/ErrorTypes.fs index 145ee44..722ace5 100644 --- a/SafetyFirst/ErrorTypes.fs +++ b/SafetyFirst/ErrorTypes.fs @@ -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 diff --git a/SafetyFirst/InfiniteSeq.fs b/SafetyFirst/InfiniteSeq.fs index b90c489..454d20e 100644 --- a/SafetyFirst/InfiniteSeq.fs +++ b/SafetyFirst/InfiniteSeq.fs @@ -1,22 +1,29 @@ namespace SafetyFirst +#nowarn "44" + +open System open System.Collections.Generic open SafetyFirst.Numbers /// -/// An infinite sequence created by InfiniteSeq.init. +/// An infinite sequence created by e.g., InfiniteSeq.init. /// The functions in InfiniteSeq are all safe for use with infinite sequences. -/// Note that an InfiniteSeq is technically finite, with an upper bound supplied -/// at the time of creation. This upper bound represents a limit such that we can -/// be sure that the application "hung" if the sequence produced that many elements. -/// This allows for safe usage of InfiniteSeq without needing to worry about -/// the application truly hanging in an infinite loop. -/// Note that this makes the InfiniteSeq type inappropriate for intentionally initiating -/// an infinite loop (e.g., with an iter function). You might consider using a -/// regular infinite seq with Seq.initInfinite if you're looking to initiate an -/// infinite loop. /// -type InfiniteSeq<'a> = private InfiniteSeq of seq<'a> +type InfiniteSeq<'a> = + private | InfiniteSeq of seq<'a> + + interface IEnumerable<'a> with + member this.GetEnumerator() = + let (InfiniteSeq xs) = this + xs.GetEnumerator() + + interface System.Collections.IEnumerable with + member this.GetEnumerator() = + let (InfiniteSeq xs) = this + (xs :> System.Collections.IEnumerable).GetEnumerator() + + [] module InfiniteSeqTypes = @@ -24,12 +31,31 @@ module InfiniteSeqTypes = type InfiniteSeqHung = InfiniteSeqHung of string /// -/// Functions safe to use with InfiniteSeqs. None of the functions in this module -/// hang indefinitely. +/// Functions safe to use with InfiniteSeqs. /// module InfiniteSeq = - let private always x _ = x let private hung = InfiniteSeqHung "Program execution hung. This infinite sequence was allowed to evaluate elements for too long." + let private protect f x = + try Ok (f x) + with :? InfiniteSequenceEvaluationHung -> Error hung + + let private toLazyResults (xs: _ seq) = + seq { + use mutable iter = xs.GetEnumerator() + let mutable keepGoing = true + while keepGoing do + let nextElement = + protect (fun _ -> + iter.MoveNext() |> ignore + iter.Current) () + + match nextElement with + | Error e -> + keepGoing <- false + yield Error e + | Ok v -> + yield Ok v + } /// /// Generates a new sequence which, when iterated, will return successive @@ -37,15 +63,14 @@ module InfiniteSeq = /// will not be saved, that is the function will be reapplied as necessary to /// regenerate the elements. The function is passed the index of the item being /// generated. - /// Note that an InfiniteSeq is technically finite, with the upper bound supplied + /// Note that an InfiniteSeq created with this function is technically finite, + /// with the upper bound supplied /// representing a limit such that we can be sure that the application "hung" if - /// the sequence produced that many elements. - /// This allows for safe usage of InfiniteSeq without needing to worry about - /// the application truly hanging in an infinite loop. + /// the sequence produced that many elements. If the sequence produces more than the + /// specified maximum number of elements, an exception will be thrown. /// - [] let init (MaxElements maxElements) transform = - InfiniteSeq (Seq.initInfinite transform |> Seq.truncate maxElements) + InfiniteSeq (Seq.initInfinite transform |> Seq.isHungAfter maxElements) /// /// Generates a new sequence which, when iterated, will return successive @@ -53,15 +78,47 @@ module InfiniteSeq = /// will not be saved, that is the function will be reapplied as necessary to /// regenerate the elements. The function is passed the index of the item being /// generated. - /// Note that an InfiniteSeq is technically finite, with the upper bound supplied + /// Note that an InfiniteSeq created with this function is technically finite, + /// with the upper bound supplied + /// representing a limit such that we can be sure that the application "hung" if + /// the sequence produced that many elements. If the sequence produces more than the + /// specified maximum number of elements, an exception will be thrown. + /// + let initBounded (maxElements) transform = init (MaxElements maxElements) transform + + /// + /// Generates a new infinite sequence by calling the given function. Unlike InfiniteSeq.init, + /// this version does not set an upper bound on the number of elements, so the application can hang if + /// a bug causes an infinite loop. + /// + let initUnbounded transform = InfiniteSeq (Seq.initInfinite transform) + + /// + /// Assert that the given sequence is infinite (or bounded by Seq.isHungAfter). Note that there + /// is no possible runtime check to ensure that the sequence is actually infinite. Functions in this module + /// can throw if used with a sequence that is not actually infinite. + /// + let assume xs = InfiniteSeq xs + + /// + /// Generates a new sequence which, when iterated, will return successive + /// elements by calling the given function. The results of calling the function + /// will not be saved, that is the function will be reapplied as necessary to + /// regenerate the elements. The function is passed the index of the item being generated. + /// Note that an InfiniteSeq created with this function is technically finite, + /// with the upper bound supplied /// representing a limit such that we can be sure that the application "hung" if /// the sequence produced that many elements. - /// This allows for safe usage of InfiniteSeq without needing to worry about - /// the application truly hanging in an infinite loop. /// [] + [] let Init maxElements transform = - InfiniteSeq (Seq.initInfinite transform |> Seq.truncate maxElements) + InfiniteSeq (Seq.initInfinite transform |> Seq.isHungAfter maxElements) + + /// + /// Returns a new sequence that contains the elements of the first sequence followed by the elements of the second sequence. + /// + let append (xs) (InfiniteSeq ys) = InfiniteSeq (Seq.append xs ys) /// /// Returns a new collection containing only the elements of the collection @@ -70,102 +127,148 @@ module InfiniteSeq = let filter f (InfiniteSeq xs) = InfiniteSeq (Seq.filter f xs) /// - /// Computes the element at the specified index in the collection. Returns - /// an error if the sequence hung (produced too many elements). + /// Guard against hanging by providing an upper bound that represents a limit such that we can + /// be sure that the application "hung" if the sequence produced that many elements. If more than + /// maxElements elements are consumed, an exception is thrown. While this can be used with + /// any InfiniteSeq, this function is mostly for use with + /// unbounded infinite sequences (created with initUnbounded or assume). + /// For example, you might not know what a proper upper bound is until after you filter an infinite sequence. + /// If used on an already bounded InfiniteSeq, + /// it will apply a new bound _on top of_ the existing bound, but will not override the existing one. + /// So InfiniteSeq.initBounded 100 |> InfiniteSeq.isHungAfter 500 |> InfiniteSeq.take 200 will throw an exception, + /// as will InfiniteSeq.initBounded 500 |> InfiniteSeq.isHungAfter 100 |> InfiniteSeq.take 200 + /// + let isHungAfter maxElements (InfiniteSeq xs) = InfiniteSeq (Seq.isHungAfter maxElements xs) + + /// + /// Computes the element at the specified index in the collection. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// This function can throw if the computation hung when called on an InfiniteSeq created with init. + /// + let item (NaturalInt i) (InfiniteSeq xs) = Seq.item i xs + + /// + /// Computes the element at the specified index in the collection. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] [] - let item' (NaturalInt i) (InfiniteSeq xs) = - Seq.item' i xs |> Result.mapError (always hung) + let item' i xs = + protect (item i) xs /// - /// Computes the element at the specified index in the collection. Returns - /// an error if the sequence hung (produced too many elements). + /// Computes the element at the specified index in the collection. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let itemSafe i xs = item' i xs /// - /// Computes the element at the specified index in the collection. Returns - /// None if the sequence hung (produced too many elements). + /// Computes the element at the specified index in the collection. + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let tryItem i xs = item' i xs |> Result.toOption /// - /// Returns the first N elements of the sequence. Note that this will happen - /// eagerly to check for a hang. If you want to iterate the result lazily, consider using - /// takeLazy instead. Returns - /// an error if the sequence hung (produced too many elements). + /// Returns the first N elements of the sequence. + /// This function returns immediately because of lazy evaluation, but when iterating the result, + /// it can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// Iterating this function's result can throw if the computation hung when called on an InfiniteSeq created with init. + /// + let take n (InfiniteSeq xs) = Seq.take n xs + + /// + /// Returns the first N elements of the sequence. Note that this will happen eagerly to check for a hang. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] [] - let take' n (InfiniteSeq xs) = - Seq.take' n xs |> Result.mapError (always hung) + let take' n xs = + protect (take n >> List.ofSeq >> Seq.ofList) xs /// - /// Returns the first N elements of the sequence. Note that this will happen - /// eagerly to check for a hang. If you want to iterate the result lazily, consider using - /// truncate instead. Returns - /// an error if the sequence hung (produced too many elements). + /// Returns the first N elements of the sequence. Note that this will happen eagerly to check for a hang. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let takeSafe n xs = take' n xs /// - /// Returns the first N elements of the sequence. Note that this will happen - /// eagerly to check for a hang. If you want to iterate the result lazily, consider using - /// truncate instead. Returns - /// an error if the sequence hung (produced too many elements). + /// Returns the first N elements of the sequence. Note that this will happen eagerly to check for a hang. + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let tryTake n xs = take' n xs |> Result.toOption /// - /// Lazily returns up to the first N elements of the sequence. - /// Note that reaching the end of the infinite sequence represents - /// the application hanging, and we cannot preemptively detect a hang while executing lazily. - /// As such the possibility of a hang is deferred to each individual element. - /// This only returns elements up to the first Error, so there is no guarantee that - /// the resulting sequence would contain N elements. - /// If you are able to eagerly evaluate the first n elements, consider using - /// take' instead, which is likely easier to consume. + /// Lazily returns up to the first N elements of the sequence. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. /// + [] let truncate n (InfiniteSeq xs) = - let xs = Seq.append (Seq.map Ok xs) [Error hung] - in (Seq.truncate n xs) + toLazyResults xs |> Seq.truncate n + + + /// + /// Returns a sequence that, when iterated, yields elements of the underlying sequence while the + /// given predicate returns True, and then returns no further elements. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// Iterating the result of this function can throw if the sequence hung on a bounded InfiniteSeq created with init. + /// + let takeWhile predicate (InfiniteSeq xs) = + Seq.takeWhile predicate xs /// /// Returns a sequence that, when iterated, yields elements of the underlying sequence while the /// given predicate returns True, and then returns no further elements. Note that the resulting - /// sequence is evaluated eagerly to ensure that a hang does not occur when iterated. If you - /// expect to possibly receive an infinite result from this function, consider using - /// takeWhileLazy instead. Returns - /// an error if the sequence hung (produced too many elements). + /// sequence is evaluated eagerly to ensure that a hang does not occur when iterated. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// [] - let takeWhile' predicate (InfiniteSeq xs) = + [] + let takeWhile' predicate (InfiniteSeq xs) = let xs = Seq.cache xs - Seq.find' (not << predicate) xs + xs + |> protect (Seq.find (not << predicate)) |> Result.map (fun _ -> Seq.takeWhile predicate xs) - |> Result.mapError (always hung) /// /// Returns a sequence that, when iterated, yields elements of the underlying sequence while the /// given predicate returns True, and then returns no further elements. Note that the resulting /// sequence is evaluated eagerly to ensure that a hang does not occur when iterated. If you - /// expect to possibly receive an infinite result from this function, consider using - /// takeWhileLazy instead. Returns - /// None if the sequence hung (produced too many elements). + /// expect to possibly receive an infinite result from this function, consider using + /// takeWhileLazy instead. + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let tryTakeWhile predicate xs = takeWhile' predicate xs |> Result.toOption /// - /// Lazily returns elements of the underlying sequence while the given predicate returns True, and + /// Lazily returns elements of the underlying sequence while the given predicate returns True, and /// then returns no further elements. Note that reaching the end of the infinite sequence represents /// the application hanging, and we cannot preemptively detect a hang while executing lazily. As such - /// the possibility of a hang is deferred to each individual element. If you are expecting a finite - /// result and are able to eagerly evaluate up to the first element that doesn't pass the predicate, + /// the possibility of a hang is deferred to each individual element, which will throw an exception + /// if the sequence hung (produced too many elements). If you are expecting a finite + /// result and are able to eagerly evaluate up to the first element that doesn't pass the predicate, /// consider using takeWhile' instead, which is likely easier to consume. /// + [] let takeWhileLazy predicate (InfiniteSeq xs) = - let xs = Seq.append (Seq.map Ok xs) [Error hung] - in xs |> Seq.takeWhile (function | Error _ -> true | Ok x -> predicate x) + toLazyResults xs + |> Seq.takeWhile (function + | Ok v -> predicate v + | Error _ -> true) /// /// Applies the given function to each element of the seq. Return the seq comprised of the results x @@ -176,19 +279,12 @@ module InfiniteSeq = let choose chooser (InfiniteSeq xs) = InfiniteSeq (Seq.choose chooser xs) /// - /// Divides the input sequence into chunks of size at most size. + /// Divides the input sequence into chunks of size chunkSize. /// Each chunk is guaranteed to contain chunkSize elements. /// Same as InfiniteSeq.chunkBySizeUnsafe, but restricts the input to a PositiveInt. /// - let chunksOf ((PositiveInt n) as chunkSize) (InfiniteSeq xs) : InfiniteSeq> = + let chunksOf chunkSize (InfiniteSeq xs) : InfiniteSeq> = Seq.chunksOf chunkSize xs - |> Seq.map (fun innerChunk -> - if Array.NonEmpty.length innerChunk = n - then Some innerChunk - else None - ) - |> Seq.takeWhile Option.isSome - |> Seq.choose id |> InfiniteSeq /// @@ -213,16 +309,27 @@ module InfiniteSeq = let skipWhile predicate (InfiniteSeq xs) = InfiniteSeq (Seq.skipWhile predicate xs) /// - /// Returns the first element of the sequence. Returns an error if - /// the sequence hung (produced too many elements). + /// Returns the first element of the sequence. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// This function can throw if the computation hung when called on an InfiniteSeq created with init. + /// + let head (InfiniteSeq xs) = Seq.head xs + + /// + /// Returns the first element of the sequence. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// [] - let head' (InfiniteSeq xs) = Seq.head' xs |> Result.mapError (always hung) + [] + let head' xs = protect head xs /// - /// Returns the first element of the sequence. Returns None if - /// the sequence hung (produced too many elements). + /// Returns the first element of the sequence. + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let inline tryHead xs = head' xs |> Result.toOption /// @@ -232,21 +339,32 @@ module InfiniteSeq = let tail xs = skip 1 xs /// - /// Returns tuple of head element and tail of the list. - /// Returns an error if the sequence hung (produced too many elements). + /// Returns tuple of head element and tail of the sequence. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// This function can throw if the computation hung when called on an InfiniteSeq created with init. + /// + let uncons xs = head xs, tail xs + + /// + /// Returns tuple of head element and tail of the sequence. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// [] - let uncons' xs = - result { - let! h = head' xs + [] + let uncons' xs = + result { + let! h = head' xs let t = tail xs return (h, t) } /// - /// Returns tuple of head element and tail of the list. - /// Returns None if the sequence hung (produced too many elements). + /// Returns tuple of head element and tail of the sequence. + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let tryUncons xs = uncons' xs |> Result.toOption /// @@ -272,50 +390,52 @@ module InfiniteSeq = /// /// Build a new collection whose elements are the results of applying the given function - /// to the corresponding elements of the two collections pairwise. Truncates the - /// infinite sequence to the same length as the finite sequence. The resulting sequence + /// to the corresponding elements of the two collections pairwise. Truncates the + /// infinite sequence to the same length as the finite sequence. The resulting sequence /// is computed eagerly (though of course the elements of the infinite sequence that aren't - /// needed are left lazy). Returns an error if the infinite sequence hung - /// while trying to produce as many elements as the finite sequence. + /// needed are left lazy). + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// - let map2L f (InfiniteSeq xs) ys = - let result = Seq.map2 f xs ys |> fseq - if FSeq.length result = FSeq.length ys - then Ok result - else Error hung + [] + let map2L f (InfiniteSeq xs) (ys: _ fseq) = + Seq.map2 f xs ys |> protect (List.ofSeq >> fseq) /// /// Build a new collection whose elements are the results of applying the given function - /// to the corresponding elements of the two collections pairwise. Truncates the - /// infinite sequence to the same length as the finite sequence. The resulting sequence + /// to the corresponding elements of the two collections pairwise. Truncates the + /// infinite sequence to the same length as the finite sequence. The resulting sequence /// is computed eagerly (though of course the elements of the infinite sequence that aren't - /// needed are left lazy). Returns None if the infinite sequence hung - /// while trying to produce as many elements as the finite sequence. + /// needed are left lazy). + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let inline tryMap2L f xs ys = map2L f xs ys |> Result.toOption /// /// Build a new collection whose elements are the results of applying the given function - /// to the corresponding elements of the two collections pairwise. Truncates the - /// infinite sequence to the same length as the finite sequence. The resulting sequence + /// to the corresponding elements of the two collections pairwise. Truncates the + /// infinite sequence to the same length as the finite sequence. The resulting sequence /// is computed eagerly (though of course the elements of the infinite sequence that aren't - /// needed are left lazy). Returns an error if the infinite sequence hung - /// while trying to produce as many elements as the finite sequence. + /// needed are left lazy). + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// - let map2R f xs (InfiniteSeq ys) = - let result = Seq.map2 f xs ys |> fseq - if FSeq.length result = FSeq.length xs - then Ok result - else Error hung + [] + let map2R f (xs: _ fseq) (InfiniteSeq ys) = + Seq.map2 f xs ys |> protect (List.ofSeq >> fseq) /// /// Build a new collection whose elements are the results of applying the given function - /// to the corresponding elements of the two collections pairwise. Truncates the - /// infinite sequence to the same length as the finite sequence. The resulting sequence + /// to the corresponding elements of the two collections pairwise. Truncates the + /// infinite sequence to the same length as the finite sequence. The resulting sequence /// is computed eagerly (though of course the elements of the infinite sequence that aren't - /// needed are left lazy). Returns None if the infinite sequence hung - /// while trying to produce as many elements as the finite sequence. + /// needed are left lazy). + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let inline tryMap2R f xs ys = map2R f xs ys |> Result.toOption /// @@ -326,17 +446,26 @@ module InfiniteSeq = /// /// Searches the sequence until an element matching the predicate is found. - /// Returns an error if the infinite sequence hung - /// while trying to find a matching element. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). + /// This function can throw if the computation hung when called on an InfiniteSeq created with init. + /// + let find predicate (InfiniteSeq xs) = Seq.find predicate xs + + /// + /// Searches the sequence until an element matching the predicate is found. + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// [] - let find' predicate (InfiniteSeq xs) = Seq.find' predicate xs |> Result.mapError (always hung) - + [] + let find' predicate xs = protect (find predicate) xs + /// /// Searches the sequence until an element matching the predicate is found. - /// Returns None if the infinite sequence hung - /// while trying to find a matching element. + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let inline tryFind predicate xs = find' predicate xs |> Result.toOption /// @@ -345,51 +474,49 @@ module InfiniteSeq = let zip (InfiniteSeq xs) (InfiniteSeq ys) = InfiniteSeq <| Seq.zip xs ys /// - /// Combines the two sequences into a list of pairs. - /// Truncates the infinite sequence to the same length as the finite sequence. + /// Combines the two sequences into a list of pairs. + /// Truncates the infinite sequence to the same length as the finite sequence. /// The resulting sequence is computed eagerly (though of course the elements - /// of the infinite sequence that aren't needed are left lazy). - /// Returns an error if the infinite sequence hung while trying - /// to produce as many elements as the finite sequence. + /// of the infinite sequence that aren't needed are left lazy). + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// - let zipL (InfiniteSeq xs) ys = - let result = Seq.zip xs ys |> fseq - if FSeq.length result = FSeq.length ys - then Ok result - else Error hung + [] + let zipL (InfiniteSeq xs) (ys: _ fseq) = + Seq.zip xs ys |> protect (List.ofSeq >> fseq) /// - /// Combines the two sequences into a list of pairs. - /// Truncates the infinite sequence to the same length as the finite sequence. + /// Combines the two sequences into a list of pairs. + /// Truncates the infinite sequence to the same length as the finite sequence. /// The resulting sequence is computed eagerly (though of course the elements - /// of the infinite sequence that aren't needed are left lazy). - /// Returns None if the infinite sequence hung while trying - /// to produce as many elements as the finite sequence. + /// of the infinite sequence that aren't needed are left lazy). + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let tryZipL xs ys = zipL xs ys |> Result.toOption /// - /// Combines the two sequences into a list of pairs. - /// Truncates the infinite sequence to the same length as the finite sequence. + /// Combines the two sequences into a list of pairs. + /// Truncates the infinite sequence to the same length as the finite sequence. /// The resulting sequence is computed eagerly (though of course the elements - /// of the infinite sequence that aren't needed are left lazy). - /// Returns an error if the infinite sequence hung while trying - /// to produce as many elements as the finite sequence. + /// of the infinite sequence that aren't needed are left lazy). + /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// - let zipR xs (InfiniteSeq ys) = - let result = Seq.zip xs ys |> fseq - if FSeq.length result = FSeq.length xs - then Ok result - else Error hung + [] + let zipR (xs: _ fseq) (InfiniteSeq ys) = + Seq.zip xs ys |> protect (List.ofSeq >> fseq) /// - /// Combines the two sequences into a list of pairs. - /// Truncates the infinite sequence to the same length as the finite sequence. + /// Combines the two sequences into a list of pairs. + /// Truncates the infinite sequence to the same length as the finite sequence. /// The resulting sequence is computed eagerly (though of course the elements - /// of the infinite sequence that aren't needed are left lazy). - /// Returns None if the infinite sequence hung while trying - /// to produce as many elements as the finite sequence. + /// of the infinite sequence that aren't needed are left lazy). + /// Returns None if the sequence hung on a bounded InfiniteSeq created with init. + /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). /// + [] let inline tryZipR xs ys = zipR xs ys |> Result.toOption /// @@ -416,27 +543,15 @@ module InfiniteSeq = /// /// // let split splitAfter xs = - // uncons xs - // |> Result.map (fun (head, InfiniteSeq tail) -> - // let nonEmpty = Seq.NonEmpty.create head tail - // InfiniteSeq (Seq.NonEmpty.split splitAfter nonEmpty) - // |> map (InfiniteSeq << seq)) - // |> Result.mapError hungErr - - // let private uncurry f (a, b) = f a b + // InfiniteSeq (Seq.split splitAfter xs) /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. /// For example: /// - /// NonEmptySeq.splitPairwise (=) (seq { 0;1;1;2;3;4;4;4;5;...}) + /// InfiniteSeq.splitPairwise (=) (seq { 0;1;1;2;3;4;4;4;5;...}) /// //returns seq { [0;1];[1;2;3;4];[4];[4;5];... } /// /// - // let splitPairwise splitBetween xs = - // uncons xs - // |> Result.map (fun (head, InfiniteSeq tail) -> - // let nonEmpty = Seq.NonEmpty.create head tail - // InfiniteSeq (Seq.NonEmpty.splitPairwise splitBetween nonEmpty) - // |> map (InfiniteSeq << seq)) - // |> Result.mapError hungErr + let splitPairwise splitBetween (InfiniteSeq xs) = + InfiniteSeq (Seq.splitPairwise splitBetween xs) diff --git a/SafetyFirst/Seq.fs b/SafetyFirst/Seq.fs index 70e7da9..d9a4413 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -191,6 +191,26 @@ let headSafe xs = [] let inline head' xs = headSafe xs +/// +/// Guard against hanging by providing an upper bound that represents a limit such that we can +/// be sure that the application "hung" if the sequence produced that many elements. If more than +/// maxElements elements are consumed, an exception is thrown. This is intended for use with +/// infinite sequences, but is safe to use with finite sequences as well. +/// +let isHungAfter maxElements xs = + seq { + for i, x in Seq.indexed xs -> + if i < maxElements then x + else + let message = + if maxElements < 0 then + (sprintf "Program execution is considered to have hung, since this sequence produced more than 0 elements (maxElements set to %i)." maxElements) + else + (sprintf "Program execution is considered to have hung, since this sequence produced more than %i elements." maxElements) + + raise (InfiniteSequenceEvaluationHung message) + } + /// /// Computes the element at the specified index in the collection. /// Returns an IndexOutOfRange Error if the index is negative or exceeds the size of the collection. @@ -435,32 +455,29 @@ let inline skip' count xs = skipSafe count xs let inline trySkip count xs = skipSafe count xs |> Result.toOption /// -/// Returns a sequence that skips at least N elements of the underlying sequence and then yields the +/// Returns a sequence that lazily skips N elements of the underlying sequence and then yields the /// remaining elements of the sequence. -/// Returns an empty sequence if count exceeds the length of xs -/// NOTE: This eagerly evaluates the skipped elements to ensure there are enough elements, -/// as opposed to the unsafe Seq.skip, which lazily evaluates and will throw as you iterate it. -/// NOTE: This evaluates the skipped elements twice: once to ensure there are enough elements, -/// and a second time to produce the result. This is necessary because caching the sequence -/// would make it no longer memory-safe for use with infinite sequences. If the input sequence -/// is expensive to compute but finite, it is recommended you cache it with Seq.cache before -/// calling this function. -/// -let skipLenient count xs = - skip' count xs - |> Result.defaultValue Seq.empty - -/// -/// Returns a sequence that skips at least N elements of the underlying sequence and then yields the +/// Returns an empty sequence if count exceeds the length of xs. +/// +let skipLenient count (xs: _ seq) = + seq { + use e = xs.GetEnumerator() + let mutable remaining = count + let mutable enoughElements = true + while remaining > 0 && enoughElements do + if e.MoveNext() then + remaining <- remaining - 1 + else + enoughElements <- false + if enoughElements then + while e.MoveNext() do + yield e.Current + } + +/// +/// Returns a sequence that lazily skips N elements of the underlying sequence and then yields the /// remaining elements of the sequence. -/// Returns an empty sequence if count exceeds the length of xs -/// NOTE: This eagerly evaluates the skipped elements to ensure there are enough elements, -/// as opposed to the unsafe Seq.skip, which lazily evaluates and will throw as you iterate it. -/// NOTE: This evaluates the skipped elements twice: once to ensure there are enough elements, -/// and a second time to produce the result. This is necessary because caching the sequence -/// would make it no longer memory-safe for use with infinite sequences. If the input sequence -/// is expensive to compute but finite, it is recommended you cache it with Seq.cache before -/// calling this function. +/// Returns an empty sequence if count exceeds the length of xs. /// let inline drop count xs = skipLenient count xs @@ -687,7 +704,16 @@ module NonEmpty = /// /// Builds a new collection whose elements are the corresponding elements of the input collection paired with the integer index (from 0) of each element. /// - let indexed (NonEmpty xs) : NonEmptySeq<_> = NonEmpty (Seq.indexed xs) + let indexed (NonEmpty xs: NonEmptySeq<_>) : NonEmptySeq<_> = NonEmpty (Seq.indexed xs) + + /// + /// Guard against hanging by providing an upper bound that represents a limit such that we can + /// be sure that the application "hung" if the sequence produced that many elements. If more than + /// maxElements elements are consumed, an exception is thrown. This is intended for use with + /// infinite sequences, but is safe to use with finite sequences as well. + /// + let isHungAfter maxElements (NonEmpty xs: NonEmptySeq<_>) : NonEmptySeq<_> = + NonEmpty (isHungAfter maxElements xs) /// /// Builds a new collection whose elements are the results of applying the given function