From fdb316a4caa4972ab4bf12c97f4fe1f73b3a14d2 Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Fri, 22 Dec 2023 02:38:36 +0100 Subject: [PATCH 1/4] Implement `max|min`, `maxBy|minBy` and `maxByAsync|minByAsync` --- src/FSharp.Control.TaskSeq/TaskSeq.fs | 6 ++ src/FSharp.Control.TaskSeq/TaskSeq.fsi | 76 +++++++++++++++++++ src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 71 ++++++++++++++++- 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index d3aedcb9..02b29e44 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -162,6 +162,12 @@ type TaskSeq private () = // Utility functions // + static member max source = Internal.maxMin max source + static member min source = Internal.maxMin min source + static member maxBy projection source = Internal.maxMinBy (<) projection source // looks like 'less than', is 'greater than' + static member minBy projection source = Internal.maxMinBy (>) projection source + static member maxByAsync projection source = Internal.maxMinByAsync (<) projection source // looks like 'less than', is 'greater than' + static member minByAsync projection source = Internal.maxMinByAsync (>) projection source static member length source = Internal.lengthBy None source static member lengthOrMax max source = Internal.lengthBeforeMax max source static member lengthBy predicate source = Internal.lengthBy (Some(Predicate predicate)) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index c34895d2..04ef0253 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -71,6 +71,82 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member lengthByAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// + /// Returns the greatest of all elements of the sequence, compared via . + /// + /// + /// The input task sequence. + /// The largest element of the sequence. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + static member max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison + + /// + /// Returns the smallest of all elements of the sequence, compared via . + /// + /// + /// The input task sequence. + /// The smallest element of the sequence. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + static member min: source: TaskSeq<'T> -> Task<'T> when 'T: comparison + + /// + /// Returns the greatest of all elements of the task sequence, compared via + /// on the result of applying the function to each element. + /// + /// If is asynchronous, use . + /// + /// + /// A function to transform items from the input sequence into comparable keys. + /// The input sequence. + /// The largest element of the sequence. + /// Thrown when the input sequence is null. + /// Thrown when the input sequence is empty. + static member maxBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + + /// + /// Returns the smallest of all elements of the task sequence, compared via + /// on the result of applying the function to each element. + /// + /// If is asynchronous, use . + /// + /// + /// A function to transform items from the input sequence into comparable keys. + /// The input sequence. + /// The smallest element of the sequence. + /// Thrown when the input sequence is null. + /// Thrown when the input sequence is empty. + static member minBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + + /// + /// Returns the greatest of all elements of the task sequence, compared via + /// on the result of applying the function to each element. + /// + /// If is synchronous, use . + /// + /// + /// A function to transform items from the input sequence into comparable keys. + /// The input sequence. + /// The largest element of the sequence. + /// Thrown when the input sequence is null. + /// Thrown when the input sequence is empty. + static member maxByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + + /// + /// Returns the smallest of all elements of the task sequence, compared via + /// on the result of applying the function to each element. + /// + /// If is synchronous, use . + /// + /// + /// A function to transform items from the input sequence into comparable keys. + /// The input sequence. + /// The smallest element of the sequence. + /// Thrown when the input sequence is null. + /// Thrown when the input sequence is empty. + static member minByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + /// /// Returns a task sequence that is given by the delayed specification of a task sequence. /// diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index e59d50c3..8ba1c997 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -163,7 +163,6 @@ module internal TaskSeqInternal = checkNonNull (nameof source) source task { - use e = source.GetAsyncEnumerator CancellationToken.None let mutable go = true let mutable i = 0 @@ -178,6 +177,76 @@ module internal TaskSeqInternal = return i } + let inline maxMin ([] maxOrMin) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let! nonEmpty = e.MoveNextAsync() + + if not nonEmpty then + raiseEmptySeq () + + let mutable acc = e.Current + + while! e.MoveNextAsync() do + acc <- maxOrMin e.Current acc + + return acc + } + + let inline maxMinBy ([] compare) ([] projection) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let! nonEmpty = e.MoveNextAsync() + + if not nonEmpty then + raiseEmptySeq () + + let value = e.Current + let mutable accProjection = projection value + let mutable accValue = value + + while! e.MoveNextAsync() do + let value = e.Current + let currentProjection = projection value + + if compare currentProjection accProjection then + accProjection <- currentProjection + accValue <- value + + return accValue + } + + + let inline maxMinByAsync ([] compare) ([] projectionAsync) (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + let! nonEmpty = e.MoveNextAsync() + + if not nonEmpty then + raiseEmptySeq () + + let value = e.Current + let! projValue = projectionAsync value + let mutable accProjection = projValue + let mutable accValue = value + + while! e.MoveNextAsync() do + let value = e.Current + let! currentProjection = projectionAsync value + + if compare currentProjection accProjection then + accProjection <- currentProjection + accValue <- value + + return accValue + } + let tryExactlyOne (source: TaskSeq<_>) = checkNonNull (nameof source) source From c0c5e80a3785fdc76873683d96d14179a3b2e73a Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sat, 23 Dec 2023 19:00:04 +0100 Subject: [PATCH 2/4] Add tests for `TaskSeq.min|max|minBy|maxBy` and async variants --- .../FSharp.Control.TaskSeq.Test.fsproj | 1 + .../TaskSeq.MaxMin.Tests.fs | 317 ++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.MaxMin.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 3536363d..ffe3bbec 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -32,6 +32,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.MaxMin.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.MaxMin.Tests.fs new file mode 100644 index 00000000..1cb47865 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.MaxMin.Tests.fs @@ -0,0 +1,317 @@ +module TaskSeq.Tests.MaxMin + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.max +// TaskSeq.min +// TaskSeq.maxBy +// TaskSeq.minBy +// TaskSeq.maxByAsync +// TaskSeq.minByAsync +// + +type MinMax = + | Max = 0 + | Min = 1 + | MaxBy = 2 + | MinBy = 3 + | MaxByAsync = 4 + | MinByAsync = 5 + +module MinMax = + let getFunction = + function + | MinMax.Max -> TaskSeq.max + | MinMax.Min -> TaskSeq.min + | MinMax.MaxBy -> TaskSeq.maxBy id + | MinMax.MinBy -> TaskSeq.minBy id + | MinMax.MaxByAsync -> TaskSeq.maxByAsync Task.fromResult + | MinMax.MinByAsync -> TaskSeq.minByAsync Task.fromResult + | _ -> failwith "impossible" + + let getByFunction = + function + | MinMax.MaxBy -> TaskSeq.maxBy + | MinMax.MinBy -> TaskSeq.minBy + | MinMax.MaxByAsync -> fun by -> TaskSeq.maxByAsync (by >> Task.fromResult) + | MinMax.MinByAsync -> fun by -> TaskSeq.minByAsync (by >> Task.fromResult) + | _ -> failwith "impossible" + + let getAll () = + [ MinMax.Max; MinMax.Min; MinMax.MaxBy; MinMax.MinBy; MinMax.MaxByAsync; MinMax.MinByAsync ] + |> List.map getFunction + + let getAllMin () = + [ MinMax.Min; MinMax.MinBy; MinMax.MinByAsync ] + |> List.map getFunction + + let getAllMax () = + [ MinMax.Max; MinMax.MaxBy; MinMax.MaxByAsync ] + |> List.map getFunction + + let isMin = + function + | MinMax.Min + | MinMax.MinBy + | MinMax.MinByAsync -> true + | _ -> false + + let isMax = isMin >> not + + +type AllMinMaxFunctions() as this = + inherit TheoryData() + + do + this.Add MinMax.Max + this.Add MinMax.Min + this.Add MinMax.MaxBy + this.Add MinMax.MinBy + this.Add MinMax.MaxByAsync + this.Add MinMax.MinByAsync + +type JustMin() as this = + inherit TheoryData() + + do + this.Add MinMax.Min + this.Add MinMax.MinBy + this.Add MinMax.MinByAsync + +type JustMax() as this = + inherit TheoryData() + + do + this.Add MinMax.Max + this.Add MinMax.MaxBy + this.Add MinMax.MaxByAsync + +type JustMinMaxBy() as this = + inherit TheoryData() + + do + this.Add MinMax.MaxBy + this.Add MinMax.MinBy + this.Add MinMax.MaxByAsync + this.Add MinMax.MinByAsync + +module EmptySeq = + [)>] + let ``Null source raises ArgumentNullException`` (minMaxType: MinMax) = + let minMax = MinMax.getFunction minMaxType + + assertNullArg <| fun () -> minMax (null: TaskSeq) + + [)>] + let ``Empty sequence raises ArgumentException`` variant = + let test minMax = + let data = Gen.getEmptyVariant variant + + fun () -> minMax data |> Task.ignore + |> should throwAsyncExact typeof + + for minMax in MinMax.getAll () do + test minMax + +module Functionality = + [] + let ``TaskSeq-max should return maximum`` () = task { + let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList + let! max = TaskSeq.max ts + max |> should equal 'Z' + } + + [] + let ``TaskSeq-maxBy should return maximum of input, not the projection`` () = task { + let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList + let! max = TaskSeq.maxBy id ts + max |> should equal 'Z' + + let ts = [ 1..10 ] |> TaskSeq.ofList + let! max = TaskSeq.maxBy (~-) ts + max |> should equal 1 // as negated, -1 is highest, should not return projection, but original + } + + [] + let ``TaskSeq-maxByAsync should return maximum of input, not the projection`` () = task { + let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList + let! max = TaskSeq.maxByAsync Task.fromResult ts + max |> should equal 'Z' + + let ts = [ 1..10 ] |> TaskSeq.ofList + let! max = TaskSeq.maxByAsync (fun x -> Task.fromResult -x) ts + max |> should equal 1 // as negated, -1 is highest, should not return projection, but original + } + + [] + let ``TaskSeq-min should return minimum`` () = task { + let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList + let! min = TaskSeq.min ts + min |> should equal 'A' + } + + [] + let ``TaskSeq-minBy should return minimum of input, not the projection`` () = task { + let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList + let! min = TaskSeq.minBy id ts + min |> should equal 'A' + + let ts = [ 1..10 ] |> TaskSeq.ofList + let! min = TaskSeq.minBy (~-) ts + min |> should equal 10 // as negated, -10 is lowest, should not return projection, but original + } + + [] + let ``TaskSeq-minByAsync should return minimum of input, not the projection`` () = task { + let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList + let! min = TaskSeq.minByAsync Task.fromResult ts + min |> should equal 'A' + + let ts = [ 1..10 ] |> TaskSeq.ofList + let! min = TaskSeq.minByAsync (fun x -> Task.fromResult -x) ts + min |> should equal 10 // as negated, 1 is highest, should not return projection, but original + } + + +module Immutable = + [)>] + let ``TaskSeq-max, maxBy, maxByAsync returns maximum`` variant = task { + let ts = Gen.getSeqImmutable variant + + for max in MinMax.getAllMax () do + let! max = max ts + max |> should equal 10 + } + + [)>] + let ``TaskSeq-min, minBy, minByAsync returns minimum`` variant = task { + let ts = Gen.getSeqImmutable variant + + for min in MinMax.getAllMin () do + let! min = min ts + min |> should equal 1 + } + + [)>] + let ``TaskSeq-maxBy, maxByAsync returns maximum after projection`` variant = task { + let ts = Gen.getSeqImmutable variant + let! max = ts |> TaskSeq.maxBy (fun x -> -x) + max |> should equal 1 // because -1 maps to item '1' + } + + + [)>] + let ``TaskSeq-minBy, minByAsync returns minimum after projection`` variant = task { + let ts = Gen.getSeqImmutable variant + let! min = ts |> TaskSeq.minBy (fun x -> -x) + min |> should equal 10 // because -10 maps to item 10 + } + +module SideSeffects = + [] + let ``TaskSeq-max, maxBy, maxByAsync prove we execute after-effects`` () = task { + let mutable i = 0 + + let ts = taskSeq { + i <- i + 1 + i <- i + 1 + yield i // 2 + i <- i + 1 + yield i // 3 + yield i + 1 // 4 + i <- i + 1 // we should get here + } + + do! ts |> TaskSeq.max |> Task.map (should equal 4) + do! ts |> TaskSeq.maxBy (~-) |> Task.map (should equal 6) // next iteration & negation "-6" maps to "6" + + do! + ts + |> TaskSeq.maxByAsync Task.fromResult + |> Task.map (should equal 12) // no negation + + i |> should equal 12 + } + + [] + let ``TaskSeq-min, minBy, minByAsync prove we execute after-effects test`` () = task { + let mutable i = 0 + + let ts = taskSeq { + i <- i + 1 + i <- i + 1 + yield i // 2 + i <- i + 1 + yield i // 3 + yield i + 1 // 4 + i <- i + 1 // we should get here + } + + do! ts |> TaskSeq.min |> Task.map (should equal 2) + do! ts |> TaskSeq.minBy (~-) |> Task.map (should equal 8) // next iteration & negation + + do! + ts + |> TaskSeq.minByAsync Task.fromResult + |> Task.map (should equal 10) // no negation + + i |> should equal 12 + } + + + [)>] + let ``TaskSeq-max with sequence that changes length`` (minMax: MinMax) = task { + let max = MinMax.getFunction minMax + let mutable i = 0 + + let ts = taskSeq { + i <- i + 10 + yield! [ 1..i ] + } + + do! max ts |> Task.map (should equal 10) + do! max ts |> Task.map (should equal 20) // mutable state dangers!! + do! max ts |> Task.map (should equal 30) // id + } + + [)>] + let ``TaskSeq-min with sequence that changes length`` (minMax: MinMax) = task { + let min = MinMax.getFunction minMax + let mutable i = 0 + + let ts = taskSeq { + i <- i + 10 + yield! [ 1..i ] + } + + do! min ts |> Task.map (should equal 1) + do! min ts |> Task.map (should equal 1) // same min after changing state + do! min ts |> Task.map (should equal 1) // id + } + + [)>] + let ``TaskSeq-minBy, maxBy with sequence that changes length`` (minMax: MinMax) = task { + let mutable i = 0 + + let ts = taskSeq { + i <- i + 10 + yield! [ 1..i ] + } + + let test minMaxFn v = + if MinMax.isMin minMax then + // this ensures the "min" version behaves like the "max" version + minMaxFn (~-) ts |> Task.map (should equal v) + else + minMaxFn id ts |> Task.map (should equal v) + + do! test (MinMax.getByFunction minMax) 10 + do! test (MinMax.getByFunction minMax) 20 + do! test (MinMax.getByFunction minMax) 30 + } From 9a165f1e64f6b700a7508b58e5b2f07ea1be03fd Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sat, 23 Dec 2023 19:00:27 +0100 Subject: [PATCH 3/4] Fix wrong order of compare function for `minBy|maxBy` and variants --- src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 8ba1c997..ed92a9fe 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -195,6 +195,7 @@ module internal TaskSeqInternal = return acc } + // 'compare' is either `<` or `>` (i.e, less-than, greater-than resp.) let inline maxMinBy ([] compare) ([] projection) (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -213,14 +214,14 @@ module internal TaskSeqInternal = let value = e.Current let currentProjection = projection value - if compare currentProjection accProjection then + if compare accProjection currentProjection then accProjection <- currentProjection accValue <- value return accValue } - + // 'compare' is either `<` or `>` (i.e, less-than, greater-than resp.) let inline maxMinByAsync ([] compare) ([] projectionAsync) (source: TaskSeq<_>) = checkNonNull (nameof source) source @@ -240,7 +241,7 @@ module internal TaskSeqInternal = let value = e.Current let! currentProjection = projectionAsync value - if compare currentProjection accProjection then + if compare accProjection currentProjection then accProjection <- currentProjection accValue <- value From 2e9541cb51da3f6011ad8c13ca8a5bdda0ab1a9b Mon Sep 17 00:00:00 2001 From: Abel Braaksma Date: Sat, 23 Dec 2023 19:26:38 +0100 Subject: [PATCH 4/4] Update `readme.md` and package files for `max|min` etc --- README.md | 16 +++++++++++----- assets/nuget-package-readme.md | 9 +++++---- release-notes.txt | 1 + 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a3c499e6..13b7c26c 100644 --- a/README.md +++ b/README.md @@ -289,10 +289,10 @@ This is what has been implemented so far, is planned or skipped: | 🚫 | `mapFoldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | ✅ [#2][] | `mapi` | `mapi` | `mapiAsync` | | | | `mapi2` | `mapi2` | `mapi2Async` | | -| | `max` | `max` | | | -| | `maxBy` | `maxBy` | `maxByAsync` | | -| | `min` | `min` | | | -| | `minBy` | `minBy` | `minByAsync` | | +| ✅ [#221][]| `max` | `max` | | | +| ✅ [#221][]| `maxBy` | `maxBy` | `maxByAsync` | | +| ✅ [#221][]| `min` | `min` | | | +| ✅ [#221][]| `minBy` | `minBy` | `minByAsync` | | | ✅ [#2][] | `ofArray` | `ofArray` | | | | ✅ [#2][] | | `ofAsyncArray` | | | | ✅ [#2][] | | `ofAsyncList` | | | @@ -511,7 +511,12 @@ module TaskSeq = val mapAsync: mapper: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> TaskSeq<'U> val mapi: mapper: (int -> 'T -> 'U) -> source: TaskSeq<'T> -> TaskSeq<'U> val mapiAsync: mapper: (int -> 'T -> #Task<'U>) -> source: TaskSeq<'T> -> TaskSeq<'U> - val ofArray: source: 'T[] -> TaskSeq<'T> + val max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison + val max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison + val maxBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + val minBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + val maxByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison + val minByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison val ofArray: source: 'T[] -> TaskSeq<'T> val ofAsyncArray: source: Async<'T> array -> TaskSeq<'T> val ofAsyncList: source: Async<'T> list -> TaskSeq<'T> val ofAsyncSeq: source: seq> -> TaskSeq<'T> @@ -604,6 +609,7 @@ module TaskSeq = [#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 [#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217 [#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219 +[#221]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/221 [issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues [nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/ diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md index 1e1a7f84..ceaff83d 100644 --- a/assets/nuget-package-readme.md +++ b/assets/nuget-package-readme.md @@ -169,10 +169,10 @@ This is what has been implemented so far, is planned or skipped: | 🚫 | `mapFoldBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") | | ✅ [#2][] | `mapi` | `mapi` | `mapiAsync` | | | | `mapi2` | `mapi2` | `mapi2Async` | | -| | `max` | `max` | | | -| | `maxBy` | `maxBy` | `maxByAsync` | | -| | `min` | `min` | | | -| | `minBy` | `minBy` | `minByAsync` | | +| ✅ [#221][]| `max` | `max` | | | +| ✅ [#221][]| `maxBy` | `maxBy` | `maxByAsync` | | +| ✅ [#221][]| `min` | `min` | | | +| ✅ [#221][]| `minBy` | `minBy` | `minByAsync` | | | ✅ [#2][] | `ofArray` | `ofArray` | | | | ✅ [#2][] | | `ofAsyncArray` | | | | ✅ [#2][] | | `ofAsyncList` | | | @@ -309,3 +309,4 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l [#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 [#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217 [#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219 +[#221]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/221 diff --git a/release-notes.txt b/release-notes.txt index 74096d35..df20d858 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -7,6 +7,7 @@ Release notes: * TaskSeq.truncate, drop, #209 * TaskSeq.where, whereAsync, #217 * TaskSeq.skipWhile, skipWhileInclusive, skipWhileAsync, skipWhileInclusiveAsync, #219 + * TaskSeq.max, min, maxBy, minBy, maxByAsync, minByAsync, #221 - Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135 - BINARY INCOMPATIBILITY: 'TaskSeq' module is now static members on 'TaskSeq<_>', fixes #184