diff --git a/docs/TaskSeqAdvanced.fsx b/docs/TaskSeqAdvanced.fsx index 3f2198fc..18a5c5e3 100644 --- a/docs/TaskSeqAdvanced.fsx +++ b/docs/TaskSeqAdvanced.fsx @@ -4,8 +4,8 @@ title: Advanced Task Sequence Operations category: Documentation categoryindex: 2 index: 6 -description: Advanced F# task sequence operations including groupBy, mapFold, distinct, partition, countBy, compareWith, withCancellation and in-place editing. -keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, groupBy, mapFold, distinct, distinctUntilChanged, except, partition, countBy, compareWith, withCancellation, insertAt, removeAt, updateAt +description: Advanced F# task sequence operations including groupBy, mapFold, threadState, distinct, partition, countBy, compareWith, withCancellation and in-place editing. +keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, groupBy, mapFold, threadState, distinct, distinctUntilChanged, except, partition, countBy, compareWith, withCancellation, insertAt, removeAt, updateAt --- *) (*** condition: prepare ***) @@ -26,7 +26,7 @@ keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, groupBy, mapFold, disti # Advanced Task Sequence Operations This page covers advanced `TaskSeq<'T>` operations: grouping, stateful transformation with -`mapFold`, deduplication, set-difference, partitioning, counting by key, lexicographic +`mapFold` and `threadState`, deduplication, set-difference, partitioning, counting by key, lexicographic comparison, cancellation, and positional editing. *) @@ -113,15 +113,52 @@ let numbered : Task = --- -## scan and scanAsync +## threadState and threadStateAsync -`TaskSeq.scan` is the streaming sibling of `fold`: it emits each intermediate state as a new -element, starting with the initial state: +`TaskSeq.threadState` is the lazy, streaming counterpart to `mapFold`. It threads a state +accumulator through the sequence while yielding each mapped result — but unlike `mapFold` it +never materialises the results into an array, and it discards the final state. This makes it +suitable for infinite sequences and pipelines where intermediate results should be streamed rather +than buffered: *) let numbers : TaskSeq = TaskSeq.ofSeq (seq { 1..5 }) +// Produce a running total without collecting the whole sequence first +let runningSum : TaskSeq = + numbers + |> TaskSeq.threadState (fun acc x -> acc + x, acc + x) 0 + +// yields lazily: 1, 3, 6, 10, 15 + +(** + +Compare with `scan`, which also emits a running result but prepends the initial state: + +```fsharp +let viaScan = numbers |> TaskSeq.scan (fun acc x -> acc + x) 0 +// yields: 0, 1, 3, 6, 10, 15 (one extra initial element) + +let viaThreadState = numbers |> TaskSeq.threadState (fun acc x -> acc + x, acc + x) 0 +// yields: 1, 3, 6, 10, 15 (no initial element; result == new state here) +``` + +`TaskSeq.threadStateAsync` accepts an asynchronous folder: + +*) + +let asyncRunningSum : TaskSeq = + numbers + |> TaskSeq.threadStateAsync (fun acc x -> Task.fromResult (acc + x, acc + x)) 0 + +(** + +`TaskSeq.scan` is the streaming sibling of `fold`: it emits each intermediate state as a new +element, starting with the initial state: + +*) + let runningTotals : TaskSeq = numbers |> TaskSeq.scan (fun acc n -> acc + n) 0 diff --git a/docs/TaskSeqCombining.fsx b/docs/TaskSeqCombining.fsx index eea4d22c..14e1834e 100644 --- a/docs/TaskSeqCombining.fsx +++ b/docs/TaskSeqCombining.fsx @@ -4,8 +4,8 @@ title: Combining Task Sequences category: Documentation categoryindex: 2 index: 5 -description: How to combine, slice and reshape F# task sequences using append, zip, take, skip, chunkBySize, windowed and related combinators. -keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, append, zip, zip3, take, skip, drop, truncate, takeWhile, skipWhile, chunkBySize, windowed, pairwise, concat +description: How to combine, slice and reshape F# task sequences using append, zip, zipWith, splitAt, take, skip, chunkBySize, chunkBy, windowed and related combinators. +keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, append, zip, zip3, zipWith, zipWith3, splitAt, take, skip, drop, truncate, takeWhile, skipWhile, chunkBySize, chunkBy, windowed, pairwise, concat --- *) (*** condition: prepare ***) @@ -26,7 +26,7 @@ keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, append, zip, zip3, take # Combining Task Sequences This page covers operations that combine multiple sequences or reshape a single sequence: append, -zip, concat, slicing with take/skip, chunking and windowing. +zip, zipWith, concat, slicing with take/skip/splitAt, chunking and windowing. *) @@ -121,6 +121,54 @@ let triples : TaskSeq = TaskSeq.zip3 letters nums booleans --- +## zipWith and zipWithAsync + +`TaskSeq.zipWith` is like `zip` but applies a mapping function to produce a result instead of +yielding a tuple. The result sequence stops when the shorter source ends: + +*) + +let addPairs : TaskSeq = TaskSeq.zipWith (+) nums nums +// 2, 4, 6, 8 + +(** + +`TaskSeq.zipWithAsync` accepts an asynchronous mapping function: + +*) + +let asyncProduct : TaskSeq = + TaskSeq.zipWithAsync (fun a b -> Task.fromResult (a * b)) nums nums +// 1, 4, 9, 16, ... + +(** + +--- + +## zipWith3 and zipWithAsync3 + +`TaskSeq.zipWith3` combines three sequences with a three-argument mapping function, stopping at +the shortest: + +*) + +let sumThree : TaskSeq = + TaskSeq.zipWith3 (fun a b c -> a + b + c) nums nums nums +// 3, 6, 9, 12, ... + +(** + +`TaskSeq.zipWithAsync3` takes an asynchronous three-argument mapper: + +*) + +let asyncSumThree : TaskSeq = + TaskSeq.zipWithAsync3 (fun a b c -> Task.fromResult (a + b + c)) nums nums nums + +(** + +--- + ## pairwise `TaskSeq.pairwise` produces a sequence of consecutive pairs. An input with fewer than two elements @@ -158,6 +206,26 @@ let atMost10 : TaskSeq = consecutive |> TaskSeq.truncate 10 // 1, 2, 3, 4, --- +## splitAt + +`TaskSeq.splitAt count` splits a sequence into a prefix array and a lazy remainder sequence. The +prefix always contains _at most_ `count` elements — it never throws when the sequence is shorter. +The remainder sequence is a lazy view over the unconsumed tail and can be iterated once: + +*) + +let splitData : TaskSeq = TaskSeq.ofList [ 1..10 ] + +let splitExample : Task> = TaskSeq.splitAt 4 splitData +// prefix = [|1;2;3;4|], rest = lazy 5,6,7,8,9,10 + +(** + +Unlike `take`/`skip`, a single `splitAt` call evaluates elements only once — the prefix is +materialised eagerly and the rest is yielded lazily without re-reading the source. + +--- + ## skip and drop `TaskSeq.skip count` skips exactly `count` elements and throws if the source is shorter: @@ -245,6 +313,33 @@ let chunks : TaskSeq = consecutive |> TaskSeq.chunkBySize 2 --- +## chunkBy and chunkByAsync + +`TaskSeq.chunkBy projection` groups _consecutive_ elements with the same key into `(key, elements[])` pairs. +A new group starts each time the key changes. Unlike `groupBy`, elements that are not adjacent are +**not** merged, so the source order is preserved and the sequence can be infinite: + +*) + +let words : TaskSeq = TaskSeq.ofList [ "apple"; "apricot"; "banana"; "blueberry"; "cherry" ] + +let byFirstLetter : TaskSeq = + words |> TaskSeq.chunkBy (fun w -> w[0]) +// ('a', [|"apple";"apricot"|]), ('b', [|"banana";"blueberry"|]), ('c', [|"cherry"|]) + +(** + +`TaskSeq.chunkByAsync` accepts an asynchronous projection: + +*) + +let byFirstLetterAsync : TaskSeq = + words |> TaskSeq.chunkByAsync (fun w -> Task.fromResult w[0]) + +(** + +--- + ## windowed `TaskSeq.windowed windowSize` produces a sliding window of exactly `windowSize` consecutive diff --git a/docs/TaskSeqConsuming.fsx b/docs/TaskSeqConsuming.fsx index d10ba3c8..707ddefd 100644 --- a/docs/TaskSeqConsuming.fsx +++ b/docs/TaskSeqConsuming.fsx @@ -5,7 +5,7 @@ category: Documentation categoryindex: 2 index: 4 description: How to consume F# task sequences using iteration, folding, searching, collecting and aggregation operations. -keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, iter, fold, find, toArray, toList, sum, average, length, head, last, contains, exists, forall +keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, iter, fold, find, toArray, toList, sum, average, length, head, last, firstOrDefault, lastOrDefault, contains, exists, forall --- *) (*** condition: prepare ***) @@ -220,7 +220,7 @@ let countEvens : Task = numbers |> TaskSeq.lengthBy (fun n -> n % 2 = 0) --- -## Element access: head, last, item, exactlyOne +## Element access: head, last, item, exactlyOne, firstOrDefault, lastOrDefault *) @@ -237,6 +237,20 @@ let tryOnly : Task = TaskSeq.singleton 42 |> TaskSeq.tryExactlyOne (** +`TaskSeq.firstOrDefault` and `TaskSeq.lastOrDefault` return a caller-supplied default when the +sequence is empty — useful as a concise alternative to `tryHead`/`tryLast` when `None` would +need to be immediately unwrapped: + +*) + +let firstOrZero : Task = TaskSeq.empty |> TaskSeq.firstOrDefault 0 // 0 +let lastOrZero : Task = TaskSeq.empty |> TaskSeq.lastOrDefault 0 // 0 + +let firstOrMinus1 : Task = numbers |> TaskSeq.firstOrDefault -1 // 1 +let lastOrMinus1 : Task = numbers |> TaskSeq.lastOrDefault -1 // 5 + +(** + --- ## Searching: find, pick, contains, exists, forall diff --git a/docs/TaskSeqGenerating.fsx b/docs/TaskSeqGenerating.fsx index 51369c41..f8ca4a5d 100644 --- a/docs/TaskSeqGenerating.fsx +++ b/docs/TaskSeqGenerating.fsx @@ -5,7 +5,7 @@ category: Documentation categoryindex: 2 index: 2 description: How to create F# task sequences using the taskSeq computation expression, init, unfold, and conversion functions. -keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, taskSeq, computation expression, init, unfold, ofArray, ofSeq, singleton, replicate +keywords: F#, task sequences, TaskSeq, IAsyncEnumerable, taskSeq, computation expression, init, unfold, ofArray, ofSeq, singleton, replicate, replicateInfinite, replicateUntilNoneAsync --- *) (*** condition: prepare ***) @@ -237,7 +237,7 @@ let countingAsync : TaskSeq = --- -## singleton, replicate and empty +## singleton, replicate, replicateInfinite, and empty *) @@ -249,6 +249,49 @@ let nothing : TaskSeq = TaskSeq.empty (** +`TaskSeq.replicateInfinite` yields a constant value indefinitely. Always combine it with a +bounding operation such as `take` or `takeWhile`: + +*) + +let infinitePings : TaskSeq = TaskSeq.replicateInfinite "ping" + +let first10pings : TaskSeq = infinitePings |> TaskSeq.take 10 + +(** + +`TaskSeq.replicateInfiniteAsync` calls a function on every step, useful for polling or streaming +side-effectful sources: + +*) + +let mutable counter = 0 + +let pollingSeq : TaskSeq = + TaskSeq.replicateInfiniteAsync (fun () -> + task { + counter <- counter + 1 + return counter + }) + +let first5counts : TaskSeq = pollingSeq |> TaskSeq.take 5 + +(** + +`TaskSeq.replicateUntilNoneAsync` stops when the function returns `None`, making it easy to +wrap a pull-based source that signals end-of-stream with `None`: + +*) + +let readLine (reader: System.IO.TextReader) = + TaskSeq.replicateUntilNoneAsync (fun () -> + task { + let! line = reader.ReadLineAsync() + return if line = null then None else Some line + }) + +(** + --- ## delay