Смоделировать полиморфные варианты в F#?
Я новичок в F#, поэтому заранее простите меня, если это глупый вопрос или если синтаксис может быть немного неправильным. Надеюсь, в любом случае удастся понять суть вопроса.
Чего я бы хотел добиться, так это возможности сочинять, например, Result
(или Either
или что-то подобное) с разными типами ошибок (размеченные объединения) без создания явного размеченного объединения, которое включает в себя объединение двух других размеченных объединений.
Приведу пример.
Допустим, у меня есть типаж Person
определяется так:
type Person =
{ Name: string
Email: string }
Представьте, что у вас есть функция, которая проверяет имя:
type NameValidationError =
| NameTooLong
| NameTooShort
let validateName person : Result<Person, NameValidationError>
и еще один, который проверяет адрес электронной почты:
type EmailValidationError =
| EmailTooLong
| EmailTooShort
let validateEmail person : Result<Person, EmailValidationError>
Теперь я хочу сочинить validateName
а также validateEmail
, но проблема в том, что тип ошибки в Result
имеет разные виды. Я хотел бы получить функцию (или оператор), которая позволяет мне делать что-то вроде этого:
let validatedPerson = person |> validateName |>>> validateEmail
(|>>>
это "магический оператор")
Используя |>>>
тип ошибки validatedPerson
был бы союз NameValidationError
а также EmailValidationError
:
Result<Person, NameValidationError | EmailValidationError>
Чтобы было понятно, должно быть возможно использовать произвольное количество функций в цепочке композиции, то есть:
let validatedPerson : Result<Person, NameValidationError | EmailValidationError | XValidationError | YValidationError> =
person |> validateName |>>> validateEmail|>>> validateX |>>> validateY
В таких языках, как ReasonML, вы можете использовать так называемые полиморфные варианты, но это недоступно в F# как afaict.
Можно ли как-то имитировать полиморфные варианты, используя дженерики с типами объединения (или любой другой метод)?! Или это невозможно?
2 ответа
Есть несколько интересных предложений по объединению стертых типов, допускающих ограничения анонимного объединения в стиле Typescript.
type Goose = Goose of int
type Cardinal = Cardinal of int
type Mallard = Mallard of int
// a type abbreviation for an erased anonymous union
type Bird = (Goose | Cardinal | Mallard)
Магический оператор, который даст вам NameValidationError | EmailValidationError
его тип существовал бы только во время компиляции. Это будет стерто доobject
во время выполнения.
Но он все еще стоит на наковальне, так что, может быть, мы все еще сможем получить читабельный код, стирая сами?
Оператор композиции может "стереть" (на самом деле прямоугольник) тип ошибки результата:
let (|>>) input validate =
match input with
| Ok(v) -> validate v |> Result.mapError(box)
| Error(e) -> Error(box e)
и у нас может быть частичный активный шаблон, чтобы сделать случаи DU сопоставления типов приемлемыми.
let (|ValidationError|_|) kind = function
| Error(err) when Object.Equals(kind, err) -> Some ()
| _ -> None
Пример (с суперсмещенными проверками):
let person = { Name = "Bob"; Email = "bob@email.com "}
let validateName person = Result.Ok(person)
let validateEmail person = Result.Ok(person)
let validateVibe person = Result.Error(NameTooShort)
let result = person |> validateName |>> validateVibe |>> validateEmail
match result with
| ValidationError NameTooShort -> printfn "Why is your name too short"
| ValidationError EmailTooLong -> printfn "That was a long address"
| _ -> ()
Это будет шунтировать validateVibe
Вероятно, это более подробный вариант, чем вы бы хотели, но он позволяет вам помещать вещи в DU без явного его определения.
F# имеет Choice
типы, которые определены следующим образом:
type Choice<'T1,'T2> =
| Choice1Of2 of 'T1
| Choice2Of2 of 'T2
type Choice<'T1,'T2,'T3> =
| Choice1Of3 of 'T1
| Choice2Of3 of 'T2
| Choice3Of3 of 'T3
// Going up to ChoiceXOf7
С вашими существующими функциями вы можете использовать их следующим образом:
// This function returns Result<Person,Choice<NameValidationError,EmailValidationError>>
let validatePerson person =
validateName person
|> Result.mapError Choice1Of2
|> Result.bind (validateEmail >> Result.mapError Choice2Of2)
Вот как вы бы восприняли результат:
let displayValidationError person =
match person with
| Ok p -> None
| Error (Choice1Of2 NameTooLong) -> Some "Name too long"
| Error (Choice2Of2 EmailTooLong) -> Some "Email too long"
// etc.
Если вы хотите добавить третью проверку в validatePerson
вам нужно будет переключиться на Choice<_,_,_>
Случаи DU, например Choice1Of3
и так далее.