Jestem nowy, aby f # tak wybacz mi z góry, jeśli jest to głupie pytanie lub jeśli składnia może być nieco wyłączona. Mam nadzieję, że w każdym razie możliwe jest zrozumienie dystrybucji pytania.

Chęć osiągnąć możliwość komponowania np. Result 's (lub Either lub coś podobnego) mający różne typy błędów (związki dyskryminowane) bez Tworzenie Wyraźne Dyskryminowany związek, który obejmuje Związek dwóch innych związków dyskryminowanych.

Pozwól mi przedstawić przykład.

Powiedzmy, że mam zdefiniowany typ Person:

type Person =
    { Name: string
      Email: string }

Wyobraź sobie, że masz funkcję, która weryfikuje nazwę:

type NameValidationError = 
  | NameTooLong
  | NameTooShort

let validateName person : Result<Person, NameValidationError>

A inny, który potwierdza adres e-mail:

type EmailValidationError = 
  | EmailTooLong
  | EmailTooShort

let validateEmail person : Result<Person, EmailValidationError>

Teraz chcę komponować validateName i validateEmail, ale problem polega na tym, że typ błędu w Result ma różne typy. Chęć osiągnąć funkcję (lub operator), który pozwala mi zrobić coś takiego:

let validatedPerson = person |> validateName |>>> validateEmail

(|>>> jest "magicznym operatorem")

Używając |>>> typ błędu validatedPerson byłby związkiem NameValidationError i EmailValidationError:

Result<Person, NameValidationError | EmailValidationError>

Aby go było wyjaśnić, powinno być możliwe do użycia dowolnej liczby funkcji w łańcuchu kompozycji, tj.:

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

W językach takich jak Powód Możesz użyć czegoś o nazwie Warianty polimorficzne ale jest to Niedostępny w F # jako AFACT.

Czy byłoby możliwe jakoś naśladne warianty polimorficzne za pomocą rodzajów generycznych z typami unijnymi (lub inną techniką)?! Czy jest to niemożliwe?

7
Johan 8 marzec 2020, 13:42

2 odpowiedzi

Najlepsza odpowiedź

Jest coś ciekawego Propozycje do usuniętych związków typu, umożliwiający dla związku anonimowego stylu typowe ograniczenia.

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) 

Magiczny operator, który dałby ci NameValidationError | EmailValidationError miałaby jego typ tylko w czasie kompilacji. Zostałoby to usunięte do object w czasie wykonywania.

Ale nadal jest na kowadle, więc może nadal możemy mieć trochę czytelnego kodu, robiąc on sami sami?

Operator kompozycji może "usunąć" (pole, naprawdę) typ błędu:

let (|>>) input validate = 
    match input with 
    | Ok(v) -> validate v |> Result.mapError(box) 
    | Error(e) -> Error(box e)        

Możemy mieć częściowy aktywny wzór, aby dopasować się do pasujących przypadków DUS.

let (|ValidationError|_|) kind = function
    | Error(err) when Object.Equals(kind, err) -> Some () 
    | _ -> None

Przykład (z super stronniczych walidacjach):

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"
| _ -> ()

To będzie przesunąć na validateVibe

2
Asti 8 marzec 2020, 17:55

Jest to prawdopodobnie bardziej gadatliwy niż chciałbyś, ale pozwala na to, abyś umieścił rzeczy w Du bez wyraźnego określenia.

F # ma typy Choice, które są zdefiniowane w ten sposób:

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

Dzięki istniejącemu funkcjom korzystałbyś ich w ten sposób:

// This function returns Result<Person,Choice<NameValidationError,EmailValidationError>>
let validatePerson person =
    validateName person
    |> Result.mapError Choice1Of2
    |> Result.bind (validateEmail >> Result.mapError Choice2Of2)

W ten sposób zużywałbyś wynik:

let displayValidationError person =
    match person with
    | Ok p -> None
    | Error (Choice1Of2 NameTooLong) -> Some "Name too long"
    | Error (Choice2Of2 EmailTooLong) -> Some "Email too long"
    // etc.

Jeśli chcesz dodać trzecią walidacja do {x0}}, musisz przełączyć na Choice<_,_,_> DU Cases, np. Choice1Of3 i tak dalej.

2
TheQuickBrownFox 9 marzec 2020, 10:27