簡體   English   中英

F#自定義計算工作流程

[英]F# custom computation workflow

給出結果類型

type Result<'t> = OK of 't | Error of string 

我有這些函數都返回Async <Result <'t>>這些函數組合如下:

let a = async { return Result.OK 1000 }
let b = async { return Result.Error "some message" }

let sum x y = 
    async {
        let! r1 = x
        match r1 with
        | Result.OK v1 -> 
             let! r2 = y
             match r2 with
             | Result.OK v2 -> return v1 + v2
             | Result.Error msg -> return Result.Error msg
        | Result.Error msg -> return Result.Error msg
    }

這段代碼看起來很糟糕,所以我希望這樣:

type Result = Ok of int | Error of string

type MyMonadBuilder() =
    member x.Bind (v,f) = 
        async { 
            let! r = v
            match r with
            | Ok r' -> return! f r'
            | Error msg -> return Error msg
        }

    member x.Return v = async {return Ok v }

    member x.Delay(f) = f()

let mymonad = MyMonadBuilder()
let runMyMonad = Async.RunSynchronously

let a = mymonad { return 10 }
let b = mymonad { return 20 }

let c = 
    mymonad { 
        return Result.Error "Some message"
        //??? The above doesn't work but how do I return a failure here?
    }

let d = 
    async {
        return Ok 1000
    } 
    //how to wrap this async with mymonad such that I can use it together with my other computation expressions?

let sum x y = 
    mymonad {
        let! v1 = x
        let! v2 = y
        return v1 + v2
    }

[<EntryPoint>]
let main argv = 
    let v = sum a b |> runMyMonad
    match v with
    | Ok v' -> printfn "Ok: %A" v'
    | Error msg -> printf "Error: %s" msg

    System.Console.Read() |> ignore
    0 

所以問題是:

  1. 如何編寫函數c使其在mymonad中返回錯誤?
  2. 如何編寫函數d,使其包含與mymonad的異步?
  3. 如何以與Async類似的方式使我的monad參數化?

......這樣我就可以寫了

let f (a:MyMonad<int>) (b:MyMonad<string>) = ...

更新:

另外,我想並行運行幾個mymonad操作,然后查看結果數組以查看錯誤和成功之處。 出於這個原因,我認為使用例外不是一個好主意。

另外,關於問題3,我的意思是使我的類型參數化並且不透明,以使調用者不知道/不在乎他們正在處理異步。 我編寫monad的方式是調用方始終可以使用Async.RunSynchronously運行mymonad表達式。

更新2:

到目前為止,我最終得到以下結果:

  1. 我為MyMonadBuilder的每個成員使用顯式類型
  2. 我將ReturnFrom添加到MyMonadBuilder。 我使用此函數包裝Async <Result <'t>>
  3. 我添加了諸如failwith之類的輔助函數,它創建了一個帶有錯誤值的mymonad

代碼如下所示:

type MyMonad<'t> = 't Result Async

type MyMonadBuilder() =
    member x.Bind<'t> (v,f) : MyMonad<'t>= 
        async { 
            let! r = v
            match r with
            | Ok r' -> return! f r'
            | Error msg -> return Error msg
        }

    member x.Return<'t> v  : MyMonad<'t> = async {return Ok v }
    member x.ReturnFrom<'t> v  : MyMonad<'t> = v

    member x.Delay(f) = f()

let failwith<'t> : string -> MyMonad<'t> = Result.Error >> async.Return

對於我的目的,這看起來相當不錯。 謝謝!

異步工作流通過異常自動支持錯誤處理,因此慣用解決方案只是使用異常。 如果要區分某些特殊類型的錯誤,則可以只定義自定義異常類型:

exception MyError of string

// Workflow succeeds and returns 1000
let a = async { return 1000 }
// Workflow throws 'MyError' exception
// (using return! means that it can be treated as a workflow returning int)
let b = async { return! raise (MyError "some message") }

// Exceptions are automatically propagated
let sum = async {
  let! r1 = a
  let! r2 = b
  return r1 + r2 }

如果要處理異常,可以在異步工作流中使用try ... with MyError msg -> ...

可以定義一個自定義計算生成器,以使用諸如Result類的代數數據類型重新實現此功能,但是除非您有充分的理由這樣做,否則我不建議您使用這種方法-它不適用於標准庫,很復雜,不適合一般的F#風格。

在您的計算表達式中,值的類型為Async<Result<'T>>return自動將類型'T的參數包裝在返回Ok的異步工作流中。 如果要構造表示失敗的值,可以使用return! 並創建一個返回Result.Error的異步工作流。 你可能需要這樣的東西:

let c = mymonad { 
  return! async.Return(Result.Error "Some message")
}
let d = mymonad {
    return 1000
} 

但正如我所說,使用例外是一種更好的方法。

編輯:要在注釋中回答問題-如果您有許多異步計算,您仍然可以將最終結果包裝在您的自定義類型中。 但是,您不需要重建整個異步工作流庫 - 仍可以使用標准異常處理基元操作中的錯誤:

// Primitive async work that may throw an exception
let primitiveAsyncWork = async { ... } 

// A wrapped computation that returns standard Option type
let safeWork = async {
  try 
    let! res = primitiveAsyncWork
    return Some res
  with e -> return None }

// Run 10 instances of safeWork in parallel and filter out failed computations
async { let! results = [ for i in 0 .. 9 -> safeWork ] |> Async.Parallel
        return results |> Seq.choose id }

我的ExtCore庫中的asyncChoice工作流已經實現了-在NuGet上可用,因此您要做的就是添加對項目的引用,在源文件中打開ExtCore.Control命名空間,並開始編寫如下代碼:

open ExtCore.Control

let asyncDivide100By (x : int) =
    asyncChoice {
        if x = 0 then
            return! AsyncChoice.error "Cannot divide by zero."
        else
            return (100 / x)
    }

let divide100By (x : int) =
    let result =
        asyncDivide100By x
        |> Async.RunSynchronously

    match result with
    | Choice1Of2 result ->
        printfn "100 / %i = %i" x result
    | Choice2Of2 errorMsg ->
        printfn "An error occurred: %s" errorMsg


[<EntryPoint>]
let main argv =
    divide100By 10
    divide100By 1
    divide100By 0

    0   // Exit code

asyncChoice是使用F#Core庫中的標准Async<'T>Choice<_,_>類型構造的,因此您不會出現任何兼容性問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM