diff --git a/README.md b/README.md index 9cf9bcd..cac8cf2 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,11 @@ An implementation of [`IAsyncEnumerable<'T>`][3] as a computation expression: `taskSeq { ... }` with an accompanying `TaskSeq` module and functions, that allow seamless use of asynchronous sequences similar to F#'s native `seq` and `task` CE's. -* Latest stable version: [0.3.0 is on NuGet][nuget]. -* Latest prerelease version: [0.4.0-alpha.1 is on NuGet][nuget]. +* Latest stable version: [0.4.0 is on NuGet][nuget]. ## Release notes -See [release notes.txt](release-notes.txt) for the version history of `TaskSeq`. See [Status overview](#status--planning) for current status of the surface area of `TaskSeq`. +See [Releases](https://github.com/fsprojects/FSharp.Control.TaskSeq/releases) for the an extensive version history of `TaskSeq`. See [Status overview](#status--planning) below for a progress report. ----------------------------------------- diff --git a/backdate-tags.cmd b/backdate-tags.cmd new file mode 100644 index 0000000..1444417 --- /dev/null +++ b/backdate-tags.cmd @@ -0,0 +1,60 @@ +@echo off + +REM Batch file to override the date and/or message of existing tag, or create a new +REM tag that takes the same date/time of an existing commit. +REM +REM Usage: +REM > backdate-tags.cmd v0.1.1 "New message" +REM +REM How it works: +REM * checkout the commit at the moment of the tag +REM * get the date/time of that commit and store in GIT_COMMITER_DATE env var +REM * recreate the tag (it will now take the date of its commit) +REM * push tags changes to remove (with --force) +REM * return to HEAD +REM +REM PS: +REM * these escape codes are for underlining the headers so they stand out between all GIT's output garbage +REM * the back-dating trick is taken from here: https://stackoverflow.com/questions/21738647/change-date-of-git-tag-or-github-release-based-on-it + +ECHO. +ECHO List existing tags: +git tag -n + +ECHO. +ECHO Checkout to tag: +git checkout tags/%1 + +REM Output the first string, containing the date of commit, and put it in a file +REM then set the contents of that file to env var GIT_COMMITTER_DATE (which in turn is needed to enable back-dating) +REM then delete the temp file +ECHO. +ECHO Retrieve original commit date + +git show --format=%%aD | findstr "^[MTWFS][a-z][a-z],.*" > _date.tmp +< _date.tmp (set /p GIT_COMMITTER_DATE=) +del _date.tmp + +ECHO Committer date for tag: %GIT_COMMITTER_DATE% +ECHO Overriding tag '%1' with text: %2 +ECHO. +REM Override (with -af) the tag, if it exists (no quotes around %2) +git tag -af %1 -m %2 + +ECHO. +ECHO Updated tag: +git tag --points-at HEAD -n +ECHO. + +REM Push to remove and override (with --force) +ECHO Push changes to remote +git push --tags --force + +REM Go back to original HEAD +ECHO. +ECHO Back to original HEAD +git checkout - + +ECHO. +ECHO List of all tags +git tag -n diff --git a/release-notes.txt b/release-notes.txt index df20d85..7f0d630 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -1,38 +1,43 @@ Release notes: -0.4.x (unreleased) - - overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136 +0.4.0 + - overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234 - new surface area functions, fixes #208: * TaskSeq.take, skip, #209 * TaskSeq.truncate, drop, #209 * TaskSeq.where, whereAsync, #217 * TaskSeq.skipWhile, skipWhileInclusive, skipWhileAsync, skipWhileInclusiveAsync, #219 * TaskSeq.max, min, maxBy, minBy, maxByAsync, minByAsync, #221 + * TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt, #236 + * TaskSeq.forall, forallAsync, #240 + * TaskSeq.concat (overloads: seq, array, resizearray, list), #237 - Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135 - - BINARY INCOMPATIBILITY: 'TaskSeq' module is now static members on 'TaskSeq<_>', fixes #184 + - Performance: several inline and allocation improvements + - BINARY INCOMPATIBILITY: 'TaskSeq' module replaced by static members on 'TaskSeq<_>', fixes #184 - DEPRECATIONS (warning FS0044): - type 'taskSeq<_>' is renamed to 'TaskSeq<_>', fixes #193 - function 'ValueTask.ofIValueTaskSource` renamed to `ValueTask.ofSource`, fixes #193 - function `ValueTask.FromResult` is renamed to `ValueTask.fromResult`, fixes #193 0.4.0-alpha.1 - - fixes not calling Dispose for 'use!', 'use', or `finally` blocks #157 (by @bartelink) + - bugfix: not calling Dispose for 'use!', 'use', or `finally` blocks #157 (by @bartelink) - BREAKING CHANGE: null args now raise ArgumentNullException instead of NullReferenceException, #127 - adds `let!` and `do!` support for F#'s Async<'T>, #79, #114 - adds TaskSeq.takeWhile, takeWhileAsync, takeWhileInclusive, takeWhileInclusiveAsync, #126 (by @bartelink) - adds AsyncSeq vs TaskSeq comparison chart, #131 - - removes release-notes.txt from file dependencies, but keep in the package, #138 + - bugfix: removes release-notes.txt from file dependencies, but keep in the package, #138 0.3.0 - - internal renames, improved doc comments, signature files for complex types, hide internal-only types, fixes #112. + - improved xml doc comments, signature files for exposing types, fixes #112. - adds support for static TaskLike, allowing the same let! and do! overloads that F# task supports, fixes #110. - implements 'do!' for non-generic Task like with Task.Delay, fixes #43. - - adds support for 'for .. in ..' with task sequences in F# tasks and async, #75, #93 and #99 (with help from @theangrybyrd). + - task and async CEs extended with support for 'for .. in ..do' with TaskSeq, #75, #93, #99 (in part by @theangrybyrd). - adds TaskSeq.singleton, #90 (by @gusty). - - fixes overload resolution bug with 'use' and 'use!', #97 (thanks @peterfaria). + - bugfix: fixes overload resolution bug with 'use' and 'use!', #97 (thanks @peterfaria). - improves TaskSeq.empty by not relying on resumable state, #89 (by @gusty). - - does not throw exceptions anymore for unequal lengths in TaskSeq.zip, fixes #32. + - bugfix: does not throw exceptions anymore for unequal lengths in TaskSeq.zip, fixes #32. + - BACKWARD INCOMPATIBILITY: several internal-only types now hidden 0.2.2 - removes TaskSeq.toSeqCachedAsync, which was incorrectly named. Use toSeq or toListAsync instead. diff --git a/src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj b/src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj index 53be692..83587f7 100644 --- a/src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj +++ b/src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj @@ -16,18 +16,23 @@ - - - - + + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all 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 d625d93..dbf1cfc 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -60,16 +60,20 @@ - + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj index 297af81..9e6b6d9 100644 --- a/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj +++ b/src/FSharp.Control.TaskSeq/FSharp.Control.TaskSeq.fsproj @@ -13,7 +13,7 @@ The 'taskSeq' computation expression adds support for awaitable asynchronous sequences with similar ease of use and performance to F#'s 'task' CE, with minimal overhead through ValueTask under the hood. TaskSeq brings 'seq' and 'task' together in a safe way. Generates optimized IL code through resumable state machines, and comes with a comprehensive set of functions in module 'TaskSeq'. See README for documentation and more info. - Copyright 2023 + Copyright 2022-2024 https://github.com/fsprojects/FSharp.Control.TaskSeq https://github.com/fsprojects/FSharp.Control.TaskSeq taskseq-icon.png @@ -22,7 +22,7 @@ Generates optimized IL code through resumable state machines, and comes with a c False nuget-package-readme.md $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../release-notes.txt")) - taskseq;f#;computation expression;IAsyncEnumerable;task;async;asyncseq; + taskseq;f#;fsharp;asyncseq;seq;sequences;sequential;threading;computation expression;IAsyncEnumerable;task;async;iteration True snupkg @@ -57,7 +57,15 @@ Generates optimized IL code through resumable state machines, and comes with a c - - + + + + true + diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index cb5eefe..ff98586 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -274,7 +274,7 @@ type TaskSeq = static member append: source1: TaskSeq<'T> -> source2: TaskSeq<'T> -> TaskSeq<'T> /// - /// Concatenates a task sequence with a non-async F# in + /// Concatenates a task sequence with a (non-async) F# in /// and returns a single task sequence. /// /// @@ -285,7 +285,7 @@ type TaskSeq = static member appendSeq: source1: TaskSeq<'T> -> source2: seq<'T> -> TaskSeq<'T> /// - /// Concatenates a non-async F# in with a task sequence in + /// Concatenates a (non-async) F# in with a task sequence in /// and returns a single task sequence. /// /// diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs index 958678a..6ac6d0e 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fs @@ -525,16 +525,16 @@ module LowPriority = // and we need a way to distinguish these two methods. // // Types handled: - // - ValueTask (non-generic, because it implements GetResult() -> unit) + // - (non-generic) ValueTask (because it implements GetResult() -> unit) // - ValueTask<'T> (because it implements GetResult() -> 'TResult) - // - Task (non-generic, because it implements GetResult() -> unit) + // - (non-generic) Task (because it implements GetResult() -> unit) // - any other type that implements GetAwaiter() // // Not handled: // - Task<'T> (because it only implements GetResult() -> unit, not GetResult() -> 'TResult) [] - member inline _.Bind< ^TaskLike, 'T, 'U, ^Awaiter, 'TOverall + member inline _.Bind< ^TaskLike, 'T, 'U, ^Awaiter when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) and ^Awaiter :> ICriticalNotifyCompletion and ^Awaiter: (member get_IsCompleted: unit -> bool) diff --git a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi index 4c185be..e060704 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeqBuilder.fsi @@ -173,7 +173,7 @@ module LowPriority = type TaskSeqBuilder with [] - member inline Bind< ^TaskLike, 'T, 'U, ^Awaiter, 'TOverall> : + member inline Bind< ^TaskLike, 'T, 'U, ^Awaiter> : task: ^TaskLike * continuation: ('T -> ResumableTSC<'U>) -> ResumableTSC<'U> when ^TaskLike: (member GetAwaiter: unit -> ^Awaiter) and ^Awaiter :> ICriticalNotifyCompletion diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index d7773ce..66a92f2 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -62,7 +62,21 @@ module internal TaskSeqInternal = let inline raiseEmptySeq () = invalidArg "source" "The input task sequence was empty." - let inline raiseCannotBeNegative name = invalidArg name "The value must be non-negative." + /// Moves the enumerator to its first element, assuming it has just been allocated. + /// Raises "The input sequence was empty" if there was no first element. + let inline moveFirstOrRaiseUnsafe (e: IAsyncEnumerator<_>) = task { + let! hasFirst = e.MoveNextAsync() + + if not hasFirst then + invalidArg "source" "The input task sequence was empty." + } + + /// Tests the given integer value and raises if it is -1 or lower. + let inline raiseCannotBeNegative name value = + if value >= 0 then + () + else + invalidArg name $"The value must be non-negative, but was {value}." let inline raiseOutOfBounds name = invalidArg name "The value or index must be within the bounds of the task sequence." @@ -183,10 +197,7 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let! nonEmpty = e.MoveNextAsync() - - if not nonEmpty then - raiseEmptySeq () + do! moveFirstOrRaiseUnsafe e let mutable acc = e.Current @@ -202,10 +213,7 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let! nonEmpty = e.MoveNextAsync() - - if not nonEmpty then - raiseEmptySeq () + do! moveFirstOrRaiseUnsafe e let value = e.Current let mutable accProjection = projection value @@ -228,10 +236,7 @@ module internal TaskSeqInternal = task { use e = source.GetAsyncEnumerator CancellationToken.None - let! nonEmpty = e.MoveNextAsync() - - if not nonEmpty then - raiseEmptySeq () + do! moveFirstOrRaiseUnsafe e let value = e.Current let! projValue = projectionAsync value @@ -276,7 +281,10 @@ module internal TaskSeqInternal = let count = match count with - | Some c -> if c >= 0 then c else raiseCannotBeNegative (nameof count) + | Some c -> + raiseCannotBeNegative (nameof count) c + c + | None -> Int32.MaxValue match initializer with @@ -741,9 +749,7 @@ module internal TaskSeqInternal = let skipOrTake skipOrTake count (source: TaskSeq<_>) = checkNonNull (nameof source) source - - if count < 0 then - raiseCannotBeNegative (nameof count) + raiseCannotBeNegative (nameof count) count match skipOrTake with | Skip -> @@ -907,8 +913,7 @@ module internal TaskSeqInternal = /// InsertAt or InsertManyAt let insertAt index valueOrValues (source: TaskSeq<_>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -933,8 +938,7 @@ module internal TaskSeqInternal = } let removeAt index (source: TaskSeq<'T>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -951,8 +955,7 @@ module internal TaskSeqInternal = } let removeManyAt index count (source: TaskSeq<'T>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 @@ -970,8 +973,7 @@ module internal TaskSeqInternal = } let updateAt index value (source: TaskSeq<'T>) = - if index < 0 then - raiseCannotBeNegative (nameof index) + raiseCannotBeNegative (nameof index) index taskSeq { let mutable i = 0 diff --git a/src/FSharp.Control.TaskSeq/Utils.fs b/src/FSharp.Control.TaskSeq/Utils.fs index c02bab3..ec076f2 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fs +++ b/src/FSharp.Control.TaskSeq/Utils.fs @@ -1,22 +1,15 @@ namespace FSharp.Control -open System.Threading.Tasks open System -open System.Diagnostics -open System.Threading +open System.Threading.Tasks [] module ValueTaskExtensions = - /// Extensions for ValueTask that are not available in NetStandard 2.1, but are - /// available in .NET 5+. We put them in Extension space to mimic the behavior of NetStandard 2.1 type ValueTask with - - /// (Extension member) Gets a task that has already completed successfully. static member inline CompletedTask = - // This mimics how it is done in .NET itself + // This mimics how it is done in net5.0 and later internally Unchecked.defaultof - module ValueTask = let False = ValueTask() let True = ValueTask true @@ -24,15 +17,15 @@ module ValueTask = let inline ofSource taskSource version = ValueTask(taskSource, version) let inline ofTask (task: Task<'T>) = ValueTask<'T> task - let inline ignore (vtask: ValueTask<'T>) = + let inline ignore (valueTask: ValueTask<'T>) = // this implementation follows Stephen Toub's advice, see: // https://github.com/dotnet/runtime/issues/31503#issuecomment-554415966 - if vtask.IsCompletedSuccessfully then + if valueTask.IsCompletedSuccessfully then // ensure any side effect executes - vtask.Result |> ignore + valueTask.Result |> ignore ValueTask() else - ValueTask(vtask.AsTask()) + ValueTask(valueTask.AsTask()) [] let inline FromResult (value: 'T) = ValueTask<'T> value @@ -40,7 +33,6 @@ module ValueTask = [] let inline ofIValueTaskSource taskSource version = ofSource taskSource version - module Task = let inline fromResult (value: 'U) : Task<'U> = Task.FromResult value let inline ofAsync (async: Async<'T>) = task { return! async } @@ -72,14 +64,12 @@ module Async = let inline ofTask (task: Task<'T>) = Async.AwaitTask task let inline ofUnitTask (task: Task) = Async.AwaitTask task let inline toTask (async: Async<'T>) = task { return! async } - let inline bind binder (task: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder task } - let inline ignore (async': Async<'T>) = async { - let! _ = async' - return () - } + let inline ignore (async: Async<'T>) = Async.Ignore async let inline map mapper (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { let! result = async return mapper result } + + let inline bind binder (async: Async<'T>) : Async<'U> = ExtraTopLevelOperators.async { return! binder async } diff --git a/src/FSharp.Control.TaskSeq/Utils.fsi b/src/FSharp.Control.TaskSeq/Utils.fsi index d34a1e5..b171720 100644 --- a/src/FSharp.Control.TaskSeq/Utils.fsi +++ b/src/FSharp.Control.TaskSeq/Utils.fsi @@ -6,10 +6,12 @@ open System.Threading.Tasks.Sources [] module ValueTaskExtensions = - type System.Threading.Tasks.ValueTask with - /// (Extension member) Gets a task that has already completed successfully. - static member inline CompletedTask: System.Threading.Tasks.ValueTask + /// Shims back-filling .NET 5+ functionality for use on netstandard2.1 + type ValueTask with + + /// (Extension member) Gets a ValueTask that has already completed successfully. + static member inline CompletedTask: ValueTask module ValueTask = @@ -24,13 +26,13 @@ module ValueTask = /// /// The function is deprecated since version 0.4.0, - /// please use in its stead. See . + /// please use in its stead. See . /// [] val inline FromResult: value: 'T -> ValueTask<'T> /// - /// Initialized a new instance of with an representing + /// Initializes a new instance of with an /// representing its operation. /// val inline ofSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask @@ -42,21 +44,24 @@ module ValueTask = [] val inline ofIValueTaskSource: taskSource: IValueTaskSource -> version: int16 -> ValueTask - /// Creates a ValueTask form a Task<'T> + /// Creates a ValueTask from a Task<'T> val inline ofTask: task: Task<'T> -> ValueTask<'T> - /// Ignore a ValueTask<'T>, returns a non-generic ValueTask. - val inline ignore: vtask: ValueTask<'T> -> ValueTask + /// Convert a ValueTask<'T> into a non-generic ValueTask, ignoring the result + val inline ignore: valueTask: ValueTask<'T> -> ValueTask module Task = - /// Convert an Async<'T> into a Task<'T> + /// Creates a Task<'U> that's completed successfully with the specified result. + val inline fromResult: value: 'U -> Task<'U> + + /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` val inline ofAsync: async: Async<'T> -> Task<'T> - /// Convert a unit-task into a Task + /// Convert a non-generic Task into a Task val inline ofTask: task': Task -> Task - /// Convert a non-task function into a task-returning function + /// Convert a plain function into a task-returning function val inline apply: func: ('a -> 'b) -> ('a -> Task<'b>) /// Convert a Task<'T> into an Async<'T> @@ -66,8 +71,8 @@ module Task = val inline toValueTask: task: Task<'T> -> ValueTask<'T> /// - /// Convert a ValueTask<'T> to a Task<'T>. To use a non-generic ValueTask, - /// consider using: . + /// Convert a ValueTask<'T> to a Task<'T>. For a non-generic ValueTask, + /// consider: . /// val inline ofValueTask: valueTask: ValueTask<'T> -> Task<'T> @@ -80,25 +85,22 @@ module Task = /// Bind a Task<'T> val inline bind: binder: ('T -> #Task<'U>) -> task: Task<'T> -> Task<'U> - /// Create a task from a value - val inline fromResult: value: 'U -> Task<'U> - module Async = /// Convert an Task<'T> into an Async<'T> val inline ofTask: task: Task<'T> -> Async<'T> - /// Convert a unit-task into an Async + /// Convert a non-generic Task into an Async val inline ofUnitTask: task: Task -> Async - /// Convert a Task<'T> into an Async<'T> + /// Starts the `Async<'T>` computation, returning the associated `Task<'T>` val inline toTask: async: Async<'T> -> Task<'T> /// Convert an Async<'T> into an Async, ignoring the result - val inline ignore: async': Async<'T> -> Async + val inline ignore: async: Async<'T> -> Async /// Map an Async<'T> val inline map: mapper: ('T -> 'U) -> async: Async<'T> -> Async<'U> /// Bind an Async<'T> - val inline bind: binder: (Async<'T> -> Async<'U>) -> task: Async<'T> -> Async<'U> + val inline bind: binder: (Async<'T> -> Async<'U>) -> async: Async<'T> -> Async<'U>