F#奇妙游(10):可区分联合
这是个什么鬼
好吧,我也是学过C语言的骨灰程序员,看到下面的代码,我也是大受震撼的。还能这么玩?
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
这样的话,能玩的就多了。比如比较简单的类继承关系。
一个小小的类库
水力直径计算
那么我们就设计一个计算水力直径的类库。众所周知,水力直径是在计算管道流动时的重要特征尺度;与之类似的还有个概念,是水力半径。水力半径和水力直径唯一的关系是都是跟流动相关的特征尺度,别看中文网站(也有英文网站)有什么水力直径是水力半径的4倍什么的,那都是错误的。
水力直径是管道流动的特征尺度;水力半径是明渠流动的特征尺度。也就是前者是流体把整个管道充满了,比如运输气体的管道,流体与管道内壁全部接触。而明渠流动也很好理解,就是上面没有盖子的流动,比如河流、水渠,这个时候流体会因为重力作用,仅仅浸润下半部分管道、水渠壁面,上方还有一个自由液面。
对于水力直径(Hydraulic Diameter),计算公式如下:
D
=
4
∗
A
P
D = \frac{4 * A}{P}
D=P4∗A
其中,
P
P
P是管道截面与流体接触的长度,此时这个长度为整个内壁面对应的长度;
A
A
A是流动面积。对于水力半径,计算公式如下。
r
=
A
P
r = \frac{A}{P}
r=PA
这两个参数的定义类似,但是
P
P
P的计算中不包含自由液面的部分。没有自由液面,我们就不用水力半径作为尺度。这个是两个的本质区别。所以如果有人给你计算圆管道的水力半径等于管道半径的一半什么的,你就知道他还没搞懂,你就夸他独立思考,很有见地。
类库设计
首先,我们建模这个问题是就需要表达流通截面的概念,这个截面可能是不同的形状,那么不同的形状水力直径怎么计算呢?根据前面的公式,只需要能够计算周长和面积就行。所以可以设计下面这样的类继承关系,目前我们就考虑圆形截面管道和方形截面管道。
F#类设计
那么在F#里面,这个玩意就可以这么整。定义一个抽象类,然后用inherit
关键词来定义继承关系。对于学过Java的人来说,简直是不要太自然。
[<AbstractClass>]
type Shape() =
abstract member Area: float
abstract member Perimeter: float
member this.HydraulicDiameter = 4.0 * this.Area / this.Perimeter
type Circle(radius: float) =
inherit Shape()
member this.Radius = radius
override this.Area = Math.PI * radius * radius
override this.Perimeter = 2.0 * Math.PI * radius
type Rectangle(width: float, height: float) =
inherit Shape()
member this.Width = width
member this.Height = height
override this.Area = width * height
override this.Perimeter = 2.0 * (width + height)
这个类库功能相当孱弱,基本的计算两个很无聊形状的水力直径反正是做到了。客户端代码如下。
let c = Circle(1.0)
printfn "%F" c.HydraulicDiameter
let r = Rectangle(2.0, 2.0)
printfn "%F" r.HydraulicDiameter
从输出可以看到,这两个的水力直径都是2.0。这个结果是正确的,因为圆形截面的水力直径是直径,方形截面的水力直径是边长。这个结果也是符合我们的预期的。
奇怪又不奇怪的Discriminated Union
那么,有了这个奇怪的玩意之后,我们该怎么写呢?
type Shape =
| Circle of r: float
| Rectangle of w: float * h: float
member this.Area =
match this with
| Circle(r) -> Math.PI * r * r
| Rectangle(w, h) -> w * h
member this.Perimeter =
match this with
| Circle(r) -> 2.0 * Math.PI * r
| Rectangle(w, h) -> 2.0 * (w + h)
member this.HydraulicDiameter =
4 * this.Area / this.Perimeter
客户端代码保持不变,结果还是一样的。
两者的比较
比较起来,用类来实现的增加新的形状需要定义对应的类,把面积和周长计算的函数定义清楚,并定义对应的表达形状尺寸的属性。
type Triangle(base: float, height: float, position: float) =
inherit Shape()
member this.Base = base
member this.Height = height
member this.Position = position
override this.Area = 0.5 * base * height
override this.Perimeter =
let b1 = base * position
let b2 = base * (1.0 - position)
let le b h = Math.Sqrt(b * b + h * h)
base + le b1 height + le b2 height
这就定义了一个三角形,用底边长度、高和顶点在底边投影的位置来定义三角形。这个三角形的周长计算比较复杂,需要用到勾股定理。这个时候,我们就可以用Discriminated Union来定义这个三角形。
用Discriminated Union的方式,需要编辑Shape的代码,增加新的形状,然后在Area和Perimeter的计算中增加对新形状的处理。这里不需要重复定义形状尺寸的属性,因为Discriminated Union的构造函数就是这个属性。
type Shape =
| Circle of r: float
| Rectangle of w: float * h: float
| Triangle of b: float * h: float * p: float
member this.Area =
match this with
| Circle(r) -> Math.PI * r * r
| Rectangle(w, h) -> w * h
| Triangle(b, h, p) -> 0.5 * b * h
member this.Perimeter =
match this with
| Circle(r) -> 2.0 * Math.PI * r
| Rectangle(w, h) -> 2.0 * (w + h)
| Triangle(b, h, p) ->
let b1 = b * p
let b2 = b * (1.0 - p)
let le b h = Math.Sqrt(b * b + h * h)
b + le b1 h + le b2 h
member this.HydraulicDiameter =
4 * this.Area / this.Perimeter
此外,用类的方式定义的库,在.NET体系中的其它语言调用的时候,可以直接当成类来用。而用Discriminated Union的方式定义的库,在其它语言中调用的时候,需要用对应的Discriminated Union的方式来定义。比如,用C#来调用的时候,需要用下面的代码来定义Discriminated Union。
// call F# Discriminated Union
using Microsoft.FSharp.Core;
namespace MyNamespace
{
class Program
{
static void Main(string[] args)
{
// Create a circle shape
Shape circle = Shape.NewCircle(5.0);
// Create a rectangle shape
Shape rectangle = Shape.NewRectangle(2.0, 3.0);
// Call the area function from C#
double circleArea = area(circle);
double rectangleArea = area(rectangle);
}
static double area(Shape shape)
{
switch (shape.Tag)
{
case Shape.Tags.Circle:
double radius = ((Shape.Circle)shape).Item;
return System.Math.PI * radius * radius;
case Shape.Tags.Rectangle:
var rectangle = (Shape.Rectangle)shape;
return rectangle.Item1 * rectangle.Item2;
default:
throw new ArgumentException("Invalid shape type.");
}
}
}
}
这样看起来还是挺烦人的……
总结
- Discriminated Union可以用来实现一些比较简单的继承关系;
- Discriminated Union的每个构造函数可以带多个参数,这些参数就是Discriminated Union的属性;
- 比较复杂的继承关系,还是用类来实现比较好;
- 与.NET体系中的其它语言交互的时候,Discriminated Union的使用会比较麻烦。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~