今天來試試看函數式的思考吧,碎念一下當初選這個題目是因為發現自己OO中毒,沒有繼承不會寫程式。想要深入學習的FP話推薦Haskell趣學指南,我就拿裡面的題目來用C#重作一次。
後綴表示法是有別於我們習慣的算式表示法,目的是為了不用括弧表現加減乘除的先後順序,舉個例子來說
1 - ( 2 + 3 ) x 4
我們知道要先計算2+3=5,再將5x4=20,最後計算1-20=-19,如果換成後綴表示法則是
1 2 3 + 4 x -
當遇到計算符號+,就把前面兩項相加,這時候計算式會變成 1 5 4 x -
,依照這個規律把每一項消去,我們可以用堆疊的觀念去解決
圖片來源 http://learnyouahaskell.com/functionally-solving-problems#reverse-polish-notation-calculator
依序將輸入的數字放入堆疊,如果遇到運算符號就計算堆疊最上面的兩個元素
var input = "1 1 +";
var array = input.Split(' ') // 取得["1","1","+"]
接下來要遍歷一次array,只要遇到數字就放入堆疊,如果是運算符號的話就要進行計算,這邊可以拆成三個子題目
判斷是不是數字可以用TryParse,我們要包裝一下方法的回傳值,然後用select將集合Map過去,(bool IsInt, int number, string operator)
這個東西其實很類似Either
var input = "1 1 +";
var array = input.Split(' ') // 取得["1","1","+"]
.Select(ParseInt);
(bool IsInt, int number, string oper) ParseInt(string s)
{
var isInt = int.TryParse(s, out var n);
return (isInt, n, s);
}
Aggregate
符合需求var input = "1 1 +";
var array = input.Split(' ') // 取得["1","1","+"]
.Select(ParseInt);
.Aggregate(new Stack<int>(), Compute)
.First();
(bool IsInt, int number, string oper) ParseInt(string s)
{
var isInt = int.TryParse(s, out var n);
return (isInt, n, s);
}
Stack<int> Compute(Stack<int> stack, (bool IsInt, int number, string oper) s)
{
return s switch
{
(true, var v, _) => PushIntoStack(stack, v),
(false, _, var o) => Operate(stack, o),
_ => throw new InvalidOperationException()
}
}
Stack<int> PushIntoStack(Stack<int> s, int v)
{
s.push(v);
return s
}
Stack<int> Operate(Stack<int> s, string o)
{
return o switch
{
"+" => PushIntoStack(s, (s.Pop() + s.Pop())),
_ => throw new InvalidOperationException()
}
}
到這邊程式就大致上完成了,接下來只要把其他的運算符號功能也加上去就ok了。
我很難說這樣寫比較好,在C#中有提供Stack型別,不像Haskell,如果不強制要求函數有回傳值的話,其實直接用預設的方法可以更簡單。我們還是可以仔細觀察,不知道有沒有注意到這個設計方式資料與行為分離的特徵,所有的函數都只是在處理資料,然後用pattern match的方式做資料的組合或拆解。最後我自己並不喜歡沒有回傳值的方法,尤其在維護大型程式碼的時候,方法名稱能夠提供的訊息太少,下挖常常會有意想不到的驚嚇。
目前的程式碼還有一些缺陷,就當我在埋梗,之後在講講有什麼不足吧。