Имитировать полиморф c вариантов в F #? - PullRequest
7 голосов
/ 08 марта 2020

Я новичок в 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

(|>>> - это "волхв c operator")

При использовании |>>> тип ошибки validatedPerson будет представлять собой объединение NameValidationError и EmailValidationError:

Result<Person, NameValidationError | EmailValidationError>

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

let validatedPerson : Result<Person, NameValidationError | EmailValidationError | XValidationError | YValidationError> = 
       person |> validateName |>>> validateEmail |>>> validateX |>>> validateY

В таких языках, как ReasonML , вы можете использовать что-то под названием polymorphi c варианты , но это недоступно в F # как afaict.

Можно ли как-нибудь имитировать c polymorphi c вариантов, используя обобщения с типами объединения (или любым другим способом) ?! Или это невозможно?

Ответы [ 2 ]

2 голосов
/ 09 марта 2020

Это, вероятно, более многословно, чем вы хотели бы, но оно позволяет вам помещать вещи в 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 и т. д.

2 голосов
/ 08 марта 2020

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

Оператор magi c, который даст вам 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

...