3

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.

retendo
  • 1,309
  • 2
  • 12
  • 18
  • 1
    Have you tried inheriting your CE builder class from the task builder? – Fyodor Soikin Jan 15 '21 at 17:26
  • Yes, but I found no way of overriding the Return member function, which seems to be required for this to work in case of subclassing. – retendo Jan 16 '21 at 00:28
  • I also tried composition instead of inheritance, as in: create a new CE builder that receives an instance of a TaskResultBuilder and call methods on it. this achieves goal #1 easily, but the problem is that I would need to reimplement and delegate all the other methods of TaskResultBuilder to achieve goal #2. I also don't know if it is "safe" to have a CE builder "own" another CE builder. – retendo Jan 16 '21 at 00:38
  • Can you post the code of the inheritance solution and describe what problems you run into? – Fyodor Soikin Jan 16 '21 at 01:46
  • @FyodorSoikin Added the inheritance version – retendo Jan 17 '21 at 12:27

0 Answers0