模式匹配是惯用的类型还是不良的设计?


18

似乎F#代码经常针对类型进行模式匹配。当然

match opt with 
| Some val -> Something(val) 
| None -> Different()

似乎很常见。

但是从OOP的角度来看,这看起来非常像基于运行时类型检查的控制流,通常会对此皱眉。要说明,在OOP中,您可能更喜欢使用重载:

type T = 
    abstract member Route : unit -> unit

type Foo() = 
    interface T with
        member this.Route() = printfn "Go left"

type Bar() = 
    interface T with
        member this.Route() = printfn "Go right"

这肯定是更多代码。OTOH,在我的OOP-y看来,它具有结构上的优势:

  • 扩展到新形式T很容易;
  • 我不必担心会发现路径选择控制流程的重复。和
  • 从某种意义上说,路线选择是一成不变的,我一经Foo掌握就不必担心Bar.Route()的实现

模式匹配与我没有看到的类型有优势吗?它是惯用语言还是不常用的功能?


4
从OOP角度看功能语言有多大意义?无论如何,模式匹配的真正力量在于嵌套模式。仅检查最外层的构造函数是可能的,但绝不是全部。
Ingo 2014年

But from an OOP perspective, that looks an awful lot like control-flow based on a runtime type check, which would typically be frowned on.听起来太教条了。有时,您想将自己的操作与层次结构分开:也许1)您不能将操作添加到不属于该层次结构的b / c中;2)您想要拥有的操作的类与您的层次结构不匹配;3)您可以将op添加到您的层次结构中,但不希望您不想因为大多数客户不使用的一堆废话而混乱您的层次结构的API。

