这个问题似乎一次又一次地出现,不知何故,公认的答案往往是“函数记录”。这样做没有合理的动机。 记录用于数据。它们具有结构上的平等性,但通过将函数放入其中会完全破坏,因为函数不具有结构上的平等性。
接口
那么,F# 中接口的替代方案是什么?
好吧,如果您绝对必须将函数组合在一起,F# 允许您定义interfaces。是:接口:
type IDoSomething =
abstract DoIt : unit -> unit
abstract DoItMore : unit -> unit
abstract DoItMost : unit -> unit
这个语言特性是存在的,所以如果你需要一个界面,没有理由想出一些奇怪的替代品。
但是,它不是功能性的
对,它不是函数式的,但也不是创建函数记录。
问题是是否有一种单一的、无处不在的函数式方法可以将相关函数组合在一起。 Haskell 有类型类,Clojure 有协议(在我看来,这有点像类型类,但我几乎不是 Clojure 专家)。
F# 既没有类型类也没有协议;同样,您在该语言中获得的最接近的是接口。
功能
综上所述,函数式编程的基本构建块是:函数。有时,函数由其他函数组成,或者返回其他函数。我们称这些为higher-order functions。
惯用的函数式代码通常通过高阶函数表示。 传递其他函数,而不是将接口传递给函数:
let run foo bar baz = List.map foo >> bar >> List.groupBy baz
由于类型推断,即使是上面这种无意义的示例也可以编译。它的类型为('a -> 'b) -> ('b list -> 'c list) -> ('c -> 'd) -> ('a list -> ('d * 'c list) list)。我不知道它的作用(我只是编造的),但重点是foo、bar 和baz 是函数。例如,foo 是 'a -> 'b 类型的函数。
即使有run这样荒谬的功能,你也可以应用它,它可能真的有意义:
type Parity = Even | Odd
let parity i =
match i % 2 with
| 0 -> Even
| _ -> Odd
open System
let tryParse s =
match Int32.TryParse s with
| true, i -> Some i
| _ -> None
let runP = run tryParse (List.choose id) parity
runP 函数的类型为 string list -> (Parity * int list) list。它有什么作用?它需要一个字符串列表,丢弃那些不是整数的,并按奇偶校验(偶数/奇数)对它们进行分组:
> runP ["Foo"; "1"; "42"; "Bar"; "Baz"; "1337"];;
val it : (Parity * int list) list = [(Odd, [1; 1337]); (Even, [42])]
所以,它毕竟是(某种)有用的!
从 OOP 到 FP
在这篇咆哮的开头,我写道:“如果你绝对必须将函数组合在一起”。我写 if 是有原因的。即使在OOD 中,从Interface Segregation Principle,我们知道我们不应该强迫客户端依赖它不需要的功能。将一组函数传递给客户端很容易违反该原则。接口定义的成员越多,违规的风险就越大。
除此之外,从Dependency Inversion Principle 得出“客户 [...] 拥有抽象接口”(APPP,第 11 章)。换句话说,客户端声明它需要什么,接口必须符合它;定义接口的不是实现。
一旦您开始关注这些以及SOLID principles 的其余部分,您应该开始意识到您定义的接口越精细越好。 The logical conclusion is to define all interfaces with only a single method。如果客户端需要多个成员,则始终可以将两个接口作为两个参数传递,但如果接口已定义,则永远不能从接口中删除成员。
简而言之,这就是可扩展性:您可以扩展,但不能减少。
在 OOD 中,理想情况下,接口应该只定义一个成员,但在函数式编程中,我们有一个更自然的多态候选者:函数。
因此,将函数作为参数传递。这是实现它的函数式方法。
编辑:另见my other answer(关于免费单子)