什么是 F# 中的可区分联合以及我们在 OOP 中有什么类型的替代方案

What is Discriminated Union in F# and what type of alternative we have in OOP

我正在从 C# 开始学习函数式编程。 由于我对 C# 的深入了解,当然,我选择了 F# 作为我的第一门函数式语言,并尝试投入时间来学习它。

现在我需要了解什么是 受歧视的工会 以及为什么它很重要以及为什么我们真正需要它?!

我确实做了很多研究

但是导师、讲师、文章和博客帖子的问题在于,人们实际上是在试图 describe/teach 我们 Discriminated Unions 有很多函数式编程术语对我们来说当然是非常不可理解的,人家全是OOP,只会一点LINQ,表达式,高阶函数。

我是函数式世界的新手,我的大脑充满了面向对象编程的思维模式,所以很难从那个角度理解这个概念。

如果你真的google它,你会得到这样的回应:

Discriminated Unions # You can combine singleton types, union types, type guards, and type aliases to build an advanced pattern called discriminated unions, also known as tagged unions or algebraic data types. Discriminated unions are useful in functional programming.

在我看来这真的没有任何意义。 所以请以人性化和正常的方式告诉我什么是歧视联盟,我们为什么需要它?什么可以与 OOP 世界相提并论? (因为它真的对我有帮助)

谢谢。

OOP 世界并没有严格的 DU 模拟(这就是它经常发音的原因 "deficient"),但最接近的是两级继承层次结构。

考虑以下 DU:

type Shape = Circle of radius:Float | Rectangle of width:Float * height:Float

这种类型的语义(即"meaning")可以这样含糊地表达:形状有两种形式 - Circle,有半径,Rectangle,有宽度和高度,没有其他形状

大致 等同于以下继承层次结构:

abstract class Shape {}

class Circle : Shape { 
    public double Radius { get; set; }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
}

此 C# 代码段还含糊地表达了“形状有两种形式 - CircleRectangle”的想法,但存在一些重要区别:

  1. 以后(或者其他库),可能会出现更多的形状。其他人可能只是声明一个继承自 Shape 的新 class - 然后就可以了。 F# 歧视联合不允许这样做。

  2. CircleRectangle 是它们自己的类型。这意味着可以声明一个采用 Circle 而不是 Rectangle 的方法。 F# 歧视联合不允许这样做。在 F# 中,Shape 是一种类型,但 CircleRectangle 不是类型。不能有 Circle.

  3. 类型的变量、参数或属性
  4. F# 提供了许多用于处理 DU 的简化语法结构,在 C# 中必须编写得非常冗长,并且有很多噪音。


第(1)点和第(2)点表面上看起来是局限性(确实,我在这两种情况下都使用了"do not allow"),但这实际上是一个特征。这个想法是一些限制(不是全部)导致更正确、更稳定的程序。回顾过去:"goto considered harmful",引用取代了指针,垃圾收集取代了手动内存管理——所有这些都带走了一些灵活性,有时甚至是性能,但通过大大提高的代码可靠性弥补了这一点。

这与 F# DU 相同:除了 CircleRectangle 之外可能没有其他类型的形状这一事实允许编译器检查使用 Shape - 即验证是否已处理所有可能的情况。如果您稍后决定添加第三种形状,编译器将帮助找到所有需要处理这种新情况的地方。


第三点讲到"making the right thing easy and the wrong thing hard"的思路。为此,F# 提供了一些有用的语法(例如模式匹配)和一些有用的默认值(例如不变性、结构比较),在 C# 中必须手动编码并严格执行。

有区别的联合有点像 OOP 中的 class 层次结构。 OOP 中的 classic 示例有点像动物,可以是狗或猫。在 OOP 中,您可以将其表示为具有一些抽象方法(例如 MakeAnimalNoise)的基础 class 和用于狗和猫的具体子 class。

在函数式编程中,匹配的东西是一个可区分的联合Animal,有两种情况:

type Animal =  
  | Dog of breed:string
  | Cat of fluffynessLevel:int

在 OOP 中,您有虚拟方法。在 FP 中,您使用模式匹配将操作编写为函数:

let makeAnimalNoise animal = 
  match animal with
  | Dog("Chihuahua") -> "woof sqeek sqeek woof"
  | Dog(other) -> "WOOF"
  | Cat(fluffyness) when fluffyness > 10 -> "MEEEOOOOW"
  | Cat(other) -> "meow"

FP 和 OOP 方法之间有一个重要区别:

  • 使用抽象class,您可以轻松添加新案例,但添加新操作需要修改所有现有classes。
  • 有了discriminated union,你可以很容易地添加一个新的操作,但是添加一个新的case需要修改所有现有的函数。

如果您有 OOP 背景,这可能看起来很奇怪。在 OOP 中讨论 classes 时,每个人都强调可扩展性的需要(通过添加新的 classes)。实际上,我认为你两者都需要——所以你选择哪个方向并不重要。 FP 有它的好处,就像 OOP 有(有时)一样。

当然,这是一个完全没有用的例子。有关这在实践中如何有用的更现实的讨论,请参阅 Scott Wlaschin 的精彩文章 Designing with Types series

虽然其他答案大多涵盖了该主题,但我认为值得添加 "traditionally" 在 OO 语言中实现的方式是使用访问者模式。 Mark Seeman 在 his blog.

上很好地解释了两者之间的同构