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:
- I would like to have all those
TaskResult.mapError RepositoryError.fromDatabaseError
calls be made under the hood inside a new CE. Let's call itrepoTaskResult { ... }
- 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.