如何在 F# 任务计算表达式中的每个步骤上实现非嵌套异常处理?
How to implement non-nested exception handling on each step in an F# task computation expression?
鉴于 F# task
computation expression 我可以写:-
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
函数调用进行 非嵌套 异常处理。这可以在具有多个 await
的 async
方法中很容易地在 C# 中完成。
如何在 F# 计算表达式中执行此操作?如果我以简单明了的方式尝试,第一个 try..with
的 accessToken
不会流入第二个 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
}
在你编辑之后,我发现你实际上想要的是"early return"——一种在到达终点之前"interrupt"执行流程的能力.这在 F# 中通常是不可能的(尽管一些计算构建器可能为此提供专门的工具),因为 F# 基本上是基于表达式的,而不是基于语句的。
缺乏早期 return 是一件好事,因为它迫使您仔细考虑您的程序应该做什么,而不是仅仅放弃。但这是另一次哲学讨论。
但是,还有其他方法可以达到类似的效果。在这种特定情况下,我会将这两个操作连同它们的异常处理一起放入单独的函数中,然后将这些函数链接在一起:
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# 处理异常的方法:避免它们,永远不要抛出它们,而是 return 错误类型(上面的示例使用 Option<_>
, but also see e.g. Result<_,_>
),如果您必须与库交互抛出异常的代码,将它们放在将异常转换为错误类型的包装器中。
翻译C#代码的主要问题是F#不允许你使用return
提前跳出函数体。您可以通过各种方式避免嵌套异常,但您将无法 return 提早。这可以实现为 another computatione expression,但这更多的是一种好奇心,而不是你真正想在这里使用的东西。
我的建议是将函数拆分为一个获取所有资源和处理异常的函数,以及另一个执行这些操作的函数。这并没有消除嵌套,但会使代码更具可读性。
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# task
computation expression 我可以写:-
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
函数调用进行 非嵌套 异常处理。这可以在具有多个 await
的 async
方法中很容易地在 C# 中完成。
如何在 F# 计算表达式中执行此操作?如果我以简单明了的方式尝试,第一个 try..with
的 accessToken
不会流入第二个 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
}
在你编辑之后,我发现你实际上想要的是"early return"——一种在到达终点之前"interrupt"执行流程的能力.这在 F# 中通常是不可能的(尽管一些计算构建器可能为此提供专门的工具),因为 F# 基本上是基于表达式的,而不是基于语句的。
缺乏早期 return 是一件好事,因为它迫使您仔细考虑您的程序应该做什么,而不是仅仅放弃。但这是另一次哲学讨论。
但是,还有其他方法可以达到类似的效果。在这种特定情况下,我会将这两个操作连同它们的异常处理一起放入单独的函数中,然后将这些函数链接在一起:
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# 处理异常的方法:避免它们,永远不要抛出它们,而是 return 错误类型(上面的示例使用 Option<_>
, but also see e.g. Result<_,_>
),如果您必须与库交互抛出异常的代码,将它们放在将异常转换为错误类型的包装器中。
翻译C#代码的主要问题是F#不允许你使用return
提前跳出函数体。您可以通过各种方式避免嵌套异常,但您将无法 return 提早。这可以实现为 another computatione expression,但这更多的是一种好奇心,而不是你真正想在这里使用的东西。
我的建议是将函数拆分为一个获取所有资源和处理异常的函数,以及另一个执行这些操作的函数。这并没有消除嵌套,但会使代码更具可读性。
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
组合更好并且支持取消,所以它通常是一个明智的默认选择。