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=P4A
其中, P P P是管道截面与流体接触的长度,此时这个长度为整个内壁面对应的长度; A A A是流动面积。对于水力半径,计算公式如下。
r = A P r = \frac{A}{P} r=PA
这两个参数的定义类似,但是 P P P的计算中不包含自由液面的部分。没有自由液面,我们就不用水力半径作为尺度。这个是两个的本质区别。所以如果有人给你计算圆管道的水力半径等于管道半径的一半什么的,你就知道他还没搞懂,你就夸他独立思考,很有见地。

类库设计

首先,我们建模这个问题是就需要表达流通截面的概念,这个截面可能是不同的形状,那么不同的形状水力直径怎么计算呢?根据前面的公式,只需要能够计算周长和面积就行。所以可以设计下面这样的类继承关系,目前我们就考虑圆形截面管道和方形截面管道。

Shape
Circle
Rectangle

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.");
            }
        }
    }
}

这样看起来还是挺烦人的……

总结

  1. Discriminated Union可以用来实现一些比较简单的继承关系;
  2. Discriminated Union的每个构造函数可以带多个参数,这些参数就是Discriminated Union的属性;
  3. 比较复杂的继承关系,还是用类来实现比较好;
  4. 与.NET体系中的其它语言交互的时候,Discriminated Union的使用会比较麻烦。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大福是小强

除非你钱多烧得慌……

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值