體會 OOP 與 FP 不同的思維

OOP 最自豪的就是 Polymorphism (多型),若使用 FP,該如何實現這個 OOP 的招牌菜呢?

Version


macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
C# 7.2
F# 4.1

Shape Again


在眾多 OOP 書中,都會看到這個經典 Shape,這是展示 OOP 經典的 Polymorphism 與 Virtual / Override 的經典範例。

oly00

  • CircleRectangleTriangle 都抽象化成 Shape 這個 abstract class
  • 因為每個形狀計算面積的公式都不同,因此在 Shape 開 abstract method,再由 CircleRectangleTriangle 各自實作 Area()
  • 最後將 CircleRectangleTriangle 都擺進 List,一一的呼叫每個形狀的 Area(),因為 OOP 的 Polymorphism 機制,所以我們不必去判斷 class 型別,自己會執行對的 class 與 method

這就是我們熟悉的 Polymorphism。

將分別以 C# 與 F# 實作此需求,體會一下 OOP 與 FP 的差異。

CSharp


Shape.cs

1
2
3
4
5
6
7
namespace ConsoleApp
{
public abstract class Shape
{
public abstract double Area();
}
}

定義 Shape abstract class,其中 Area() 為 abstract method,因為每個形狀計算面積的公式不同,必須由子類別去實作。

Circle.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace ConsoleApp
{
public class Circle : Shape
{
private readonly double _radius;

public Circle(double radius)
{

_radius = radius;
}

public override double Area()
{

return 3.14 * _radius * _radius;
}
}
}

Circle 由 constructor 傳入 radius,並 override Area() 計算圓形面積。

Rectangle.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace ConsoleApp
{
public class Rectangle : Shape
{
private readonly double _width;
private readonly double _height;

public Rectangle(double width, double height)
{

_width = width;
_height = height;
}

public override double Area()
{

return _width * _height;
}
}
}

Rectangle 由 constructor 傳入 widthheight,並 override Area() 計算矩形面積。

Triangle.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
namespace ConsoleApp
{
public class Triangle: Shape
{
private readonly double _base;
private readonly double _height;

public Triangle(double base_, double height)
{

_base = base_;
_height = height;
}

public override double Area()
{

return 0.5 * _base * _height;
}
}
}

Triangle 由 constructor 傳入 baseheight,並 override Area() 計算三角形面積。

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

List<Shape> myList = new List<Shape>
{
new Circle(10.0),
new Rectangle(2.0, 3.0),
new Triangle(4.0, 5.0)
};

foreach (var shape in myList)
{
var result = shape.Area();
Console.WriteLine("{0:F1}", result);
}
}
}
}

CircleRectangleTriangle 3 個物件都塞入 List,使用 foreach 執行每個物件的 Area(),不用判斷型別,就自動會執行對的物件,這就是我們熟知的 Polymorphism。

oly00

各自算出各形狀的面積。

FSharp


Program.fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
open System

type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base_: float * height: float

let area shape =
match shape with
| Circle radius -> 3.14 * radius * radius
| Rectangle (width, height) -> width * height
| Triangle (base_, height) -> 0.5 * base_ * height

[<EntryPoint>]
let main argv =
let circle = Circle 10.0
let rectangle = Rectangle (2.0, 3.0)
let triangle = Triangle (4.0, 5.0)

[circle; rectangle; triangle]
|> List.map area
|> List.iter (printfn "%A")

0 // return an integer exit code

第 3 行

1
2
3
4
type Shape = 
| Circle of radius: float
| Rectangle of width: float * height: float
| Triangle of base_: float * height: float

定義 Shape union,包含 CircleRectangleTriangle 三個 case。

至於三個型別的資料,則直接在 case 後面接 of 表示。

我們看到 C# 必須使用 abstract class繼承,才能描述 ShapeCircleRectangleTriangle 之間的抽象關係,但 F# 使用 union只要 4 行即可,而且還是強型別

第 8 行

1
2
3
4
5
let area shape =
match shape with
| Circle radius -> 3.14 * radius * radius
| Rectangle (width, height) -> width * height
| Triangle (base_, height) -> 0.5 * base_ * height

至於 Area() 的多型怎麼辦 ?

只要使用 Pattern Matching 判斷 shape 的型別,各自實作其面積公式即可。

我們看到 C# 必須使用 virtual / override 機制才能實現 Polymorphism,但 F# 使用 Pattern Matching 只要 5 行即可

15 行

1
2
3
4
5
6
7
8
let main argv =
let circle = Circle 10.0
let rectangle = Rectangle (2.0, 3.0)
let triangle = Triangle (4.0, 5.0)

[circle; rectangle; triangle]
|> List.map area
|> List.iter (printfn "%A")

不必使用 foreach ,只要以 Pipeline 方式透過 List.map 執行每個物件的 area(),再以 Pipeline 方式透過 List.iter 將計算的結果交給 printfn 顯示。

C# 雖然也可以使用 LINQ 方式,與 F# 極為類似,不過 LINQ 已經屬於 functional ,不是傳統 OOP 的 imperative

oly00

Conclusion


  • FP 使用 union 即可實作出 OOP 的 繼承 概念
  • FP 使用 Pattern Matching 即可實作出 OOP 的 virtual/override 概念
  • OOP 是將 data 與 function 合一,都包在 class 內;FP 是將 data 與 function 分離,data 歸 Type,function 還是 function
  • F# 的程式碼明顯比 C# 精簡
  • 並不是要比較哪個語言的優劣,只想強調 OOP 與 FP 以不同的思維,都可以實作出 Polymorphism

Sample Code


完整的範例可以在我的 GitHub 上找到:C#F#

Reference


F# for fun and profit, Four Key Concepts

2018-03-21