3

给定 F#task计算表达式,我可以编写:-

task {
    try
        let! accessToken = getAccessTokenAsync a b

        try
            let! resource = getResourceAsync accessToken uri
            // do stuff
        with
            | ex -> printfn "Failed to get API resource.  %s" ex.Message
    with
        | ex -> printfn "Failed to get access token.  %s" ex.Message

    return ()
}

但我想做的是围绕两个函数调用进行非嵌套异常处理。getBlahAsync这可以在 C# 中通过async具有多个awaits 的方法很容易地完成。

如何在 F# 计算表达式中这样做?如果我以简单明了的方式尝试,accessToken从第一个try..with不会流入第二个try..with

(嵌套的问题是该// do stuff部分可能会增长一点,将外部推得with越来越远try。)

如何在 C# 中做到这一点:-

static async Task MainAsync()
{
    String accessToken = null;
    try
    {
        accessToken = await GetAccessTokenAsync("e", "p");
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine("Failed to get access token.  " + ex.Message);
        return;
    }

    String resource = null;
    try
    {
        resource = await GetResourceAsync(accessToken);
    }
    catch (Exception ex)
    {
        Console.Error.WriteLine("Failed to get API resource.  " + ex.Message);
        return;
    }

    // do stuff
}
4

2 回答 2

2

翻译 C# 代码的主要问题是 F# 不允许您使用return提前跳出函数体。您可以通过各种方式避免嵌套异常,但您将无法提前返回。这可以作为另一个计算表达式来实现,但这比你真正想要在这里使用的东西更令人好奇。

我的建议是将函数拆分为一个获取所有资源并处理异常的函数,以及另一个处理异常的函数。这并不能消除嵌套,但会使代码具有相当的可读性。

let doStuff accessToken resource = task {
  // do stuff
}

let getResourcesAndDoStuff a b uri = task {
  try
    let! accessToken = getAccessTokenAsync a b
    try
      let! resource = getResourceAsync accessToken uri
      return! doStuff accessToken resource
    with ex -> 
      printfn "Failed to get API resource.  %s" ex.Message
  with ex ->
    printfn "Failed to get access token.  %s" ex.Message 
}

顺便说一句,您是否有某些特殊原因使用task而不是使用普通的内置 F#async工作流程?这不一定是个问题,但async可以更好地组合并支持取消,因此它通常是一个明智的默认选择。

于 2017-12-22T22:54:42.003 回答
1

在您进行编辑之后,我发现您真正想要的是“提前返回”——一种在到达终点之前“中断”执行流程的能力。这在 F# 中通常是不可能的(尽管一些计算构建器可能会为此提供专门的工具),因为 F# 基本上是基于表达式的,而不是基于语句的。

缺乏早期回报是一件好事,因为它迫使你仔细考虑你的程序应该做什么,而不是仅仅放弃。但这是另一个哲学讨论。

但是,还有其他方法可以达到类似的效果。在这种特定情况下,我会将这两个操作连同它们的异常处理一起放入单独的函数中,然后将这些函数链接在一起:

task {
    let token = task {
        try
            let! t = getAccessTokenAsync a b
            return Some t
        with
            | ex -> printfn "Failed to get access token.  %s" ex.Message
                    return None
    }

    let resouce t = task {
        try 
            let! r = getResourceAsync accessToken uri
            // do stuff
        with 
            | ex -> printfn "Failed to get API resource.  %s" ex.Message
    }

    let! t = token
    match t with
       | None -> return ()
       | Some token -> do! resource token
}

如果您发现自己经常遇到类似问题,您可能需要投资一些包装异常处理和Option链接的辅助函数:

// Applies given Task-returning function to the given Option value,
// if the Option value is None, returns None again.
// This is essentially Option.map wrapped in a task.
let (<*>) f x = task {
    match x with
    | None -> return None
    | Some r -> let! r' = f r
                return Some r'
}

// Executes given Option-returning task, returns None if an exception was thrown.
let try'' errMsg f = task {
    try return! f
    with ex -> 
        printfn "%s %s" errMsg ex.Message
        return None
}

// Executes given task, returns its result wrapped in Some,
// or returns None if an exception was thrown.
let try' errMsg f = try'' errMsg <| task { let! r = f
                                           return Some r }


task {
    let! token = getAccessTokenAsync a b |> try' "Failed to get access token."
    let! resource = getResourceAsync uri <*> token |> try'' "Failed to get API resource."
    do! doStuff <*> resource
}

这说明了处理异常的首选 F# 方式:避免它们,从不抛出它们,而是返回错误类型(上面的示例使用Option<_>,但也请参见 eg Result<_,_>),如果您必须与确实抛出异常的库代码交互,请将它们放在里面将异常转换为错误类型的包装器。

于 2017-12-22T21:19:04.597 回答