From a5f412d9c77a7db009ac2cd46392b1dd86c61e25 Mon Sep 17 00:00:00 2001 From: njlr Date: Sun, 12 Apr 2026 14:28:03 +0100 Subject: [PATCH 1/2] Adds ValueOption variants for choose --- .../FSharp.Control.TaskSeq.Test.fsproj | 1 + .../TaskSeq.ChooseV.Tests.fs | 246 ++++++++++++++++++ src/FSharp.Control.TaskSeq/TaskSeq.fs | 2 + src/FSharp.Control.TaskSeq/TaskSeq.fsi | 25 ++ src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 24 ++ 5 files changed, 298 insertions(+) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.ChooseV.Tests.fs diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 029fc91..31e06a6 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -12,6 +12,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChooseV.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChooseV.Tests.fs new file mode 100644 index 0000000..dc1783a --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.ChooseV.Tests.fs @@ -0,0 +1,246 @@ +module TaskSeq.Tests.ChooseV + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.chooseV +// TaskSeq.chooseVAsync +// + +module EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.chooseV (fun _ -> ValueNone) null + + assertNullArg + <| fun () -> TaskSeq.chooseVAsync (fun _ -> Task.fromResult ValueNone) null + + [)>] + let ``TaskSeq-chooseV`` variant = task { + let! empty = + Gen.getEmptyVariant variant + |> TaskSeq.chooseV (fun _ -> ValueSome 42) + |> TaskSeq.toListAsync + + List.isEmpty empty |> should be True + } + + [)>] + let ``TaskSeq-chooseVAsync`` variant = task { + let! empty = + Gen.getEmptyVariant variant + |> TaskSeq.chooseVAsync (fun _ -> task { return ValueSome 42 }) + |> TaskSeq.toListAsync + + List.isEmpty empty |> should be True + } + +module Immutable = + [)>] + let ``TaskSeq-chooseV can convert and filter`` variant = task { + let chooser number = + if number <= 5 then + ValueSome(char number + '@') + else + ValueNone + + let ts = Gen.getSeqImmutable variant + + let! letters1 = TaskSeq.chooseV chooser ts |> TaskSeq.toArrayAsync + let! letters2 = TaskSeq.chooseV chooser ts |> TaskSeq.toArrayAsync + + String letters1 |> should equal "ABCDE" + String letters2 |> should equal "ABCDE" + } + + [)>] + let ``TaskSeq-chooseVAsync can convert and filter`` variant = task { + let chooser number = task { + return + if number <= 5 then + ValueSome(char number + '@') + else + ValueNone + } + + let ts = Gen.getSeqImmutable variant + + let! letters1 = TaskSeq.chooseVAsync chooser ts |> TaskSeq.toArrayAsync + let! letters2 = TaskSeq.chooseVAsync chooser ts |> TaskSeq.toArrayAsync + + String letters1 |> should equal "ABCDE" + String letters2 |> should equal "ABCDE" + } + +module Immutable2 = + [)>] + let ``TaskSeq-chooseV returns all when chooser always returns ValueSome`` variant = task { + let ts = Gen.getSeqImmutable variant + let! xs = ts |> TaskSeq.chooseV ValueSome |> TaskSeq.toArrayAsync + xs |> should equal [| 1..10 |] + } + + [)>] + let ``TaskSeq-chooseVAsync returns all when chooser always returns ValueSome`` variant = task { + let ts = Gen.getSeqImmutable variant + + let! xs = + ts + |> TaskSeq.chooseVAsync (fun x -> task { return ValueSome x }) + |> TaskSeq.toArrayAsync + + xs |> should equal [| 1..10 |] + } + + [)>] + let ``TaskSeq-chooseV returns empty when chooser always returns ValueNone`` variant = task { + let ts = Gen.getSeqImmutable variant + + do! ts |> TaskSeq.chooseV (fun _ -> ValueNone) |> verifyEmpty + } + + [)>] + let ``TaskSeq-chooseVAsync returns empty when chooser always returns ValueNone`` variant = task { + let ts = Gen.getSeqImmutable variant + + do! + ts + |> TaskSeq.chooseVAsync (fun _ -> task { return ValueNone }) + |> verifyEmpty + } + + [] + let ``TaskSeq-chooseV with singleton sequence and ValueSome chooser returns singleton`` () = task { + let! xs = + taskSeq { yield 42 } + |> TaskSeq.chooseV (fun x -> ValueSome(x * 2)) + |> TaskSeq.toListAsync + + xs |> should equal [ 84 ] + } + + [] + let ``TaskSeq-chooseV with singleton sequence and ValueNone chooser returns empty`` () = + taskSeq { yield 42 } + |> TaskSeq.chooseV (fun _ -> ValueNone) + |> verifyEmpty + + [] + let ``TaskSeq-chooseV can change the element type`` () = task { + // choose maps int -> string voption, verifying type-changing behavior + let chooser n = + if n % 2 = 0 then + ValueSome(sprintf "even-%d" n) + else + ValueNone + + let! xs = + taskSeq { yield! [ 1..6 ] } + |> TaskSeq.chooseV chooser + |> TaskSeq.toListAsync + + xs |> should equal [ "even-2"; "even-4"; "even-6" ] + } + + [] + let ``TaskSeq-chooseVAsync can change the element type`` () = task { + let chooser n = task { + return + if n % 2 = 0 then + ValueSome(sprintf "even-%d" n) + else + ValueNone + } + + let! xs = + taskSeq { yield! [ 1..6 ] } + |> TaskSeq.chooseVAsync chooser + |> TaskSeq.toListAsync + + xs |> should equal [ "even-2"; "even-4"; "even-6" ] + } + +module SideEffects = + [)>] + let ``TaskSeq-chooseV applied multiple times`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + let chooser x number = + if number <= x then + ValueSome(char number + '@') + else + ValueNone + + let! lettersA = ts |> TaskSeq.chooseV (chooser 5) |> TaskSeq.toArrayAsync + let! lettersK = ts |> TaskSeq.chooseV (chooser 15) |> TaskSeq.toArrayAsync + let! lettersU = ts |> TaskSeq.chooseV (chooser 25) |> TaskSeq.toArrayAsync + + String lettersA |> should equal "ABCDE" + String lettersK |> should equal "KLMNO" + String lettersU |> should equal "UVWXY" + } + + [)>] + let ``TaskSeq-chooseVAsync applied multiple times`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + let chooser x number = task { + return + if number <= x then + ValueSome(char number + '@') + else + ValueNone + } + + let! lettersA = TaskSeq.chooseVAsync (chooser 5) ts |> TaskSeq.toArrayAsync + let! lettersK = TaskSeq.chooseVAsync (chooser 15) ts |> TaskSeq.toArrayAsync + let! lettersU = TaskSeq.chooseVAsync (chooser 25) ts |> TaskSeq.toArrayAsync + + String lettersA |> should equal "ABCDE" + String lettersK |> should equal "KLMNO" + String lettersU |> should equal "UVWXY" + } + + [] + let ``TaskSeq-chooseV evaluates each source element exactly once`` () = task { + let mutable count = 0 + + let ts = taskSeq { + for i in 1..5 do + count <- count + 1 + yield i + } + + let! xs = + ts + |> TaskSeq.chooseV (fun x -> if x < 3 then ValueSome x else ValueNone) + |> TaskSeq.toListAsync + + count |> should equal 5 // all 5 elements were visited + xs |> should equal [ 1; 2 ] + } + + [] + let ``TaskSeq-chooseVAsync evaluates each source element exactly once`` () = task { + let mutable count = 0 + + let ts = taskSeq { + for i in 1..5 do + count <- count + 1 + yield i + } + + let! xs = + ts + |> TaskSeq.chooseVAsync (fun x -> task { return if x < 3 then ValueSome x else ValueNone }) + |> TaskSeq.toListAsync + + count |> should equal 5 + xs |> should equal [ 1; 2 ] + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 9051ddc..d3a94f9 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -441,7 +441,9 @@ type TaskSeq private () = } static member choose chooser source = Internal.choose (TryPick chooser) source + static member chooseV chooser source = Internal.chooseV (TryPickV chooser) source static member chooseAsync chooser source = Internal.choose (TryPickAsync chooser) source + static member chooseVAsync chooser source = Internal.chooseV (TryPickVAsync chooser) source static member filter predicate source = Internal.filter (Predicate predicate) source static member filterAsync predicate source = Internal.filter (PredicateAsync predicate) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index f914048..7dae9dc 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1011,6 +1011,18 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member choose: chooser: ('T -> 'U option) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// + /// Applies the given function to each element of the task sequence. Returns + /// a sequence comprised of the results where the function returns . + /// If is asynchronous, consider using . + /// + /// + /// A function to transform items of type into value options of type . + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member chooseV: chooser: ('T -> 'U voption) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// /// Applies the given asynchronous function to each element of the task sequence. /// Returns a sequence comprised of the results where the function returns a result @@ -1024,6 +1036,19 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member chooseAsync: chooser: ('T -> #Task<'U option>) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// + /// Applies the given asynchronous function to each element of the task sequence. + /// Returns a sequence comprised of the results where the function returns a result + /// of . + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function to transform items of type into value options of type . + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member chooseVAsync: chooser: ('T -> #Task<'U voption>) -> source: TaskSeq<'T> -> TaskSeq<'U> + /// /// Returns a new task sequence containing only the elements of the collection /// for which the given function returns . diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 261dbba..c26d174 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -39,6 +39,11 @@ type internal ChooserAction<'T, 'U, 'TaskOption when 'TaskOption :> Task<'U opti | TryPick of try_pick: ('T -> 'U option) | TryPickAsync of async_try_pick: ('T -> 'TaskOption) +[] +type internal ChooserVAction<'T, 'U, 'TaskValueOption when 'TaskValueOption :> Task<'U voption>> = + | TryPickV of try_pickv: ('T -> 'U voption) + | TryPickVAsync of async_try_pickv: ('T -> 'TaskValueOption) + [] type internal PredicateAction<'T, 'TaskBool when 'TaskBool :> Task> = | Predicate of try_filter: ('T -> bool) @@ -1055,6 +1060,25 @@ module internal TaskSeqInternal = | None -> () } + let chooseV chooser (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + + match chooser with + | TryPickV picker -> + for item in source do + match picker item with + | ValueSome value -> yield value + | ValueNone -> () + + | TryPickVAsync picker -> + for item in source do + match! picker item with + | ValueSome value -> yield value + | ValueNone -> () + } + let filter predicate (source: TaskSeq<_>) = checkNonNull (nameof source) source From d5d661e9679f3291f229e58cbe81b40641e0d2ba Mon Sep 17 00:00:00 2001 From: njlr Date: Sun, 12 Apr 2026 14:32:53 +0100 Subject: [PATCH 2/2] Fixes doc suggestion --- src/FSharp.Control.TaskSeq/TaskSeq.fsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 7dae9dc..1caf826 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1014,7 +1014,7 @@ type TaskSeq = /// /// Applies the given function to each element of the task sequence. Returns /// a sequence comprised of the results where the function returns . - /// If is asynchronous, consider using . + /// If is asynchronous, consider using . /// /// /// A function to transform items of type into value options of type .