CurryOn.FSharp.Control 0.1.9

Extends the FSharp.Control namespace to add Operations, a Railway-Oriented Programming framework compatible with Async Worfklows and the Task Parallel Library (TPL), including a Computation Expression Builder and library functions.

Install-Package CurryOn.FSharp.Control -Version 0.1.9
dotnet add package CurryOn.FSharp.Control --version 0.1.9
paket add CurryOn.FSharp.Control --version 0.1.9
The NuGet Team does not provide support for this client. Please contact its maintainers for support.

CurryOn.FSharp.Control

The CurryOn.FSharp.Control library extends the FSharp.Control namespace with a framework for enabling the use of Railway-Oriented Programming patterns with the Task Parallel Library (TPL), Async Workflows, and Lazy Computations.

This is accomplished by providing a set of types for working with Operations and their results. An Operation is any function or expression that is intended to participate in the Railway-Oriented patterns, and is created by use of the operation Computation Expression.

open System.IO

let readFile (fileName: string) =
    operation {
        use fileStream = new StreamReader(fileName)
        return! fileStream.ReadToEndAsync()
    }    

The example above creates a function val readFile : fileName:string -&gt; Operation&lt;string,exn&gt; that takes a file name and returns Operation<string,exn> representing the result of reading all text from the file. The Operation type is a discriminated union with four cases:

type Operation<'result,'event> =
| Completed of Result: OperationResult<'result,'event>
| InProcess of IncompleteOperation: InProcessOperation<'result,'event>
| Deferred of Lazy: EventingLazy<Operation<'result,'event>>
| Cancelled of EventsSoFar: 'event list

The cases of the Operation discriminated union represent the possible states of the Operation after invocation. Since the framework supports working with Tasks and Async Workflows, the Operation may not complete immediately, and may be cancelled, so the InProcess and Cancelled cases represent these states. Since the framework supports working with Lazy computations, the Deferred case represents Operations in the state of waiting for a Lazy to be evaluated.

Operations that are not completed can be waited on synchronously using Operation.wait. They can also be waited on with an F# Async using Operation.waitAsync or as a Task using Operation.waitTask. These functions return the same type as the Completed case of the Operation discriminated union, OperationResult&lt;&#39;result,&#39;event&gt;.

type OperationResult<'result,'event> =
| Success of Result: SuccessfulResult<'result,'event>
| Failure of ErrorList: 'event list

The OperationResult type represents the result of a Completed Operation. In the readFile example above, the result type would be OperationResult&lt;string,exn&gt;, since the resulting value is a string, and since the operation may throw exceptions, such as FileNotFoundException. If no exceptions are thrown and the Operation completed successfully, the OperationResult will be the Success case, and the result will be contained within a SuccessfulResult&lt;&#39;result,&#39;event&gt;. If any exception is thrown during the operation, the OperationResult will be the Failure case, and any exceptions thrown will be present in the list.

The SuccessfulResult&lt;&#39;result,&#39;event&gt; type is used to contain the resulting value and any domain events associated with a successful Operation. The SuccessfulResult type also has members .Result and .Events to provide direct access to the result value and the domain events without pattern-matching.

type SuccessfulResult<'result,'event> =
| Value of ResultValue: 'result
| WithEvents of ResultWithEvents: SuccessfulResultWithEvents<'result,'event>

When no domain events are associated with the SuccessfulResult, the Value case will be used, and the &#39;result will be directly accessible. When a successful Operation also returns domain events, the results will be contained in a SuccessfulResultWithEvents&lt;&#39;result,&#39;event&gt; record type.

type SuccessfulResultWithEvents<'result,'event> =
    {
        Value: 'result
        Events: 'event list
    }

This allows the framework to support a usage pattern where a successful Operation can also return domain events, or carry Warnings or Informational messages along with the resulting value. To use the framework in this way, it is common practice to create a discriminated union representing the possible errors, warnings, or domain events. Then, the events can be propogated from one operation to another, such as in the following examples:

type FileAccessEvents =
| FileOpenedSuccessfully
| FileReadSuccessfully
| FileNotFound of string
| FileIsInSystemRootWarning
| UnhandledException of exn // This is returned automatically if an unhandled exception is thrown by an Operation

let getFile (fileName: string) =
    operation {
        let file = FileInfo fileName
        return! if not file.Exists
                then Result.failure [FileNotFound file.FullName]
                else Result.success file
    }

let openFile fileName =
    operation {
        let! file = getFile fileName
        return! file.OpenText() |> Result.successWithEvents <| [FileOpenedSuccessfully]
    }

let readFile fileName = 
    operation {
        use! fileStream = openFile fileName
        let! fileText = fileStream.ReadToEndAsync()
        return! Result.successWithEvents fileText [FileReadSuccessfully]
    }

let writeFile fileName contents =
    operation {
        let! file = getFile fileName
        let stream = file.OpenWrite()
        do! stream.AsyncWrite contents
        return! if file.DirectoryName = Environment.SystemDirectory
                then Result.success ()
                else Result.successWithEvents () [FileIsInSystemRootWarning]
    }

