我开始用精彩的网站https://fsharpforfunandprofit.com
学习F#在阅读关于four key concepts that differentiate F# from a standard imperative language的条目中的模式匹配时,我发现了这句话(强调我的):
这些选择类型可以通过使用在C#中进行一些模拟 子类或接口,但在C#中没有内置支持 类型系统,用于这种与错误检查的详尽匹配。
这对我来说似乎很奇怪,因为我认为我们可以在C#中使用接口中的方法定义(或抽象类中的抽象方法)获得完全等效的结果:它强制执行所有继承类来实现该方法,就像在F#代码中一样,它强制使用draw方法为所有“继承”类型提供实现。
不同之处在于,在功能情况下,所有实现都采用相同的方法,而在面向对象的情况下,每个实现都封装在其类中......但从概念上讲,在两种情况下都会得到相同的强制执行,所以我在功能方法中看不到任何好处。
我错过了什么吗?有人可以为我澄清一下吗?
答案 0 :(得分:6)
关键的洞察力是有两种建模领域的方式(广义上讲)。
让我们学习课程和界面。说你宣布IShape
并制作Circle
,Rectangle
,其他所有人都会实现它。大。您将在IShape
上使用哪些方法?让我们说Draw
。到目前为止一切都很好。
现在假设你实现了十几个形状。然后,几个月后,你发现自己需要另一项手术。我们称之为IsEmpty
。你是做什么?您将IsEmpty
添加到IShape
,然后您进入每个类中并向其中添加IsEmpty
。有点麻烦,但没关系,你可以做到。
在未来几个月,您想要添加另一项操作。然后另一个。而另一个。你很快就厌倦了这一点,但那仍然很好,你咬紧牙关,但你做到了。
但接下来出现了下一个问题:正在使用您的图书馆的其他人想要添加他们自己的操作。他们在做什么?他们无法修改您库中的IShape
界面。他们可以要求你这样做并重新发布图书馆(效率不高,是吗?)。或者他们可以根据if
+ is
实施操作 - 即if (shape is Circle) { ... } else if (shape is Rectangle) { ... }
,依此类推。但是他们遇到了你所链接的文章中描述的困难 - 编译器不会保护他们不会错过形状!
另一方面,采取有区别的联盟。您描述联盟,然后添加所需的操作,左右。每个操作都处理自身内部的所有情况(并且编译器会验证所有情况都已确实处理),您甚至可以在引用库的其他项目中添加新操作,而无需修改原始代码。总天堂!
然而,在接下来的几个月里,你发现你需要另一个案例 - 比如Triangle
。当然,您可以将此案例添加到类型中,但是您必须在每个操作中为其添加处理。更糟糕的是:那些使用你的图书馆的人 - 他们的代码在获得最新版本时会破坏,他们也必须修改他们的附加操作。乏味!
所以似乎有两种不同的互斥方式:
这是语言设计中众所周知的问题。众所周知,它有自己的名字 - " Expression Problem"。实际上有些语言可以让你吃蛋糕并吃掉它 - Haskell has type classes,Clojure has protocols,Rust 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
定义了四种可能的选项:Circle
,Rectangle
,Polygon
和Point
。
我无法在我的程序中的其他地方发明一个新的联合案例,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
如果我以这种方式为我的购物车建模,我会大大缩小可能无效状态的范围,因为我的购物车可能处于这三种状态中的一种而不是其他状态。
接口和类可让您为完全相同的状态建模,但它们不会阻止您创建任意数量的其他状态,这些状态完全没有意义且与您的域无关。