demystifyfp / FsToolkit.ErrorHandling

An opinionated F# Library for error handling
https://demystifyfp.gitbook.io/fstoolkit-errorhandling
MIT License
462 stars 59 forks source link

usage with npgsql #258

Closed jozefRudy closed 6 months ago

jozefRudy commented 6 months ago

Describe the bug

      task {
          use! conn = _dataSource.OpenConnectionAsync()

works just fine but

      taskResult {
          use! conn = _dataSource.OpenConnectionAsync()

not so much. _dataSource.OpenConnectionAsync() has a signature NpgsqlDataSource.OpenConnectionAsync(?cancellationToken: Threading.CancellationToken) : ValueTask<NpgsqlConnection>

Maybe i am overseeing something?

njlr commented 6 months ago

This works for me:

#r "nuget: FsToolkit.ErrorHandling, 4.15.1"
#r "nuget: FsToolkit.ErrorHandling.TaskResult, 4.15.1"
#r "nuget: Npgsql, 8.0.2"

open System.Threading
open System.Threading.Tasks
open FsToolkit.ErrorHandling
open Npgsql

type IDataSource =
  abstract member OpenConnectionAsync : ?cancellationToken : CancellationToken -> ValueTask<NpgsqlConnection>

let foo (_dataSource : IDataSource) =
  task {
    use! conn = _dataSource.OpenConnectionAsync()

    ()
  }

let bar (_dataSource : IDataSource) =
  taskResult {
    use! conn = _dataSource.OpenConnectionAsync()

    ()
  }

Perhaps you have an old version of a package?

jozefRudy commented 6 months ago

i think not. it's some intricate type problem beyond my current understanding.

What you wrote is right, hence if i simply write a function, it correctly behaves in both cases. Problem is implementing an interface.

while task version works, taskResult not ->

    interface IStrategyRepository with
        member this.Save(req: BacktestRequest, saved: DateTime) : Task<Result<int, exn>> =
            taskResult {
                try
                    use! conn = _dataSource.OpenConnectionAsync()

says Type constraint mismatch. The type     'TaskResultCode<Result<int,'a>,exn,Result<int,'a>>' is not compatible with type     'TaskResultCode<int,exn,int>'

version placed simply in a class has exact same signature as implementation of interface

member StrategyRepository.Save2: req: BacktestRequest * saved: DateTime -> Task<Result<int,exn>>

while task works. simply placed inside class, they both work (only implementation of interface fails). that is puzzling for me.

njlr commented 6 months ago

Classes also work for me:

#r "nuget: FsToolkit.ErrorHandling, 4.15.1"
#r "nuget: FsToolkit.ErrorHandling.TaskResult, 4.15.1"
#r "nuget: Npgsql, 8.0.2"

open System
open System.Threading
open System.Threading.Tasks
open FsToolkit.ErrorHandling
open Npgsql

type IDataSource =
  abstract member OpenConnectionAsync : ?cancellationToken : CancellationToken -> ValueTask<NpgsqlConnection>

type BacktestRequest = class end

type IStrategyRepository =
  abstract member Save : req : BacktestRequest * saved : DateTime -> Task<Result<int, exn>>

type StrategyRepository1(_dataSource : IDataSource) =
  interface IStrategyRepository with
    member this.Save(req : BacktestRequest, saved : DateTime) =
      task {
        use! conn = _dataSource.OpenConnectionAsync()

        return Ok 1
      }

type StrategyRepository2(_dataSource : IDataSource) =
  interface IStrategyRepository with
    member this.Save(req : BacktestRequest, saved : DateTime) =
      taskResult {
        use! conn = _dataSource.OpenConnectionAsync()

        return 1
      }

Maybe I'm missing something?

jozefRudy commented 6 months ago

i think i understand it more, maybe educational. I think try complicates it. Notice how with taskResult i need to use return!, and in task normal return is enough.

type StrategyRepository(dataSource: NpgsqlDataSource) =
    let _dataSource: NpgsqlDataSource = dataSource

    member this.Save2(req: BacktestRequest, saved: DateTime) : Task<Result<int, exn>> =
        task {
            try
                use! conn = _dataSource.OpenConnectionAsync()
                return Ok 1
            with e ->
                return Error e
        }

    interface IStrategyRepository with
        member this.Save(req: BacktestRequest, saved: DateTime) : Task<Result<int, exn>> =
            taskResult {
                try
                    use! conn = _dataSource.OpenConnectionAsync()
                    return! Ok 1
                with e ->
                    return! Error e
            }

alternatively, i could just return 1 from taskResult version, but then condtion in return! Error e is not satisfied (and return Error e does not work either, nor return e. so whole time this was a problem, while it showed that first statement inside taskResult was problematic, which it was not). So that was what was a problem, so it's not really 1:1 replacement in some cases.

njlr commented 6 months ago

Inside of a taskResult, return! Ok 1 is equivalent to return 1.

These examples might help with understanding:

let _ : Task<Result<int, string>> = 
  taskResult {
    return 1
  }

let _ : Task<Result<Result<int, string>, string>> = 
  taskResult {
    return Ok 1
  }

let _ : Task<Result<int, string>> = 
  taskResult {
    return! Ok 1
  }
jozefRudy commented 6 months ago

yes but for error, there is no escaping using return! Error exn i think?

    let _: Task<Result<int, exn>> =
        taskResult {
            try
                return 1
            //or return! Ok 1
            with e ->
                return! Error e
                // return e will not work
        }

Thank you for this explanation, was assigning the error to a different cause.

njlr commented 6 months ago

yes but for error, there is no escaping using return! Error exn i think?

    let _: Task<Result<int, exn>> =
        taskResult {
            try
                return 1
            //or return! Ok 1
            with e ->
                return! Error e
                // return e will not work
        }

Thank you for this explanation, was assigning the error to a different cause.

Yes, this is by design.

return in a taskResult is only for the happy path. In your case, this is of type int, so an Error exn is a type mismatch.