【问题标题】:F# how to pass equivalent of interfaceF#如何传递等效的接口
【发布时间】:2016-03-04 20:57:52
【问题描述】:

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

由于某种原因,我无法在一个参数中传递多个函数(因为缺少更好的词。)

例如,假设我有 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 个功能的模块。但是我如何将它传递给我的控制器?我是否必须将每个作为单独的函数传递?我有点想通过整个模块嘿嘿:-)

【问题讨论】:

  • 传递3个函数有什么问题?
  • 或由3个函数组成的记录
  • @MarcinJuraszek - 感觉就像是随着需求的变化不断地添加更多函数作为参数。我的 OO 思想说传递一个对象,以便在不更改现有合同/签名的情况下添加其他属性(或功能)。
  • @schmoopy 你觉得“蠕动”是正确和有意的。在某些时候,添加另一个参数可能会让人感觉非常错误,以至于会导致您质疑设计并可能对其进行重构。这正是随着您对业务问题的了解增加而应该发生的事情。

标签: f#


【解决方案1】:

我在my other answer 中写的大部分内容,我仍然认为对于 F# 来说是恰当和惯用的。然而,后来我了解到,在像 F# 这样的多范式语言中,函数或部分应用程序的使用虽然很好、可读、易于理解、简单和惯用,但实际上是 not strictly functional

简而言之,问题在于依赖项往往是不纯的,如果你将它们“注入”到客户端代码中,那么客户端代码也会变得不纯,因为纯代码不能调用不纯代码。

在 Haskell 中,您有 several options for addressing this problem,但并非所有这些都可以很好地转换为 F#。这些替代方案之一至少在某种程度上确实可以翻译:免费单子。

我不希望这个答案以免费 monads 是 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 }

此外,您可以通过编写解释器来“实现”“接口”:

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】:

    这个问题似乎一次又一次地出现,不知何故,公认的答案往往是“函数记录”。这样做没有合理的动机。 记录用于数据。它们具有结构上的平等性,但通过将函数放入其中会完全破坏,因为函数具有结构上的平等性。

    接口

    那么,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 -&gt; 'b) -&gt; ('b list -&gt; 'c list) -&gt; ('c -&gt; 'd) -&gt; ('a list -&gt; ('d * 'c list) list)。我不知道它的作用(我只是编造的),但重点是foobarbaz函数。例如,foo'a -&gt; '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 -&gt; (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(关于免费单子)

    【讨论】:

    • 完全让我大吃一惊。作为记录(没有双关语),我的背景主要是 OOP,我只是最近才开始与 F# 一起学习 FP,没有学术背景。完全 +1。
    • 如果您正在寻找更多关于 F# 和其他主题的清晰程度,Mark's courses 会让您很高兴
    • @TomasJansson 基于“领先”函数式语言的惯用语。在 Haskell 中使用类型类;在 Clojure 使用协议中;在 F# 中使用接口。不过,我必须承认我不知道它是如何在 Erlang 或 Scala 中完成的。一般来说,在大多数 FP 语言中,将函数分组在一起感觉有点异味。为什么要分组功能?那不是耦合的味道吗?
    • 这不是一个大问题,你为什么不认为这是一个惯用的方法?只要记录中的功能是纯的,我仍然认为这是一种功能惯用的方法。不是故意让它听起来很重要,你的回答很棒!
    • 我不明白这个答案如何避免我对一袋功能的感知需求。在价格时间序列上运行期权头寸的递归模拟。模拟需要以下选项函数:value、delta、vega、vanna,也许是 gamma。这些功能来自一个家族:Black 家族、Bachelier 家族,也许还有 Kirk 家族等。在某些情况下,选择完全由用户自行决定。显然发送 BlacksFamily 比发送 5 个函数参数要好得多。您如何以一种不错的方式使 BlacksFamily 成为一个功能?组成不相关。
    【解决方案3】:

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

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

    是否以及何时将多个功能耦合在一起是否有意义取决于您的具体问题领域,以及您对每种方法的某些优缺点的权衡程度……更不用说惯例、文化、风格和品味了。所谓的最佳实践从不普遍有效;它们只是提示。实用主义是最佳实践。

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

    【讨论】:

      【解决方案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 Seemannanswer是这个问题的正确答案。

      【讨论】:

      • 哈,我从来没有想过将我的方法包装在一个类型中......同意 unit() 这就是我问的原因,感觉不对。甚至类型也似乎..有点奇怪,这与创建类本质上不一样吗?我的意思是,对 CustomerDomain 的建议是什么?它是一个包含一堆函数的模块还是应该是一个包含函数的类型?
      • @schmoopy 域对象由数据 + 功能组成。此外,F# module 在概念上等同于 C# 中的 static class,这意味着您不能实例化它。 IMO,将模块用作域对象是没有意义的,我宁愿创建一个具有相关属性和函数的常规类型。你能承受的不变性越多越好,尽管有时 100% 不变性比它的价值更麻烦(例如,如果你使用序列化或 ORM,你就不能使用不可变类型)
      • 谢谢——这就是我的想法,但不确定是不是大脑的 OO 部分在说它:-)
      • 我不会太快放弃你的答案。我上面的评论将被埋在评级较低的 cmets 中。但有时你确实需要一袋函数,而袋子里会装满不同的家庭选择(比如每个期权估值模型附带的希腊函数)。也许我错过了它,但我不知道如何使它成为函数的函数。这不是函数组合的问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-12-14
      • 2014-01-25
      • 2014-12-16
      • 2014-02-28
      • 2013-02-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多