Is there an easy way to inherit a computation expressions behaviour?

152 Views Asked by At

How would you build a new computation expression that inherits most behaviour from an existing one, but you might need to override some behaviour?

Context:

I will use CE as the abbreviation for computation expression from now on.

I'm using https://github.com/demystifyfp/FsToolkit.ErrorHandling

I have a "layered" application with a repository that fetches from a database layer.

All functions in the database layer so far return Task<Result<'T, DatabaseError>> via the taskResult { ... } CE from FsToolkit.ErrorHandling.

In the repository though, I always want to return Task<Result<'T, RepositoryError>>, where RepositoryError can easily be derived from DatabaseError, which means that most of my code looks like this:

let getAll con offset chunk = taskResult {
    let! products =
        ProductEntity.allPaginated offset chunk con
        |> TaskResult.mapError RepositoryError.fromDatabaseError
    let! totalCount =
        ProductEntity.countAll con
        |> TaskResult.mapError RepositoryError.fromDatabaseError
    return { Items = products; TotalCount = totalCount }
}

The goal:

  1. I would like to have all those TaskResult.mapError RepositoryError.fromDatabaseError calls be made under the hood inside a new CE. Let's call it repoTaskResult { ... }
  2. I need to have all the current functionality of the original taskResult { ... } CE
let getAll con offset chunk = repoTaskResult {
    let! products = ProductEntity.allPaginated offset chunk con
    let! totalCount = ProductEntity.countAll con
    return { Items = products; TotalCount = totalCount }
}

Edit:

A. Trying to solve it with inheritance (inferring the correct type does not work though)

type RepositoryTaskResultBuilder() =
    inherit TaskResultBuilder()
    member __.Bind(databaseTaskResult: Task<Result<'T, DatabaseError>>, binder) =
        let repoTaskResult = databaseTaskResult |> TaskResult.mapError RepositoryError.fromDatabaseError
        __.Bind(taskResult = repoTaskResult, binder = binder)

let repoTaskResult = RepositoryTaskResultBuilder()

Usage:

let getAll con offset chunk = repoTaskResult {
    let! other = Task.singleton 1
    let! products = ProductEntity.allPaginated offset chunk con
    let! totalCount = ProductEntity.countAll con
    return { Items = products; TotalCount = totalCount }
}

Conclusion for inheritance version:

Goal #2 is achieved easily, other is correctly inferred as int. Goal #1 though is not achieved without any help for the type inference. By default let! seems to be inferred as one of the more generic Bind methods of the underlying TaskResultBuilder. That means that the whole return type is being inferred as Task<Result<'T, DatabaseError>>.

If you would help out the type inference by replacing the return statement with return! Result<_,RepositoryError>.Ok { Items = products; TotalCount = totalCount }, then you are good to go, as now the let! statements before are correctly using the newly implemented Bind method.

0

There are 0 best solutions below

Related Questions in F#