在F#中模拟多态变体?

8
我是一名新手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>

现在我想组合validateNamevalidateEmail,但问题在于Result中的错误类型不同。我想实现的是一个函数(或运算符),使我能够像这样做某事:
let validatedPerson = person |> validateName |>>> validateEmail

(|>>>是"魔法操作符")

通过使用|>>>validatedPerson的错误类型将成为NameValidationErrorEmailValidationError的联合:

Result<Person, NameValidationError | EmailValidationError>

为了明确起见,在组合链中使用任意数量的函数应该是可能的,即:

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

在像ReasonML这样的语言中,您可以使用称为多态变体的东西,但据我所知,F#中不可用
是否有可能使用联合类型的泛型(或任何其他技术)来模拟多态变体?!还是这是不可能的?

这很有趣。基本上就像Either的单子绑定(例如此处:scala.util.Try[U])),但错误字段中使用了一个未标记的联合类型。但是我不太了解F#,抱歉... - phipsgabler
2个回答

3

有一些有趣的关于擦除类型联合的建议,允许使用类似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上进行短路。

对我来说,这看起来是一个非常有趣的解决方法 :). 但是,如果我理解正确的话,我需要记住自己处理所有错误吗?理想情况下,我希望编译器_强制_我处理所有错误。 - Johan
1
@Johan 我希望他们实现那个匿名 DU 提案 :) 然后我们就可以让编译器类型检查匿名联合中的所有类型。但是 SRTP 真的可以做一些可怕的事情。我希望其他人能发表更有趣的答案。 - Asti
什么是SRTP? - Johan
1
静态解析类型参数[SRTP](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/generics/statically-resolved-type-parameters)。请参见[此答案](https://dev59.com/MLnoa4cB1Zd3GeqPMjuT),其中包含在编译时解析的嵌套列表求和实现。 - Asti

2

这种写法可能比你想要的更冗长,但它确实允许你将东西放入 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等。


这其中一个缺点是当你有半打DUs要覆盖时,会导致类型爆炸。在#538和相关问题中有一个很好的讨论。 - Asti
@Asti 是的,我很想尝试多态变量,但我从未使用过它们,因此我还没有感受到尝试理解更复杂类型错误可能带来的缺点,这会导致代码库更大。 - TheQuickBrownFox
如果你使用过Typescript,你可能已经以某种形式使用过它们。Typescript实现了结构类型,这本身就非常有趣。 - Asti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接