4
只是为了澄清,SomeNone不是类型。它们都是类型为forall a. a -> option a和的构造函数forall a. option a(对不起,不确定F#中类型注释的语法是什么)。

Answers:


21

您是正确的,因为OOP类层次结构与F#中的已区分联合非常相关,并且模式匹配与动态类型测试非常相关。实际上,这实际上是F#将有区别的联合编译到.NET的方式!

关于可扩展性,问题有两个方面:

  • OO使您可以添加新的子类,但很难添加新的(虚拟)功能
  • FP使您可以添加新功能,但很难添加新的联合用例

就是说,当您错过模式匹配中的个案时,F#会警告您,因此添加新的联合个案实际上并不是那么糟糕。

关于在根选择中查找重复项-当匹配重复时,F#将向您发出警告,例如:

match x with
| Some foo -> printfn "first"
| Some foo -> printfn "second" // Warning on this line as it cannot be matched
| None -> printfn "third"

“路线选择是不变的”这一事实也可能会引起问题。例如,如果您想在FooBar案例之间共享函数的实现,但又为Zoo案例做其他事情,则可以使用模式匹配轻松地对其进行编码:

match x with
| Foo y | Bar y -> y * 20
| Zoo y -> y * 30

通常,FP更加专注于首先设计类型,然后添加功能。因此,您可以在一个文件中的几行中放入类型(域模型),然后轻松添加在域模型上运行的函数,这确实使您受益。

两种方法-OO和FP是相当互补的,并且都有优点和缺点。棘手的事情(从OO角度来看)是F#通常使用FP样式作为默认样式。但是,如果确实确实需要添加新的子类,则可以随时使用接口。但是在大多数系统中,您同样需要添加类型和函数,因此选择实际上并没有太大关系-并且在F#中使用区分的联合会更好。

我会推荐这个很棒的博客系列以获取更多信息。


3
尽管您是对的,但我想补充一点,这不是OO与FP的问题,而是对象与求和类型的问题。除了OOP对它们的痴迷之外,没有什么东西可以使它们无法运行。而且,如果您经历了足够多的麻烦,您也可以使用主流的OOP语言来实现求和类型(尽管效果并不理想)。
Doval 2014年

1
“而且,如果您经历了足够多的麻烦,您也可以使用主流的OOP语言来实现求和类型(尽管效果并不理想)。” ->我想您最终会得到类似于.NET的类型系统中F#sum类型的编码方式的方法:)
Tarmil 2014年

8

您已经正确地观察到模式匹配(本质上是增强的switch语句)和动态调度具有相似之处。它们还以某些语言共存,效果非常令人满意。但是,有细微的差别。

我可能会使用类型系统来定义只能具有固定数量的子类型的类型:

// pseudocode
data Bool = False | True
data Option a = None | Some item:a
data Tree a = Leaf item:a | Node (left:Tree a) (right:Tree a)

有将永远是另一个亚型Bool或者Option,这样子类并不显得很有用(比如Scala一些语言具有继承,可以处理这个的概念-一个类可以被标记为当前编译单元的“最终”之外,但亚型可以在此编译单元中定义)。

因为Option现在静态地知道 like之类的子类型,所以如果我们忘记处理模式匹配中的大小写,则编译器可以发出警告。这意味着模式匹配更像是一种特殊的垂头丧气,它迫使我们处理所有选项。

此外,动态方法分派(OOP必需)也意味着运行时类型检查,但类型不同。因此,是否通过模式匹配显式地进行类型检查或通过方法调用来隐式进行类型检查是无关紧要的。


“这意味着模式匹配更像是迫使我们处理所有选项的特殊下调”-实际上,我相信(只要您仅针对构造函数而不是值或嵌套结构进行匹配),将抽象的虚方法放在超类中。
Jules

2

F#模式匹配通常是通过有区别的联合而不是使用类来完成的(因此从技术上讲根本不是类型检查)。当您不了解模式匹配的情况时,这可使编译器向您发出警告。

要注意的另一件事是,在一种功能样式中,您是按功能而不是按数据来组织事物,因此模式匹配使您可以将不同的功能收集到一个地方,而不是分散在各个类中。这还具有一个优势,您可以在需要进行更改的位置旁边看到如何处理其他案件。

添加新选项如下所示:

  1. 为您的受歧视工会添加新选项
  2. 修复所有不完整的模式匹配警告

2

部分地,您在函数式编程中会更频繁地看到它,因为您使用类型来更频繁地进行决策。我意识到您可能只是或多或少地随机选择了示例,但是等同于您的模式匹配示例的OOP看起来通常更像:

if (opt != null)
    opt.Something()
else
    Different()

换句话说,使用多态性来避免诸如OOP中的空检查之类的常规事情相对罕见。就像OO程序员不会创建空对象一样在每种情况下都,函数式程序员也不总是会重载函数,尤其是当您知道模式列表一定会很详尽时。如果您在更多情况下使用类型系统,将会发现它以您不习惯的方式使用。

相反,等同于您的OOP示例的惯用函数式编程很可能不会使用模式匹配,但是会使用fooRoutebarRoute函数,这些函数将作为参数传递给调用代码。如果有人在那种情况下使用模式匹配,通常会认为它是错误的,就像在OOP中将某些打开类型的人视为错误的一样。

那么什么时候模式匹配才被认为是好的功能编程代码?当您要做的不仅仅是查看类型,并且扩展需求时,不需要添加更多的案例。例如,Some val不仅检查opt类型是否正确Some,它还绑定val到基础类型,以便在的另一侧进行类型安全使用->。您知道您很可能永远不需要第三种情况,因此这是一个很好的用途。

模式匹配在表面上可能类似于面向对象的switch语句,但是还有很多事情要做,尤其是对于更长或嵌套的模式。在声明它等同于一些设计欠佳的OOP代码之前,请确保考虑到它所做的所有事情。通常,它正在简洁地处理无法在继承层次结构中清晰表示的情况。


我知道您知道这一点,并且在编写答案时可能会漏意,但是请注意,Some并且None不是类型,因此您无法对类型进行模式匹配。您可以对相同类型的构造函数进行模式匹配。这不像询问“ instanceof”。
安德烈斯·F
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.