CurryOn.FSharp.Control

The CurryOn.FSharp.Control library extends the FSharp.Control namespace with a framework for enabling the use of Railway-Oriented Programming patterns with the Task Parallel Library (TPL), Async Workflows, and Lazy Computations.

This is accomplished by providing a set of types for working with Operations and their results. An Operation is any function or expression that is intended to participate in the Railway-Oriented patterns, and is created by use of the operation Computation Expression.

open System.IO

let readFile (fileName: string) =
    operation {
        use fileStream = new StreamReader(fileName)
        return! fileStream.ReadToEndAsync()
    }    

The example above creates a function val readFile : fileName:string -&gt; Operation&lt;string,exn&gt; that takes a file name and returns Operation<string,exn> representing the result of reading all text from the file. The Operation type is a discriminated union with four cases:

type Operation<'result,'event> =
| Completed of Result: OperationResult<'result,'event>
| InProcess of IncompleteOperation: InProcessOperation<'result,'event>
| Deferred of Lazy: EventingLazy<Operation<'result,'event>>
| Cancelled of EventsSoFar: 'event list

The cases of the Operation discriminated union represent the possible states of the Operation after invocation. Since the framework supports working with Tasks and Async Workflows, the Operation may not complete immediately, and may be cancelled, so the InProcess and Cancelled cases represent these states. Since the framework supports working with Lazy computations, the Deferred case represents Operations in the state of waiting for a Lazy to be evaluated.

Operations that are not completed can be waited on synchronously using Operation.wait. They can also be waited on with an F# Async using Operation.waitAsync or as a Task using Operation.waitTask. These functions return the same type as the Completed case of the Operation discriminated union, OperationResult&lt;&#39;result,&#39;event&gt;.

type OperationResult<'result,'event> =
| Success of Result: SuccessfulResult<'result,'event>
| Failure of ErrorList: 'event list

The OperationResult type represents the result of a Completed Operation. In the readFile example above, the result type would be OperationResult&lt;string,exn&gt;, since the resulting value is a string, and since the operation may throw exceptions, such as FileNotFoundException. If no exceptions are thrown and the Operation completed successfully, the OperationResult will be the Success case, and the result will be contained within a SuccessfulResult&lt;&#39;result,&#39;event&gt;. If any exception is thrown during the operation, the OperationResult will be the Failure case, and any exceptions thrown will be present in the list.

The SuccessfulResult&lt;&#39;result,&#39;event&gt; type is used to contain the resulting value and any domain events associated with a successful Operation. The SuccessfulResult type also has members .Result and .Events to provide direct access to the result value and the domain events without pattern-matching.

type SuccessfulResult<'result,'event> =
| Value of ResultValue: 'result
| WithEvents of ResultWithEvents: SuccessfulResultWithEvents<'result,'event>

When no domain events are associated with the SuccessfulResult, the Value case will be used, and the &#39;result will be directly accessible. When a successful Operation also returns domain events, the results will be contained in a SuccessfulResultWithEvents&lt;&#39;result,&#39;event&gt; record type.

type SuccessfulResultWithEvents<'result,'event> =
    {
        Value: 'result
        Events: 'event list
    }

This allows the framework to support a usage pattern where a successful Operation can also return domain events, or carry Warnings or Informational messages along with the resulting value. To use the framework in this way, it is common practice to create a discriminated union representing the possible errors, warnings, or domain events. Then, the events can be propogated from one operation to another, such as in the following examples:

type FileAccessEvents =
| FileOpenedSuccessfully
| FileReadSuccessfully
| FileNotFound of string
| FileIsInSystemRootWarning
| UnhandledException of exn // This is returned automatically if an unhandled exception is thrown by an Operation

let getFile (fileName: string) =
    operation {
        let file = FileInfo fileName
        return! if not file.Exists
                then Result.failure [FileNotFound file.FullName]
                else Result.success file
    }

let openFile fileName =
    operation {
        let! file = getFile fileName
        return! file.OpenText() |> Result.successWithEvents <| [FileOpenedSuccessfully]
    }

let readFile fileName = 
    operation {
        use! fileStream = openFile fileName
        let! fileText = fileStream.ReadToEndAsync()
        return! Result.successWithEvents fileText [FileReadSuccessfully]
    }

let writeFile fileName contents =
    operation {
        let! file = getFile fileName
        let stream = file.OpenWrite()
        do! stream.AsyncWrite contents
        return! if file.DirectoryName = Environment.SystemDirectory
                then Result.success ()
                else Result.successWithEvents () [FileIsInSystemRootWarning]
    }

Release Notes

Added Awaitable type and await computation expression, allowing Async and Task to be mixed.

Version History

Version Downloads Last updated
0.1.9 187 7/13/2018
0.1.8 166 6/25/2018
0.1.7 172 6/8/2018
0.1.6 201 2/13/2018
0.1.5 258 2/9/2018
0.1.4 209 2/2/2018
0.1.3 216 1/23/2018
0.1.2 223 1/4/2018
0.1.1 198 1/3/2018
0.1.0 215 1/2/2018