简体   繁体   English

C#到F#:功能性思维与多态性

[英]C# to F#: Functional thinking vs. polymorphism

Suppose I had two classes: 假设我有两个班:

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 }
}

We have here the descriptions of two geometric shapes along with an operation in both. 我们在这里有两个几何形状的描述以及两者中的操作。

And here's my attempt in F#: 这是我在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

Suppose I made an observation about these two classes (they both have Height !) and I wanted to extract an operation that made sense for anything that had a height property. 假设我对这两个类进行了观察(它们都有Height !)并且我想提取一个对任何具有高度属性有意义的操作。 So in C# I might pull out a base class and define the operation there, as follows: 所以在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 }
}

Note how individual subclasses might have their own ideas as to the implementation of this operation. 请注意各个子类如何对此操作的实现有自己的想法。

Closer to the point, I might have a function somewhere which can accept either a Triangle or a Cylinder: 靠近点,我可能有一个函数的地方,可以接受一个三角形或气缸:

public class Superman {
    public void JumpOver(ShapeWithHeight shape) {
        try {
            if (shape.CanSuperManJumpOver()) { Jump (shape); }
        } catch {
            // ...
        }
    }
}

.. and this function can accept either a Triangle or a Cylinder. ..并且此功能可以接受三角形或圆柱形。

I'm having trouble applying the same line of thinking to F#. 我在向F#应用同样的思路时遇到了麻烦。

I've been doing some reading about functional languages. 我一直在阅读有关函数式语言的文章。 The conventional thinking is that it is preferred to express algebraic value types rather than inherited classes. 传统的思想是,最好表达代数值类型而不是继承类。 The thinking goes that it's better to compose or build richer types out of smaller building blocks rather than starting with an abstract class and narrowing from there. 我们认为最好是用较小的构建块来构建或构建更丰富的类型,而不是从抽象类开始并从那里缩小。

In F# I want to be able to define a function which takes an argument that is known to have a Height property, and work with it somehow (ie the base version of CanSuperManJumpOver ). 在F#中,我希望能够定义一个函数,该函数接受一个已知具有Height属性的参数,并以某种方式使用它(即CanSuperManJumpOver的基本版本)。 How should I structure these types in a functional world to achieve it? 我应该如何在功能世界中构建这些类型来实现它? Does my question even make sense in a functional world? 我的问题在功能性世界中是否有意义? Any comments on the thinking is most welcome. 对这种想法的任何评论都是最受欢迎的。

In my opinion, the C# design that you are describing is fundamentally wrong - you defined a type ShapeWithHeight with a virtual method CanSuperManJumpOver but the method cannot be implemented for one of the concrete instances (2D triangle) and you have to throw an exception instead. 在我看来,你所描述的C#设计根本就是错误的 - 你用虚拟方法CanSuperManJumpOver定义了一个ShapeWithHeight类型,但是这个方法不能用于其中一个具体实例(2D三角形),你必须抛出异常。

One of the key principles when modelling a domain in F# is that invalid states should not be representable (see this great article for more ). 在F#中对域进行建模时的一个关键原则是无效状态不应该是可表示的有关更多信息,请参阅此文章 )。 Your design breaks this - because you can construct a triangle and invoke an operation on it, but the operation is invalid. 你的设计打破了这个 - 因为你可以构造一个三角形并在其上调用一个操作,但操作无效。

So, the first thing to consider would be, what is actually the domain that you are trying to model? 因此,首先要考虑的是,您尝试建模的域实际上是什么? (This is a bit hard to guess from your examples, but let me try...) Let's say that you have some objects and superman can jump over 3D shapes, but not over 2D shapes. (这有点难以从你的例子中猜出,但让我试试......)假设你有一些物体,超人可以跳过3D形状,但不能跳过2D形状。 You could use a discriminated union to distinguish between these two kinds of shapes: 您可以使用区分联合来区分这两种形状:

type Height = float
type Shape2DInfo = 
  | Triangle of float * float
type Shape3DInfo = 
  | Cylinder of float

type Shape = 
  | Shape2D of Shape2DInfo
  | Shape3D of Height * Shape3DInfo

The trick is that for all 3D shapes, we now have height directly available in the Shape3D case - so you can always get height of a 3D shape (regardless of which specific shape it is - here, only cylinder). 诀窍在于,对于所有3D形状,我们现在可以在Shape3D情况下直接获得高度 - 因此您可以始终获得3D形状的高度(无论它是哪种特定形状 - 这里,只有圆柱体)。 For 2D shapes, I did not include height, because they might, or might not have it... 对于2D形状,我没有包括高度,因为它们可能,或者可能没有...

Then you can write a jumping function that pattern matches on a shape and handles the three different cases - the shape is not jump-able, the shape is too small or the shape is high enough: 然后你可以编写一个跳跃函数,模式匹配一​​个形状并处理三种不同的情况 - 形状不可跳跃,形状太小或形状足够高:

let jumpOver shape = 
  match shape with
  | Shape2D _ -> printfn "Cannot jump!"
  | Shape3D(height, _) ->
      if height = Double.MaxValue then printf "Jumped!"
      else printfn "Too boring!"

In summary - if you have an abstract class with some properties in C#, the closest thing in F# (if you want to use functional design, rather than OO design) is to use a type that stores the common properties ( Shape ) and contains value of another type ( Shape3DInfo ) that specifies the details specific for each sub class. 总结一下 - 如果你有一个在C#中有一些属性的抽象类,F#中最接近的东西(如果你想使用功能设计,而不是OO设计)是使用一个存储公共属性( Shape )并包含值的类型另一种类型( Shape3DInfo ),指定每个子类的特定细节。

In a functional programming paradigm you would start with the functions, and work out the types from that. 在函数式编程范例中,您将从函数开始,并从中计算出类型。 So the starting point would be the function 所以起点就是功能

let CanSupermanJumpOver height = 
    height = TALL

You would only start thinking about polymorphism between triangles and cylinders when you have a question which needs to apply the CanSupermanJumpOver function on both interchangeably. 当你有一个需要交替应用CanSupermanJumpOver函数的问题时,你才会开始考虑三角形和圆柱体之间的多态性。 Basically the OO paradigm focusses on hiding the internal data structures in use. 基本上,OO范例专注于隐藏正在使用的内部数据结构。 The functional paradigm focusses on encapsulating complex process logic, but the data structures are transparent. 功能范例侧重于封装复杂的流程逻辑,但数据结构是透明的。 The trick is trying to combine both approaches without compromising the benefits of either. 诀窍在于尝试将这两种方法结合起来,而不会损害其中任何一种方法 That is something I've been struggling with over the last while. 这是我最后一直在苦苦挣扎的事情。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM