iT邦幫忙

2022 iThome 鐵人賽

DAY 2
0

1. 函數是什麼?

我們先回到昨天的程式碼:

int pi = 3.14159;

相信大家看一眼就知道,如果把3.14159放到一個int型別的參數,程式理所當然的會報錯。然而從數學的角度來看,如果我們將int視為整數的集合,3.14159顯然並不在定義裡面!這邊有一個函數:

https://chart.googleapis.com/chart?cht=tx&chl=f(x)%20%3D%20%5Csqrt%7Bx%7D

如果做成圖形

https://ithelp.ithome.com.tw/upload/images/20220913/20148594c7dT8VEfok.png

如果將 -1 放到函數裡會得到什麼呢?首先-1本身就不在函數的定義域裡面,所以這個問題一開始就不成立。函數是兩個集合的對應關係,並且這個關係是一對一或是多對一的,並不存在一對多或是一對無,換句話說,只要有一個值可以放到方法裡面,我們就必定可以得到對應的函數輸出。

https://ithelp.ithome.com.tw/upload/images/20220913/20148594pn6TM3gg0Y.png

2. C#中的函數

如何在C#中表示函數的概念呢?首先最直覺的就是方法(method)了,在物件導向中,我們通常會將方法視為依附於某個物件的行為,這邊可以先提到副作用的概念(side effect),利用方法去改變物件內部或外部的狀態。在FP裡面,函數都應該是無副作用的純函數(pure function),這些預計在後面的章節詳細介紹,回到C#中可以透過下面這些功能來實現函數:

  1. 方法(method)
  2. 委派(delegate)
  3. lambda expression
  4. Dictionary

其中Dictionary可能是個比較特別的想法,但是我們確實可以用字典定義每一個輸入值對應的輸出,

那麼我們一開始提到的開根號的函數要如何透過程式語言實現呢?

在純FP的語言Haskell中我們大概可以這樣寫

root :: Floating a => a -> a
root 0 = 0
root 0.01 = 0.1
root 0.04 = 0.2
......

雖然看起來很好笑,但我們的確可以把root函數所有的值都列舉出來,而-1並沒有被定義,所以輸入的話程式會報錯,有沒有觀察到,這就相當於C#的Dictionary了!

3. 設計定義域

從定義域跟值域的關係,我們可以學到什麼呢?如果將不在定義域中的元素放到一個函數中,程式就應該要報錯並且告知是因為錯誤的輸入,最直覺的方法是在函數的一開始就進行驗證

public string CheckBMIHealth(float weight,float height)
{
    if (weight > 300) 
			throw new ArgumentException("請不要拿大象開玩笑",nameof(weight));
		//,,,
}

這種將驗證寫在方法開頭的地方,很容易因為後續的驗證越來越多導致驗證的區塊越來越大,方法內真正重要的邏輯就會變得難以閱讀,C#中曾經有一個類別**Contract**去支援這種先驗條件,不過後來就沒有繼續維護了,OOP的角度會將這些驗證抽成獨立的驗證器物件,不管怎樣總之先把函數的輸入包成一個獨立的物件,然後將驗證抽到驗證器物件中

public class PersonBodyInfoEntity
{
    public float Weight { get; set; }
    public float Height { get; set; }
}

public class Validator
{
    public static void Validate(Person person)
    {
        if (person.Weight > 300) 
					throw new ArgumentException("請不要拿大象開玩笑","Weight");
    }
}

public string CheckBMIHealth(PersonBodyInfoEntity person)
{
    Validator.Validate(person);
}

但回想到函數不應該允許定義域以外的元素,其實也可以這麼做,微軟官方文件對ArgumentException其實也有提到這樣的作法。

public class PersonBodyInfoEntity
{
    private float _weight;
    public float Weight
    {
        get => _weight;
        set
        {
            if (value > 300) 
							throw new ArgumentException("請不要拿大象開玩笑",nameof(value));

						_weight = value;
        }
    }
    public float Height { get; set; }
}

這些討論其實已經有一點接近剖面導向(AOP)了,至於那一種作法比較好就見仁見智了,這其實關係到整個系統的設計,總之可以強調的是,如果在函數最開始的地方就確保輸入值的正確性,可以大大的提高系統的穩健(缺點是會多很多的code),讓工程師對自己的程式碼更有信心,明天會從值域與null處理開始,講到Map、Linq以及高階函數。

最後補上一點查資料的時候才知道的事情,或許大家有聽過不建議在程式中拋出例外,但實際上並不是單純的不建議,而是要在語意清楚的情況下選擇適合的例外型別,以上面的範例來說由於是引數錯誤所以選用ArgumentException ,詳細可以閱讀**使用標準例外狀況類型**


上一篇
Day1. 前言
下一篇
Day3. Option
系列文
Functional Programming with C#30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言