[英]C# to F#: Functional thinking vs. polymorphism
假设我有两个班:
public class Triangle {
public float Base { get; set; }
public float Height { get; set; }
public float CalcArea() { return Base * Height / 2.0; }
}
public class Cylinder {
public float Radius { get; set; }
public float Height { get; set; }
public float CalcVolume() { return Radius * Radius * Math.PI * Height }
}
我们在这里有两个几何形状的描述以及两者中的操作。
这是我在F#中的尝试:
type Triangle = { Base: float; Height: float }
module TriangleStuff =
let CalcArea t =
t.Base * t.Height / 2.0
type Cylinder = { Radius: float; Height: float }
module CylinderStuff =
let CalcVolume c =
c.Radius * c.Radius * Math.PI * c.Height
假设我对这两个类进行了观察(它们都有Height
!)并且我想提取一个对任何具有高度属性有意义的操作。 所以在C#中我可能会拉出一个基类并在那里定义操作,如下所示:
public abstract class ShapeWithHeight {
public float Height { get; set; }
public virtual bool CanSuperManJumpOver() {
return Height == TALL; // Superman can *only* jump over tall buildings
}
public const float TALL = float.MaxValue;
}
public class Triangle : ShapeWithHeight {
public float Base { get; set; }
public float CalcArea() { return Base * Height / 2.0; }
public override bool CanSuperManJumpOver() {
throw new InvalidOperationException("Superman can only jump over 3-d objects");
}
}
public class Cylinder : ShapeWithHeight {
public float Radius { get; set; }
public float CalcVolume() { return Radius * Radius * Math.PI * Height }
}
请注意各个子类如何对此操作的实现有自己的想法。
靠近点,我可能有一个函数的地方,可以接受一个三角形或气缸:
public class Superman {
public void JumpOver(ShapeWithHeight shape) {
try {
if (shape.CanSuperManJumpOver()) { Jump (shape); }
} catch {
// ...
}
}
}
..并且此功能可以接受三角形或圆柱形。
我在向F#应用同样的思路时遇到了麻烦。
我一直在阅读有关函数式语言的文章。 传统的思想是,最好表达代数值类型而不是继承类。 我们认为最好是用较小的构建块来构建或构建更丰富的类型,而不是从抽象类开始并从那里缩小。
在F#中,我希望能够定义一个函数,该函数接受一个已知具有Height属性的参数,并以某种方式使用它(即CanSuperManJumpOver
的基本版本)。 我应该如何在功能世界中构建这些类型来实现它? 我的问题在功能性世界中是否有意义? 对这种想法的任何评论都是最受欢迎的。
在我看来,你所描述的C#设计根本就是错误的 - 你用虚拟方法CanSuperManJumpOver
定义了一个ShapeWithHeight
类型,但是这个方法不能用于其中一个具体实例(2D三角形),你必须抛出异常。
在F#中对域进行建模时的一个关键原则是无效状态不应该是可表示的 ( 有关更多信息,请参阅此文章 )。 你的设计打破了这个 - 因为你可以构造一个三角形并在其上调用一个操作,但操作无效。
因此,首先要考虑的是,您尝试建模的域实际上是什么? (这有点难以从你的例子中猜出,但让我试试......)假设你有一些物体,超人可以跳过3D形状,但不能跳过2D形状。 您可以使用区分联合来区分这两种形状:
type Height = float
type Shape2DInfo =
| Triangle of float * float
type Shape3DInfo =
| Cylinder of float
type Shape =
| Shape2D of Shape2DInfo
| Shape3D of Height * Shape3DInfo
诀窍在于,对于所有3D形状,我们现在可以在Shape3D
情况下直接获得高度 - 因此您可以始终获得3D形状的高度(无论它是哪种特定形状 - 这里,只有圆柱体)。 对于2D形状,我没有包括高度,因为它们可能,或者可能没有...
然后你可以编写一个跳跃函数,模式匹配一个形状并处理三种不同的情况 - 形状不可跳跃,形状太小或形状足够高:
let jumpOver shape =
match shape with
| Shape2D _ -> printfn "Cannot jump!"
| Shape3D(height, _) ->
if height = Double.MaxValue then printf "Jumped!"
else printfn "Too boring!"
总结一下 - 如果你有一个在C#中有一些属性的抽象类,F#中最接近的东西(如果你想使用功能设计,而不是OO设计)是使用一个存储公共属性( Shape
)并包含值的类型另一种类型( Shape3DInfo
),指定每个子类的特定细节。
在函数式编程范例中,您将从函数开始,并从中计算出类型。 所以起点就是功能
let CanSupermanJumpOver height =
height = TALL
当你有一个需要交替应用CanSupermanJumpOver函数的问题时,你才会开始考虑三角形和圆柱体之间的多态性。 基本上,OO范例专注于隐藏正在使用的内部数据结构。 功能范例侧重于封装复杂的流程逻辑,但数据结构是透明的。 诀窍在于尝试将这两种方法结合起来,而不会损害其中任何一种方法 这是我最后一直在苦苦挣扎的事情。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.