如何在f#任务计算表达式中的每个步骤上实现非巢异常处理



给定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函数调用周围具有 nested 异常处理。这可以在async方法中很容易在C#中完成,并具有多个awaits。

如何在f#计算表达式中这样做?如果我以简单明了的方式尝试,则第一个try..withaccessToken不会流入第二个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
}

翻译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构成更好并支持取消,因此通常是明智的默认选择。

在您的编辑后,我看到您实际想要的是"早期返回" - 在达到终点之前"中断"执行的能力。通常在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<_>,但也请参见例如Result<_,_>),如果您必须与确实投掷的库代码进行交互例外,将它们放入包装器中,将异常转换为错误类型。

相关内容

  • 没有找到相关文章

最新更新