Error handling F中表示失败的判别并集的优雅解决方案#
我正在从F中创建和捕获异常转移到围绕Error handling F中表示失败的判别并集的优雅解决方案#,error-handling,f#,conventions,Error Handling,F#,Conventions,我正在从F中创建和捕获异常转移到围绕Result构建的东西。我发现,这与我最初追求的用歧视性联盟来代表失败是一致的,但我遇到的问题是,我的失败歧视性联盟有很多不同的案例: type TypedValue = | Integer of int | Long of int64 | … type Failure = | ArgumentOutOfRange of {| Argument : TypedValue; Minimum : TypedValue; Maximum : TypedValue
Result
构建的东西。我发现,这与我最初追求的用歧视性联盟来代表失败是一致的,但我遇到的问题是,我的失败
歧视性联盟有很多不同的案例:
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
)
泛型是一种可能性,但是如果使用Result
在Failure中的'T
在您有自定义类型的错误需要处理的情况下,或者在您有一些其他逻辑来传播错误的情况下,而不是在标准异常实现的情况下,那么使用Result
会有什么意义呢(例如,如果您可以在出现错误的情况下继续运行代码)。但是,我不会将其用作异常的1:1替换-它只会使您的代码变得不必要的复杂和繁琐,而不会真正给您带来很多好处
为了回答您的问题,由于您在受歧视的工会中镜像标准.NET异常,您可能只需在结果中使用标准.NET异常,然后使用结果另一个选项(以及我个人通常做的事)是使用故障
联合中的特定案例对特定于域的故障进行建模,然后使用通用的意外错误
案例,该案例将exn
作为其数据,并处理任何与域无关的故障。然后,当一个域的错误发生在另一个域时,您可以使用结果.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 }))
失败如果你还没有发现这一点,一定要仔细阅读:还要注意,结果类型是选择类型的一个特例,它会给你两种以上可能的结果。不是说你应该开始在很多地方使用它来处理错误,而是要意识到它的有用性。异常有一个共同的基类,它应该是在我们通常使用的语言中,很难声明每种类型的联合异常类。我认为有人可以提出类似的论点,失败
类型应该只有几个相关的案例,其中一个案例是公共基类。消息ie字符串对我来说也很有意义(对于错误处理,我将其视为限制公共“基类”)。
type Failure =
| ArgumentOutOfRange of {| Argument : obj; Minimum : obj; Maximum : obj |}
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 }))