【问题标题】:Method implementation enforcement in functional programming vs OOP函数式编程与 OOP 中的方法实现执行
【发布时间】:2018-04-23 03:53:24
【问题描述】:

我开始通过精彩的网站https://fsharpforfunandprofit.com 学习 F#

在阅读关于four key concepts that differentiate F# from a standard imperative language 的条目中的模式匹配时,我发现了这句话(强调我的):

这些选择类型可以在 C# 中通过使用 子类或接口,但 C# 中没有内置支持 类型系统用于这种带有错误检查的详尽匹配。

这对我来说似乎很奇怪,因为我认为我们可以通过 C# 中接口中的方法定义(或抽象类中的抽象方法)获得完全等效的结果:它强制所有继承类实现该方法,就像在 F# 代码中它强制 draw 方法为所有“继承”类型提供实现一样。

不同之处在于,在功能情况下,所有实现都在同一个方法中,而在面向对象的情况下,每个实现都封装在其类中......但从概念上讲,在这两种情况下你会得到相同的执行,所以我没有看到函数式方法有任何好处。

我错过了什么吗?有人可以帮我澄清一下吗?

【问题讨论】:

    标签: oop f# functional-programming pattern-matching


    【解决方案1】:

    关键的见解是有两种建模领域的方法(广义而言)。

    让我们采用类和接口。假设您声明IShape 并让CircleRectangle 和所有其他人实现它。伟大的。 IShape 有哪些方法?假设Draw。到目前为止一切顺利。

    现在假设您实现了十几个形状。然后,几个月后,您发现自己需要进行另一次手术。我们称之为IsEmpty。你做什么工作?您将IsEmpty 添加到IShape,然后进入十几个类中的每一个,并将IsEmpty 添加到它们。有点麻烦,不过没关系,你可以的。

    几个月后您想添加另一个操作。然后另一个。还有一个。你很快就厌倦了,但这仍然很好,你咬紧牙关,但你做到了。

    但是还有下一个问题:正在使用您的库的其他人想要添加他们自己的操作。他们在做什么?他们无法修改IShape 接口,它在您的库中。他们可以要求您这样做并重新发布库(效率不是很高,是吗?)。或者他们可以按照if + is - 即if (shape is Circle) { ... } else if (shape is Rectangle) { ... } 等方式实现操作。但随后他们遇到了您链接的文章中描述的非常困难 - 编译器不会保护他们免于丢失形状!

    另一方面,采用有区别的联合。您描述联合,然后添加您想要的所有操作,左右。每个操作都处理自身内部的所有情况(编译器会验证是否确实处理了所有情况),您甚至可以在引用您的库的其他项目中添加新操作,而无需修改原始代码。涅槃!

    但是,几个月后,您发现您需要另一个案例 - 比如Triangle。当然,您可以将这种情况添加到类型中,但是您必须在每个操作中为它添加处理。更糟糕的是:那些使用你的库的人——当他们获得最新版本时,他们的代码会中断,他们也将不得不修改他们的额外操作。乏味!

    所以看起来有两种不同的互斥方式:

    1. 您可以轻松地添加新案例,但难以添加新操作(也称为“开放世界”模型)。
    2. 或者您可以轻松添加新操作,但很难添加新案例(也称为“封闭世界”模型)。

    这是语言设计中众所周知的问题。众所周知,它有自己的名字——“Expression Problem”。实际上,有些语言也可以让你吃蛋糕 - Haskell has type classesClojure has protocolsRust has traits 等。我见过的两种解决方案在实践中都不够优雅,以至于得到想知道解决表达问题是否值得。

    F# 不能解决这个问题[1] - 你不能同时拥有这两种方式。但是,F# 至少分别支持两种方式:“开放世界”的类+接口,“封闭世界”的可区分联合。另一方面,C# 仅支持“开放世界”。

    更重要的是,事实证明,在实际程序中,“封闭世界”建模远比“开放世界”有用。以这种方式建模的程序更容易理解,错误更少,更简洁。当您希望程序在编写后由您不一定认识的人(也称为“插件”)进行扩展时,“开放世界”模型通常会派上用场。这种情况确实会发生,但并不经常发生。


    [1] 如果你不计算 statically resolved type parameters 的恶作剧,无论如何它在所有情况下都不能可靠地工作

    【讨论】:

      【解决方案2】:

      因此,从概念上讲,我们正在讨论两种完全不同的域建模方法。

      考虑我们在文章中看到的函数式方法:

      type Shape =        // define a "union" of alternative structures
          | Circle of radius:int 
          | Rectangle of height:int * width:int
          | Point of x:int * y:int 
          | Polygon of pointList:(int * int) list
      
      let draw shape =    // define a function "draw" with a shape param
        match shape with
        | Circle radius -> 
            printfn "The circle has a radius of %d" radius
        | Rectangle (height,width) -> 
            printfn "The rectangle is %d high by %d wide" height width
        | Polygon points -> 
            printfn "The polygon is made of these points %A" points
        | _ -> printfn "I don't recognize this shape"
      

      这里的关键点是Shape 定义了四个可能的选项:CircleRectanglePolygonPoint

      我不能在我的程序的其他地方发明一个新的联合案例,Shape 被严格定义为这些选项之一,当模式匹配时,编译器可以检查我没有错过任何一个。

      如果我使用 C# 样式模型:

      interface IShape {}
      
      class Circle : IShape {}
      class Rectangle : IShape {}
      class Point : IShape {}
      class Polygon : IShape {}
      

      可能的类型是无限的。在一个或多个其他文件中,如果我愿意,我可以简单地定义更多:

      class Triangle : IShape {}
      class Pentagon : IShape {}
      class Hexagon : IShape {}
      

      您永远无法知道可能存在多少IShapes。

      我们上面定义的 F# Shape 不是这样。它有四个选项,只有四个。

      区分联合模型实际上非常强大,因为通常,当我们在软件中对域进行建模时,该域中的可能状态实际上是一组相对较小且简洁的选项。

      让我们以 F# for Fun and Profit 网站的购物车为例:

      type ShoppingCart = 
          | EmptyCart
          | ActiveCart of unpaidItems : string list
          | PaidCart of paidItems : string list * payment: float 
      

      如果我以这种方式为我的购物车建模,我将大大减少可能的无效状态的范围,因为我的购物车可能处于这三种状态之一,而不会处于其他状态。

      接口和类可以让您对完全相同的状态进行建模,但它们不会阻止您创建任意数量的额外状态,这些状态完全无意义且与您的领域无关。

      【讨论】:

        猜你喜欢
        • 2020-03-27
        • 2011-04-17
        • 1970-01-01
        • 2019-11-02
        • 2015-10-03
        • 1970-01-01
        • 2019-08-03
        • 2011-10-06
        • 1970-01-01
        相关资源
        最近更新 更多