I recently published the initial version of what is effectively my first open-source software package, a Railway-Oriented Programming Framework for F# that encapsulates Tasks, Async Workflows, and Lazy Computations (as well as any code written inside an operation {} computation as "Operations" and provides a library for working with Operations and their Results.

I have published the source for the project on GitHub, and I am looking for feedback on the design of the framework, the implementation of the OperationBuilder (effectively using Tasks behind the scenes) and any possible use-cases I have missed that could also be encapsulated as Operations. The github project has some documentation and examples in the readme, but the basic way the framework is used is as follows:

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

This returns an Operation<string,exn>. An Operation is a type representing a computation, which can be either Completed, InProcess, Deferred, or Cancelled.

type Operation<'result,'event> =
| Completed of OperationResult<'result,'event>
| InProcess of InProcessOperation<'result,'event>
| Deferred of EventingLazy<Operation<'result,'event>>
| Cancelled of 'event list

Operations that are not completed or cancelled can be completed by calling Operation.wait or Operation.waitAsync. Multiple Operations can be executed concurrently by calling Operation.Parallel. Operation Results are either Success or Failure, and the Failure event type is assumed to be System.Exception unless a domain event is explicitly used in the Operation computation or in another Operation called by the computation.

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

Operations and their Results can be used in other Operation computations, or acted upon by a set of functions provided in the framework, such as Operation.ok to test if an Operation was successful or Result.either which maps an OperationResult with one function if it was successful or another function if it failed.

Domain events are propagated from one Operation to another, such as in the following example:

type FileAccessEvents =
| FileOpenedSuccessfully
| FileReadSuccessfully
| FileNotFound of string
| 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]

Calling readFile with a valid file path returns an Operation that, when completed, contains the text from the file and the events [FileOpenedSuccessfully; FileReadSuccessfully].

When returning to a user interface or a non-ROP caller, the Operation can be passed to Operation.returnOrFail to either return the successful result or raise an exception based on the failure.



Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.