何时在F#中使用Discriminate Union和Record Type?

24
我试图在进入复杂的 F# 示例之前明确基础知识。我正在学习的材料介绍了Discriminate Unions和Record类型。我已经审阅了两者的材料,但我仍不清楚为什么我们会使用其中一种。
大多数我创建的玩具示例似乎都可以在两种类型中实现。记录似乎非常接近我所认为的 C# 中的对象,但我正在尝试避免依赖于映射到 C# 作为了解 F# 的方式。
所以...
- 是否有明显的理由使用其中一种? - 是否存在某些规范案例适用于其中一种? - 是否存在某些功能在其中一种中可用,而在另一种中则不可用?

1
该页面在末尾有一个简短的段落,链接为http://msdn.microsoft.com/en-us/library/dd233205.aspx。 - John Palmer
4个回答

29

可以将记录视为“and”,而判别联合可视为“or”。 这是一个字符串和一个整数:

type MyRecord = { myString: string
                  myInt: int }

虽然这个值可以是字符串或整数,但不能同时是两者:

type MyUnion = | Int of int
               | Str of string

这个虚构的游戏可以在标题界面、游戏中或显示最终分数,但只能选择其中一项。

type Game =
  | Title
  | Ingame of Player * Score * Turn
  | Endgame of Score

那么,在 DU 中有没有办法创建一个扩展 Game 的组合类型?例如 | InGameTitle of Title * Ingame,即包含 Title * Player * Score * Turn 的元组。 - Chris Tarn
@Chris:这只引出了一个问题:为什么你想这么做呢? - ildjarn
@ildjarn 你说得很对。当我写下那个评论时,我并没有完全理解DUs的目的。MisterMetaphor的回答帮助我理解为什么像Robert那样使用它,但不是我所概述的那样使用。 - Chris Tarn

12

使用记录(在函数式编程理论中称为产品类型)来描述由多个属性描述的复杂数据,例如数据库记录或某些模型实体:

type User = { Username : string; IsActive : bool }

type Body = { 
    Position : Vector2<double<m>>
    Mass : double<kg>
    Velocity : Vector2<double<m/s>> 
}

使用区分联合类型(称为总和类型)来枚举可能的数据值。例如:

type NatNumber =
| One
| Two
| Three
...

type UserStatus =
| Inactive
| Active
| Disabled

type OperationResult<'T> =
| Success of 'T
| Failure of string

请注意,区分联合类型的可能值也是互斥的--一个操作的结果可以是SuccessFailure,但不能同时出现两者。
您可以使用记录类型来编码操作的结果,例如:
type OperationResult<'T> = { 
    HasSucceeded : bool
    ResultValue : 'T
    ErrorMessage : string
}

但是,如果操作失败,它的ResultValue没有意义。因此,在此类型的辨别联合版本上进行模式匹配将如下所示:

match result with
| Success resultValue -> ...
| Failure errorMessage -> ...

如果你对操作类型的记录类型版本进行模式匹配,这样做就没有太多意义:

match result with
| { HasSucceeded = true; ResultValue = resultValue; ErrorMessage = _ } -> ...
| { HasSucceeded = false; ErrorMessage = errorMessage; ResultValue = _ } -> ...

看起来冗长笨重,可能效率也不高。我认为当你有这样的感觉时,这可能是提示你使用了错误的工具来完成任务。


谢谢您的回复。我现在明白了DU何时特别有意义。 - Chris Tarn

8

如果您来自C#,您可以将记录理解为具有添加值的密封类:

  • 默认情况下不可变
  • 默认情况下具有结构相等性
  • 易于进行模式匹配
  • 等等。

辨别联合编码替代方案,例如:

type Expr =
    | Num of int
    | Var of int 
    | Add of Expr * Expr 
    | Sub of Expr * Expr

上面的DU读法如下:一个表达式 要么 是一个整数,要么 是一个变量,要么 是两个表达式相加,要么 是两个表达式相减。这些情况不能同时发生。

构建记录时需要所有字段。您也可以在记录内部使用DU,反之亦然。

type Name =
    { FirstName : string;
      MiddleName : string option;
      LastName : string }

上面的例子表明中间名是可选的。

在F#中,通常使用元组或记录来建模数据。当需要高级功能时,可以将它们移动到类中。

另一方面,辨别联合被用于建模选择和案例之间的互斥关系。


谢谢。这个答案和另一个答案都指出 DU 是一种 OR 关系。但是我理解,单个 DU 可以保存多个值。例如,type Name 可以有 FirstNameMiddleNameLastName 的值。这仍然让我有点不确定一个具有所有字段值的记录和一个具有所有字段值的 DU 之间的区别是什么。是 DU 可以进行某种推断或操作,而记录不能吗?还是不可变属性在这里的区别? - Chris Tarn

3

理解DU(Discriminated Union)的一种(略有缺陷)方法是将其视为高级C#“union”,而记录更像是一个普通对象(具有多个独立字段)。

另一种理解DU的方法是将其视为两级类层次结构,其中顶级DU类型是抽象基类,DU的case是子类。尽管编译器隐藏了这个细节,但这种观点实际上接近于.NET的实际实现。


一个重要的区别与OO继承层次结构是DU的不同情况仅仅是标签,而不是不同的(子)类型。这有时会让新手感到困惑。 - Frank

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