«Объединение» дискриминированных союзов в F #? - PullRequest
0 голосов
/ 29 ноября 2018

Исходя из этого вопроса , у меня возникла проблема с комбинированием различных типов Result вместе.

(ниже приведен надуманный пример, а не реальный код)

Предположим, у меня есть функция, которая читает файл:

type ReadFileError = 
| FileNotFound of string

let readFile (path : string) : Result<string, ReadFileError> =
  // --- 8< --- 

Ифункция, которая каким-то образом ее анализирует:

type JsonParseError = 
| InvalidStructure of string

let parseJson (content : string) : Result<Json, JsonParseError> = 
  // --- 8< --- 

Теперь я могу объединить их, чтобы создать функцию, которая читает и анализирует файл:

type ReadJsonError = 
| ReadFileError of ReadFileError
| JsonParseError of JsonParseError

let readJson (path : string) : Result<Json, ReadJsonError> = 
  match path |> readFile with
  | Ok content -> 
    match content |> parseJson with 
    | Ok json -> Ok json
    | Error e -> Error (ReadJsonError.JsonParseError e)
  | Error e -> Error (ReadJsonError.ReadFileError e)

Как видите, объединение типов ошибокдовольно неловкоМне нужно определить новый тип объединения и правильно обернуть сторону Error.Это не то, о чем вам нужно беспокоиться при использовании подхода, основанного на исключениях, поскольку throw является открытым в отношении типов.

Можно ли сделать стиль Result удобным при комбинировании ошибок разных типов?

Ответы [ 2 ]

0 голосов
/ 29 ноября 2018

Объединение типов ошибок - проблема с Result, которую я осознал только при ее попытке.

С исключениями это "решается", когда все исключения наследуют базовый класс.Таким образом, одним подобным подходом может быть type R<'T> = Result<'T, exn>

Однако я нахожу это непривлекательным и обычно попадаю в шаблон, где я определяю свой собственный тип результата, который допускает агрегированные сбои однородного типа.

Aнемного похоже на это

type BadResult = Message of string | Exception of exn
type BadTree = Leaf of BadResult | Fork of BadTree*BadTree
type R<'T> = Good of 'T | Bad of BadTree

Другой подход может заключаться в объединении Result отказов с использованием Choice.Не уверен, что в результате вы окажетесь в особенно привлекательном месте.

let bind (t : Result<'T, 'TE>) (uf  'T -> Result<'U, 'UE>) : Result<'U, Choice<'TE, 'TU>> = ...

Возможно, это вам совсем не поможет, но, возможно, породит несколько идей о том, как поступить?

0 голосов
/ 29 ноября 2018

Короткий и простой ответ первым.Я вернусь позже для более длинного ответа.

Если вы создаете монолитное приложение, предлагается создать только один тип ошибки для всего приложения:

type AllErrors = 
| FileNotFound of string
| InvalidJsonStructure of string
| OtherErrors ...

Это дастодно хорошее место, где все ошибки определены, и вы можете создать единую printError и другие функции обработки ошибок.

Иногда это невозможно, например, если ваш код является модульным, и каждый модуль имеет свой собственныйErrorType, тогда у вас есть два варианта, все равно создайте уникальный тип и сопоставьте его или создайте вложенный, составной тип, как вы это сделали.Это твое решение.В обоих случаях вы используете Result.mapError

Синтаксически существует много способов сделать это.Чтобы избежать вложенных match s, вы используете Result.bind и Result.mapError

let readJson (path : string) : Result<Json, ReadJsonError> = 
    readFile path 
    |> Result.mapError ReadFileError
    |> Result.bind (fun content ->
    parseJson content 
    |> Result.mapError JsonParseError
    )

Если бы у вас было result Вычислительное выражение:

type Builder() =
    member inline this.Return          x       = Ok  x
    member inline this.ReturnFrom      x       =     (x:Result<_,_>)
    member        this.Bind           (w , r ) = Result.bind  r w
    member inline this.Zero           ()       = Ok ()
let result = Builder()

, то это выглядело бы какэто:

let readJson (path : string) : Result<Json, ReadJsonError> = result {
    let! content = readFile  path    |> Result.mapError ReadFileError
    return!        parseJson content |> Result.mapError JsonParseError
}

с операторами:

let (>>= ) vr f = Result.bind     f vr
let (|>>.) vr f = Result.mapError f vr

это может быть так:

let readJson (path : string) : Result<Json, ReadJsonError> = 
    readFile path     |>>. ReadFileError
    >>= fun content ->
    parseJson content |>>. JsonParseError

или это:

let readJson (path : string) : Result<Json, ReadJsonError> = 
    path 
    |>   readFile  
    |>>. ReadFileError
    >>= fun content ->
    content 
    |>   parseJson 
    |>>. JsonParseError

илидаже это:

let readJson (path : string) : Result<Json, ReadJsonError> = 
    path           |>
    readFile       |>>. 
    ReadFileError  >>= 
    fun content    ->
    content        |>   
    parseJson      |>>. 
    JsonParseError

Хорошо, этот последний просто для удовольствия.Я не защищаю ваш код, подобный этому.

также вы можете просто создать унифицированную версию ваших функций:

let readFileU = readFile  >> Result.mapError ReadFileError
let readJsonU = parseJson >> Result.mapError JsonParseError

и связать их с помощью оператора Клейсли:

let (>=>) f g p = f p |> Result.bind g

let readJson = readFileU >=> readJsonU

Еще один ответ будет позже ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...