Смоделировать полиморфные варианты в 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 и так далее.

Другие вопросы по тегам