From e5e57420f2d2c493a7890e340559c9c075ccb378 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Sun, 15 Mar 2026 21:13:47 -0400 Subject: [PATCH 1/4] Redo the InfiniteSeq module to prefer exceptions to Results --- .gitignore | 1 + README.md | 7 +- ReleaseNotes.md | 22 ++ SafetyFirst.Specs/InfiniteSeqSpec.fs | 90 +++++- SafetyFirst/ErrorTypes.fs | 3 + SafetyFirst/InfiniteSeq.fs | 418 +++++++++++++++++---------- SafetyFirst/Seq.fs | 69 +++-- 7 files changed, 422 insertions(+), 188 deletions(-) diff --git a/.gitignore b/.gitignore index 35e70a4..30bb41b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.userosscache *.sln.docstates global.json +.claude/ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/README.md b/README.md index af3aa53..4128979 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 presense 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 86cbfcb..8e1eb33 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,3 +1,25 @@ +# Version 5.4.0 + +## New features: + +### 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) + +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. + # Version 5.3.0 ## New features: diff --git a/SafetyFirst.Specs/InfiniteSeqSpec.fs b/SafetyFirst.Specs/InfiniteSeqSpec.fs index 4a470c8..a9a95bf 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 @@ -54,11 +56,12 @@ let ``eagerly taking while some predicate does not hang`` () = [] let ``lazily taking while some predicate can return an infinite result and does not hang`` () = + let xs = illFormedList |> InfiniteSeq.takeWhileLazy (always true) |> Seq.toList test <@ wellFormedList |> InfiniteSeq.takeWhileLazy (always true) |> Seq.take 10 |> Seq.toList = ([0 .. 9] |> List.map Ok) && - illFormedList |> InfiniteSeq.takeWhileLazy (always true) |> Seq.toList = [Error hung] + xs = [Error hung] && wellFormedList |> InfiniteSeq.takeWhileLazy (fun i -> i < 10) |> Seq.toList = ([0 .. 9] |> List.map Ok) @> @@ -233,17 +236,96 @@ 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 ``splits infinite sequences without hanging`` () = +// let ``splits infinite sequences without hanging`` () = // let alwaysFalse (_:int) = false // test // <@ 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..aefa846 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 { + let 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 an 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 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 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 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 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,26 @@ 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. + /// Throws an exception if the sequence hung (produced too many elements). + /// + 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 +338,31 @@ 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. + /// Throws an exception if the sequence hung (produced too many elements). + /// + 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 +388,48 @@ 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 + /// 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 + /// needed are left lazy). Throws an exception if the infinite sequence hung /// while trying to produce as many elements as the finite sequence. /// - 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 + /// 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 + /// needed are left lazy). Throws an exception if the infinite sequence hung /// while trying to produce as many elements as the finite sequence. /// + [] 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 + /// 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 + /// needed are left lazy). Throws an exception if the infinite sequence hung /// while trying to produce as many elements as the finite sequence. /// - 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 + /// 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 + /// needed are left lazy). Throws an exception if the infinite sequence hung /// while trying to produce as many elements as the finite sequence. /// + [] let inline tryMap2R f xs ys = map2R f xs ys |> Result.toOption /// @@ -326,17 +440,25 @@ module InfiniteSeq = /// /// Searches the sequence until an element matching the predicate is found. - /// Returns an error if the infinite sequence hung + /// Throws an exception if the infinite sequence hung while trying to find a matching element. + /// + let find predicate (InfiniteSeq xs) = Seq.find predicate xs + + /// + /// Searches the sequence until an element matching the predicate is found. + /// Throws an exception if the infinite sequence hung /// while trying to find a matching element. /// [] - 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 + /// Throws an exception if the infinite sequence hung /// while trying to find a matching element. /// + [] let inline tryFind predicate xs = find' predicate xs |> Result.toOption /// @@ -345,51 +467,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 + /// of the infinite sequence that aren't needed are left lazy). + /// Throws an exception if the infinite sequence hung while trying /// to produce as many elements as the finite sequence. /// - 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 + /// of the infinite sequence that aren't needed are left lazy). + /// Throws an exception if the infinite sequence hung while trying /// to produce as many elements as the finite sequence. /// + [] 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 + /// of the infinite sequence that aren't needed are left lazy). + /// Throws an exception if the infinite sequence hung while trying /// to produce as many elements as the finite sequence. /// - 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 + /// of the infinite sequence that aren't needed are left lazy). + /// Throws an exception if the infinite sequence hung while trying /// to produce as many elements as the finite sequence. /// + [] let inline tryZipR xs ys = zipR xs ys |> Result.toOption /// @@ -416,14 +536,7 @@ 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. @@ -433,10 +546,5 @@ module InfiniteSeq = /// //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 3287328..3b32795 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -191,6 +191,19 @@ 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 raise (InfiniteSequenceEvaluationHung (sprintf "Program execution is considered to have hung, since this sequence produced more than %i elements." maxElements)) + } + /// /// 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 +448,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 @@ -653,7 +663,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 From d656c0ed95e30c3ed392b7010263aa025e37c250 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Sun, 15 Mar 2026 21:34:48 -0400 Subject: [PATCH 2/4] Most of the review comments --- README.md | 2 +- SafetyFirst.Specs/InfiniteSeqSpec.fs | 3 +- SafetyFirst/InfiniteSeq.fs | 79 +++++++++++++++------------- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 4128979..0cf605b 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,7 @@ if (ts = someOtherTimeSeries) then ... 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. -Working with infinite sequences has inherent risks that your program will hang, especially in the presense 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. +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. diff --git a/SafetyFirst.Specs/InfiniteSeqSpec.fs b/SafetyFirst.Specs/InfiniteSeqSpec.fs index a9a95bf..2670a6d 100644 --- a/SafetyFirst.Specs/InfiniteSeqSpec.fs +++ b/SafetyFirst.Specs/InfiniteSeqSpec.fs @@ -56,12 +56,11 @@ let ``eagerly taking while some predicate does not hang`` () = [] let ``lazily taking while some predicate can return an infinite result and does not hang`` () = - let xs = illFormedList |> InfiniteSeq.takeWhileLazy (always true) |> Seq.toList test <@ wellFormedList |> InfiniteSeq.takeWhileLazy (always true) |> Seq.take 10 |> Seq.toList = ([0 .. 9] |> List.map Ok) && - xs = [Error hung] + illFormedList |> InfiniteSeq.takeWhileLazy (always true) |> Seq.toList = [Error hung] && wellFormedList |> InfiniteSeq.takeWhileLazy (fun i -> i < 10) |> Seq.toList = ([0 .. 9] |> List.map Ok) @> diff --git a/SafetyFirst/InfiniteSeq.fs b/SafetyFirst/InfiniteSeq.fs index aefa846..a703798 100644 --- a/SafetyFirst/InfiniteSeq.fs +++ b/SafetyFirst/InfiniteSeq.fs @@ -41,7 +41,7 @@ module InfiniteSeq = let private toLazyResults (xs: _ seq) = seq { - let mutable iter = xs.GetEnumerator() + use mutable iter = xs.GetEnumerator() let mutable keepGoing = true while keepGoing do let nextElement = @@ -111,7 +111,7 @@ module InfiniteSeq = /// the sequence produced that many elements. /// [] - [] + [] let Init maxElements transform = InfiniteSeq (Seq.initInfinite transform |> Seq.isHungAfter maxElements) @@ -133,7 +133,7 @@ module InfiniteSeq = /// 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 an an already bounded InfiniteSeq, + /// 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 @@ -167,7 +167,7 @@ module InfiniteSeq = /// /// Computes the element at the specified index in the collection. - /// Returns an error if the sequence hung on a bounded InfiniteSeq created with init. + /// 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). /// [] @@ -201,7 +201,7 @@ module InfiniteSeq = /// /// 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. + /// 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). /// [] @@ -310,7 +310,8 @@ module InfiniteSeq = /// /// Returns the first element of the sequence. - /// Throws an exception if the sequence hung (produced too many elements). + /// 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 @@ -339,7 +340,8 @@ module InfiniteSeq = /// /// Returns tuple of head element and tail of the sequence. - /// Throws an exception if the sequence hung (produced too many elements). + /// 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 @@ -388,11 +390,12 @@ 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). Throws an exception 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: _ fseq) = @@ -400,22 +403,24 @@ 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). Throws an exception 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). Throws an exception 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: _ fseq) (InfiniteSeq ys) = @@ -423,11 +428,12 @@ 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). Throws an exception 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 @@ -440,14 +446,15 @@ module InfiniteSeq = /// /// Searches the sequence until an element matching the predicate is found. - /// Throws an exception 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. - /// Throws an exception if the infinite sequence hung - /// while trying to find a matching element. + /// 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). /// [] [] @@ -455,8 +462,8 @@ module InfiniteSeq = /// /// Searches the sequence until an element matching the predicate is found. - /// Throws an exception 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 @@ -471,8 +478,8 @@ module InfiniteSeq = /// 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). - /// Throws an exception if the infinite sequence hung while trying - /// to produce as many elements as the finite 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 zipL (InfiniteSeq xs) (ys: _ fseq) = @@ -483,8 +490,8 @@ module InfiniteSeq = /// 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). - /// Throws an exception if the infinite sequence hung while trying - /// to produce as many elements as the finite 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 tryZipL xs ys = zipL xs ys |> Result.toOption @@ -494,8 +501,8 @@ module InfiniteSeq = /// 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). - /// Throws an exception if the infinite sequence hung while trying - /// to produce as many elements as the finite 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 zipR (xs: _ fseq) (InfiniteSeq ys) = @@ -506,8 +513,8 @@ module InfiniteSeq = /// 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). - /// Throws an exception if the infinite sequence hung while trying - /// to produce as many elements as the finite 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 tryZipR xs ys = zipR xs ys |> Result.toOption From b79f69d39b5089a82931c9c8670f5fd17abbccce Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Sun, 15 Mar 2026 21:38:13 -0400 Subject: [PATCH 3/4] Final review comment --- SafetyFirst/Seq.fs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SafetyFirst/Seq.fs b/SafetyFirst/Seq.fs index 3b32795..5e745d9 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -201,7 +201,14 @@ let isHungAfter maxElements xs = seq { for i, x in Seq.indexed xs -> if i < maxElements then x - else raise (InfiniteSequenceEvaluationHung (sprintf "Program execution is considered to have hung, since this sequence produced more than %i elements." maxElements)) + 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) } /// From 773657f948f71970847e9503e20be30dfc269492 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Sun, 15 Mar 2026 21:41:25 -0400 Subject: [PATCH 4/4] More testing for the new Seq.isHungAfter function --- SafetyFirst.Specs/SeqSpec.fs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/SafetyFirst.Specs/SeqSpec.fs b/SafetyFirst.Specs/SeqSpec.fs index da4b652..89dcc3a 100644 --- a/SafetyFirst.Specs/SeqSpec.fs +++ b/SafetyFirst.Specs/SeqSpec.fs @@ -182,7 +182,25 @@ let ``Safe Seq functions always produce the same output as unsafe versions for a alwaysProduceSameOutputForSeq2ExceptNonEmpty Seq.chunkBySize' Seq.chunkBySize alwaysProduceSameOutputForSeq2ExceptNonEmpty Seq.windowed' Seq.windowed -module Splitting = +[] +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 ofNonEmpty (xs:seq<#seq<_>>) = Seq.toList <| Seq.map Seq.toList xs