F#如何传递相当于的接口

时间:2015-12-01 03:06:30

标签: f#

我知道当我看到这个答案时我会笑,但出于某种原因我看不到它。

出于某种原因,我不知道如何在一个参数中传递多个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个功能的模块。但是我怎么把它传递给我的控制器呢?我是否必须将每个作为单独的函数传递?我觉得我想通过整个模块嘿嘿: - )

4 个答案:

答案 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)。我不知道它做了什么(我刚刚做了),但重点是foobarbaz函数。例如,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(在免费的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)

从技术上讲,至少有五种不同的方式来传递多个功能:

  • 作为参数,curried
  • 作为参数,tupled
  • 作为记录类型实例的字段值
  • 作为具体或抽象类类型实例的字段值(val ...)或属性(具体或抽象成员...)
  • 作为接口类型实例的属性(抽象成员)

如果将多个功能结合在一起有意义取决于您的具体问题域,以及您对每种方法的某些优点和缺点进行权衡的程度......更不用说惯例,文化,风格和品味。所谓的最佳实践永远不会普遍有效;他们只是提示。实用主义是最好的最佳实践。

以下是 Expert F#3.0 所说的内容(第579页):“建议:使用对象接口类型而不是元组或函数记录。 在第5章中,您看到了各种显式表示操作字典的方法,例如使用函数元组或函数记录。通常,我们建议您为此目的使用对象接口类型,因为与实现它们相关的语法通常更方便。“