Элегантное решение для дискриминированных союзов, представляющих сбои в F # - PullRequest
2 голосов
/ 27 марта 2019

Я ухожу от создания и перехвата исключений в 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 не будет единственным случаем в дискриминируемом объединении, и в некоторых случаях может потребоваться больше параметров типа или их вообще нет.

Ответы [ 2 ]

2 голосов
/ 27 марта 2019

Использование Result<'T, 'TError> имеет большой смысл в тех случаях, когда у вас есть особые виды ошибок, которые вам определенно необходимо обработать, или в случаях, когда у вас есть какая-то другая логика для распространения ошибок, чем та, которая реализована в стандартных исключениях (например, если вы может продолжить выполнение кода, несмотря на то, что произошла ошибка). Однако я бы не стал использовать его в качестве замены 1: 1 для исключений - он просто сделает ваш код излишне сложным и громоздким, не принося вам особых преимуществ.

Чтобы ответить на ваш вопрос, поскольку вы зеркально отражаете стандартные исключения .NET в своем дискриминационном объединении, вы, вероятно, могли бы просто использовать стандартное исключение .NET для вашего типа Result и использовать Result<'T, exn> в качестве типа данных:

if arg < 10 then Error(ArgumentOutOfRangeException("arg", "Value is too small"))
else OK(arg - 1)

Относительно случая объединения ArgumentOutOfRange и TypedValue - причина использования чего-то вроде TypedValue, как правило, заключается в том, что вам необходимо сопоставить шаблон с возможными значениями и что-то с ними сделать. В случае исключений, что вы хотите сделать со значениями? Если вам просто нужно сообщить о них пользователю, то вы можете использовать obj, который позволит вам легко распечатать их (будет не так просто получить числовые значения и провести с ними дальнейшие вычисления, но я не буду Я думаю, тебе это нужно).

type Failure = 
  | ArgumentOutOfRange of {| Argument : obj; Minimum : obj; Maximum : obj |}
1 голос
/ 27 марта 2019

Другой вариант (и то, что я обычно делаю лично) - смоделировать ваши доменные сбои с конкретными случаями в вашем 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 }))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...