我正在远离创建和捕获F#中的异常到围绕Result<'T, 'TError>
构建的东西。我找到了this,它同意我最初的追求用歧视的联盟代表失败,但我遇到了为我的Failure
歧视联盟有很多不同情况的问题:
type TypedValue =
| Integer of int
| Long of int64
| …
type Failure =
| ArgumentOutOfRange of {| Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue |}
| BufferTooSmall of {| RequiredSize : int |}
| Exception of exn
| IndexOutOfRange of {| Index : int |}
| …
我不希望有多种类型专门用于错误处理。这种“类型值”的东西根本不是优雅的,因为我要么创建冲突的名称(Byte
与System.Byte
)或创建长名称以避免冲突(| UnsignedByte of byte
)。
泛型是一种可能性,但那么'T
中的Failure<'T>
代表什么呢? ArgumentOutOfRange
不会是歧视联盟中的唯一案例,有些案例可能需要更多类型参数或根本不需要。
使用Result<'T, 'TError>
在你有明确需要处理的自定义错误的情况下,或者在你有一些其他逻辑传播错误而不是标准异常实现的错误的情况下使用Result
很有意义(例如,如果你可以继续运行代码这是一个错误的事实)。但是,我不会将它用作异常的1:1替代品 - 它只会使您的代码变得不必要复杂和繁琐,而不会给您带来太多好处。
要回答您的问题,因为您在区分联合中镜像标准.NET异常,您可能只需在Result<'T, exn>
类型中使用标准.NET异常并使用if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)
作为您的数据类型:
ArgumentOutOfRange
关于TypedValue
union case和TypedValue
- 使用像obj
这样的东西的原因通常是你需要对可能的值进行模式匹配并对它们做些什么。如果出现异常,您希望如何处理这些值?如果您只需要向用户报告,那么您可以使用type Failure =
| ArgumentOutOfRange of {| Argument : obj; Minimum : obj; Maximum : obj |}
来轻松打印它们(获取数值并用它们进行进一步计算并不容易,但我不认为你需要那个)。
Failure
另一个选择(以及我通常做的,个人)是在你的UnexpectedError
联盟中使用特定情况模拟特定于域的失败,然后有一个通用的exn
案例,它将Result.mapError
作为其数据并处理任何非域 - 相关的失败。然后,当一个域中的错误发生在另一个域中时,您可以使用open System
// Top-level domain failures
type EntityValidationError =
| EntityIdMustBeGreaterThanZero of int64
| InvalidTenant of string
| UnexpectedException of exn
// Sub-domain specific failures
type AccountValidationError =
| AccountNumberMustBeTenDigits of string
| AccountNameIsRequired of string
| EntityValidationError of EntityValidationError // Sub-domain representaiton of top-level failures
| AccountValidationUnexpectedException of exn
// Sub-domain Entity
// The fields would probably be single-case unions rather than primitives
type Account =
{
Id: int64
AccountNumber: string
}
module EntityId =
let validate id =
if id > 0L
then Ok id
else Error (EntityIdMustBeGreaterThanZero id)
module AccountNumber =
let validate number =
if number |> String.length = 10 && number |> Seq.forall Char.IsDigit
then Ok number
else Error (AccountNumberMustBeTenDigits number)
module Account =
let create id number =
id
|> EntityId.validate
|> Result.mapError EntityValidationError // Convert to sub-domain error type
|> Result.bind (fun entityId ->
number
|> AccountNumber.validate
|> Result.map (fun accountNumber -> { Id = entityId; AccountNumber = accountNumber }))
在它们之间进行转换。以下是我建模的真实域名的示例:
qazxswpoi