Error handling 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

我正在从F中创建和捕获异常转移到围绕
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 }))