我之所以對函數式代碼感興趣是因為函數式代碼富有表現力,可以使用簡短、緊湊的代碼完成工作,同時能對特定的問題給出優雅的解決方案。現代的編程語言不約而同的朝着面向對象、函數式、動態、解釋執行的方向發展,例如Ruby,Swift。而另一些語言則更加強調函數式編程,如F#,Scala,這種語言有着強大的類型推斷系統,編寫的代碼潔程度則令人嘆為觀止。
在F#編寫一個兩個數相加的函數,在F# Interactive中輸入:
let add num1 num2=num1*num2;;
F# Interactive為我們推斷了這個函數類型:val add : num1:int -> num2:int -> int,表示add有兩個int類型的參數得到了1個int類型。
函數當作參數:
//C# private int Twice(int input,Func<int,int> f) { return f(f(input)); } var result = Twice(2, n => n*n);
使用F#則只需要非常簡潔的一個函數聲明:
> let twice (input:int) f=f(f(input));; val twice : input:int -> f:(int -> int) -> int > twice 2 (fun n->n*n);; val it : int = 16
val twice : input:int -> f:(int -> int) –> int 這句話則是F# Interactive給出的推斷:twice函數需要一個int參數和一個(int->int)的函數作為參數,返回一個int.
這兩個例子僅僅是熱身,並不是本篇博客的重點,所以你覺得前兩個例子很無聊或者沒太看明白請繼續看下面的總結。
場景:某種活動會有一個日程安排(Schedule),日程安排有3中類型,只舉辦一次(Once),每天一次(Daily),每周一次(Weekly)。活動會根據日程安排(Schedule)的類型不同具有不同的宣傳內容,不同的延期舉行策略。
你對於這樣的場景會有怎么樣的思考呢?
一、面向過程類型的編碼方式
面向過程類型的編碼是需求的直譯過程,代碼會寫成這樣:
1.顯示活動的宣傳內容:
public void ShowScheduleDescriptions() { switch (ScheduleType) { case ScheduleType.Once: Console.WriteLine("this is once activity"); break; case ScheduleType.Daily: Console.WriteLine("this is daily activity"); break; case ScheduleType.Weekly: Console.WriteLine("this is weekly activity"); break; default: throw new InvalidOperationException("unsupported schedule"); } }
這樣的代碼初次看起來沒什么問題,實際存在兩個危險信號:
- 違反開放封閉(OCP)原則,如果有一天需要加入一種Monthly類型,無疑需要修改這個方法;
- 這樣的代碼風格會讓接下來的開發者不假思索的進行延續,比方說需要根據不同的活動類型延期活動;
2. 延期活動:
public void DelaySchedule() { switch (ScheduleType) { case ScheduleType.Once: Console.WriteLine("Delay one hour"); break; case ScheduleType.Daily: Console.WriteLine("Delay one day"); break; case ScheduleType.Weekly: Console.WriteLine("Delay one week"); break; default: throw new InvalidOperationException("unsupported schedule"); } }
這樣的代格違反了DRY原則,相同的代碼框架卻無法重用。
二、面向對象的編碼方式
對於一個有經驗的OO開發者,一旦看到switch,if(type=typeof(…))之類的代碼馬上會提高警惕,是不是有一些抽象類型沒有被找出來?在這個例子中則會找出下面的抽象:
public class Schedule { public virtual void ShowShowScheduleDescriptions() { } public virtual void DelaySchedule() { } } public class OnceSchedule : Schedule { public override void ShowShowScheduleDescriptions() { Console.WriteLine("this is once activity"); } public override void DelaySchedule() { Console.WriteLine("Delay one hour"); } } public class DailySchedule : Schedule { public override void ShowShowScheduleDescriptions() { Console.WriteLine("this is daily activity"); } public override void DelaySchedule() { Console.WriteLine("Delay daily day"); } } //... other schedule
這樣的代碼很好的解決了面向過程代碼的兩個問題,看起來更加具有擴展性,隨着新類型的Schedule引入,舊的代碼完全不用改動。
當然事情也不是絕對的,什么情況下需要改動舊代碼呢?當需要擴展Schedule的行為的時候,例如需求升級,不同的Schedule具有不同的舉辦方式,我們不得不在每種Schedule中加入一個 void Hold()方法。
三、函數式解決方案
函數式語言則使用可區分聯合和模式匹配來處理此類問題。
定義一個Schedule可區分聯合:
type Schedule= | Once of DateTime | Daily of DateTime*int | Weekly of DateTime*int
這個類型既說明了Schedule有三個不同的類型,同時定義了三種類型分別具有的數據結構。像是Enum和類的綜合體,但是又顯得特別精致。
1.顯示活動的宣傳內容,使用了模式匹配:
let ShowShowScheduleDescriptions schedule= match schedule with | Once(DateTime)-> printfn "this is once activity" | Daily(DateTime,int)->printfn "this is daily activity" | Weekly(DateTime,int)->printfn "this is weekly activity"
這個方法類似於switch…case,但是通過匹配可區分聯合來實現,而不是通過一個顯示的Enum來實現。
2. 延期活動:
let DelaySchedule schedule= match schedule with | Once(DateTime)-> printfn "Delay one hour" | Daily(DateTime,int)->printfn "Delay one day" | Weekly(DateTime,int)->printfn "Delay one week"
函數式編程的解決方案認為可以很方便的添加新的行為,例如增加新的行為:Hold()。通過定義可區分聯合和模式匹配來完成編碼,整個解決方案像是面向過程和面向對象的一種結合體,但是側重點不同,實現的代碼也更加精致。