[英]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.