我知道当我看到这个答案时我会笑,但出于某种原因我看不到它。
出于某种原因,我不知道如何在一个参数中传递多个func(因为缺少更好的单词。)
例如,假设我有IDoSomething有3种方法:
1.) DoIt()
2.) DoItMore()
3.) DoItMost()
在OO中,我会这样做:
type MyController(something:IDoSomething) =
let a = something.DoIt()
let b = something.DoItMore()
let c = something.DoItMost()
因此,对于F#,我将拥有一个具有上述3个功能的模块。但是我怎么把它传递给我的控制器呢?我是否必须将每个作为单独的函数传递?我觉得我想通过整个模块嘿嘿: - )
答案 0 :(得分:89)
这个问题似乎一次又一次地出现,不知怎的,接受的答案常常被证明是一个功能记录'。这样做没有合理的动机。 记录用于数据。它们具有结构上的平等性,通过将函数放入其中而完全被破坏,因为函数 don&#t; 具有结构上的平等。
那么,F#中接口的替代方法是什么?
好吧,如果你绝对必须将功能组合在一起,F#可以让你定义interfaces。是的:接口:
type IDoSomething =
abstract DoIt : unit -> unit
abstract DoItMore : unit -> unit
abstract DoItMost : unit -> unit
这种语言功能存在,所以如果你需要一个界面,没有理由想出一些奇怪的替代品。
是的,它不是功能性的,但也没有创建功能记录。
问题在于是否存在将相关功能组合在一起的单一,无处不在的功能方式。 Haskell有类型,Clojure有协议(对我来说,看起来有点类型类,但是,我不是Clojure专家)。 / p>
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])]
所以,事实证明它毕竟是有用的!
在这个咆哮的开头,我写道:"如果你绝对必须将功能组合在一起"。我写 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(在免费的monad上)
答案 1 :(得分:5)
我在my other answer写的大部分内容,我仍然认为F#是正确和惯用的。然而,后来我了解到,在F#这样的多范式语言中使用函数或部分应用程序虽然精细,可读,易懂,简单且惯用,但实际上是not strictly functional。
简而言之,问题在于依赖关系往往是不纯的,如果你注入'它们进入客户端代码,然后客户端代码也变得不纯净,因为纯代码不能调用不纯的代码。
在Haskell中,你有several options for addressing this problem,但并非所有这些都能很好地转换为F#。其中一个替代方案至少在某种程度上可以翻译:免费monad。
我不希望这个答案的出现方式是免费monad是F#界面的惯用替代品,但为了完整起见,我也添加了这个答案:
使用F# free monad recipe,OP界面变为:
type DoSomethingInstruction<'a> =
| DoIt of 'a
| DoItMore of 'a
| DoItMost of 'a
let private mapI f = function
| DoIt next -> DoIt (next |> f)
| DoItMore next -> DoItMore (next |> f)
| DoItMost next -> DoItMost (next |> f)
type DoSomethingProgram<'a> =
| Free of DoSomethingInstruction<DoSomethingProgram<'a>>
| Pure of 'a
let rec bind f = function
| Free x -> x |> mapI (bind f) |> Free
| Pure x -> f x
let doIt = Free (DoIt (Pure ()))
let doItMore = Free (DoItMore (Pure ()))
let doItMost = Free (DoItMost (Pure ()))
type DoSomethingBuilder () =
member this.Bind (x, f) = bind f x
member this.Return x = Pure x
member this.ReturnFrom x = x
member this.Zero () = Pure ()
let doDomething = DoSomethingBuilder ()
您可以使用doSomething
计算表达式编写一个小样本程序:
let p = doDomething {
do! doIt
do! doItMore
do! doItMost }
此外,您可以实施&#39; &#39;界面&#39;写一个口译员:
let rec interpret = function
| Pure x -> x
| Free (DoIt next) -> printfn "Doing it."; next |> interpret
| Free (DoItMore next) -> printfn "Doing it more!"; next |> interpret
| Free (DoItMost next) -> printfn "DOING IT MOST!"; next |> interpret
您现在可以运行该程序:
> interpret p;;
Doing it.
Doing it more!
DOING IT MOST!
val it : unit = ()
这显然需要更多的样板代码,但除了解释器之外的所有代码都是纯粹的。
答案 2 :(得分:4)
使用记录类型:
type MyFunctions = {
DoIt: (unit -> unit);
DoItMore: (unit -> unit);
DoItMost: (unit -> unit);
}
然后你可以
type MyController(functions: MyFunctions) =
let a = functions.DoIt()
let b = functions.DoItMore()
let c = functions.DoItMost()
您可以通过引用模块的函数来实例化记录类型:
module MyModule =
let doIt() =
Console.WriteLine("Do It!")
let doItMore() =
Console.WriteLine("Do It More!")
let doItMost() =
Console.WriteLine("Do It Most!")
let myRecord = {
DoIt = doIt;
DoItMore = doItMore;
DoItMost = doItMost;
}
let controller =
MyController(myRecord)
或者你可能只想
type MyController(doIt: (unit -> unit), doItMore: (unit -> unit), doItMost: (unit -> unit))
// ... Etc
旁注:F#中的很多unit
通常表示设计不良。
修改:Mark Seemann&#39; answer是此问题的正确答案。
答案 3 :(得分:2)
从技术上讲,至少有五种不同的方式来传递多个功能:
如果将多个功能结合在一起有意义取决于您的具体问题域,以及您对每种方法的某些优点和缺点进行权衡的程度......更不用说惯例,文化,风格和品味。所谓的最佳实践永远不会普遍有效;他们只是提示。实用主义是最好的最佳实践。
以下是 Expert F#3.0 所说的内容(第579页):“建议:使用对象接口类型而不是元组或函数记录。 在第5章中,您看到了各种显式表示操作字典的方法,例如使用函数元组或函数记录。通常,我们建议您为此目的使用对象接口类型,因为与实现它们相关的语法通常更方便。“