函数式编程与OOP的方法实现实现

时间:2017-11-10 00:35:58

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

我开始用精彩的网站https://fsharpforfunandprofit.com

学习F#

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

  

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

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

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

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

2 个答案:

答案 0 :(得分:6)

关键的洞察力是有两种建模领域的方式(广义上讲)。

让我们学习课程和界面。说你宣布IShape并制作CircleRectangle,其他所有人都会实现它。大。您将在IShape上使用哪些方法?让我们说Draw。到目前为止一切都很好。

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

在未来几个月,您想要添加另一项操作。然后另一个。而另一个。你很快就厌倦了这一点,但那仍然很好,你咬紧牙关,但你做到了。

但接下来出现了下一个问题:正在使用您的图书馆的其他人想要添加他们自己的操作。他们在做什么?他们无法修改您库中的IShape界面。他们可以要求你这样做并重新发布图书馆(效率不高,是吗?)。或者他们可以根据if + is实施操作 - 即if (shape is Circle) { ... } else if (shape is Rectangle) { ... },依此类推。但是他们遇到了你所链接的文章中描述的困难 - 编译器不会保护他们不会错过形状!

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

然而,在接下来的几个月里,你发现你需要另一个案例 - 比如Triangle。当然,您可以将此案例添加到类型中,但是您必须在每个操作中为其添加处理。更糟糕的是:那些使用你的图书馆的人 - 他们的代码在获得最新版本时会破坏,他们也必须修改他们的附加操作。乏味!

所以似乎有两种不同的互斥方式:

  1. 您可以轻松添加新案例,但很难添加新操作(又名"开放世界"型号)。
  2. 或者您可以轻松添加新操作,但很难添加新案例(又名"封闭世界"型号)。
  3. 这是语言设计中众所周知的问题。众所周知,它有自己的名字 - " Expression Problem"。实际上有些语言可以让你吃蛋糕并吃掉它 - Haskell has type classesClojure has protocolsRust has traits等等。我所见过的解决方案都不够优雅在实践中,让人们想知道解决表达问题是否值得。

    F#无法解决这个问题[1] - 您无法同时拥有这两种方式。然而,F#至少分别支持两种方式:"开放世界"的类+接口,"关闭世界"的区别联盟。另一方面,C#仅支持"开放世界"。

    更重要的是,事实证明,在真正的节目中,封闭的世界"建模比开放世界"更有用。以这种方式建模的程序变得更容易理解,越来越少,越简洁。 "开放世界"当你希望你的程序在你的程序写完之后,由你不一定知道的人 - 也就是"插件"时,模型通常会很有用。这种情况确实发生了,但不是经常发生。

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

答案 1 :(得分:4)

因此,从概念上讲,我们讨论的是两种截然不同的域建模方法。

考虑我们在文章中描述的功能方法:

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被严格定义为这些选项之一,并且在模式匹配时,编译器可以检查我没有错过一个。 / p>

如果我使用C#样式:

interface IShape {}

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

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

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

你永远不知道有多少IShape存在。

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

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

让我们从购物车的Fun and Profit网站的F#中再举一个例子:

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

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

接口和类可让您为完全相同的状态建模,但它们不会阻止您创建任意数量的其他状态,这些状态完全没有意义且与您的域无关。