函數語言程式設計基礎
| 此頁面的材料最初是為 isaac 計算機科學 編寫的 |
函式 是將一組輸入連線到給定輸出的規則。我們可以將輸入建模為 SetA,其中輸出是從 SetB 中選擇的。SetB 中的每個成員都不一定都對映到 SetA 中的輸入。在下面的例子中,SetA 將輸入描述為自然數(正整數和零,),函式 是將輸入加倍。輸出來自 SetB,自然數集,請注意,SetB 中並非所有元素都從 SetA 對映,例如,將正整數加倍永遠不會對映到數字 3。

描述輸入的集合的另一個名稱是域,從中選擇可能的輸出的集合稱為陪域。域和陪域的型別不必相同。例如,我們可以有一個函式,告訴我們給定的正整數輸入是否為素數。在這種情況下,域將是自然數 (),而陪域是布林值True 和 False,如下所示

解釋函式工作原理的另一種方法是描述函式型別。上面加倍函式的規則可以寫成 f: A → B,這描述了函式 f 是一個將引數(輸入)型別 A 對映到結果(輸出)型別 B 的規則。將 A 和 B 替換為它們的資料型別,將 f 替換為一個名稱,我們得到
doubleNumber: integer -> integer
我們也可以將 isPrime 函式寫成函式型別
isPrime: integer -> boolean
函式型別的其他示例包括
isCorrectPassword: string -> boolean
MD5HashingAlgorithm: string -> string
phoneNumberSearch: string -> integer
在函數語言程式設計語言中,函式是一等公民。這意味著它們可以以以下方式使用
- 分配給變數
- 分配為引數
- 在函式呼叫中返回
- 出現在表示式中
| 記住一等公民的最好方法是:EVAC。一等公民在Expressions(表示式)、Variables(變數)、Arguments(引數)中使用,並且可以從函式Calls(呼叫)中返回 |
讓我們看看它的實際應用(在您學習本章的過程中,您將更多地瞭解這段程式碼的作用),
.> byThePowerOf x y = x^y -- assigning to a variable
.> 3 * byThePowerOf 2 2 -- appearing in an expression
12
.> superPower x y = x^y
.> superPower 2 (byThePowerOf 2 2) -- assigned as an argument
16
.> twoToSomething = superPower 2 -- returned by a function call
.> twoToSomething 4
16
在編寫程式性程式設計程式碼時,您已經使用過一等公民,其中整數、字元和其他資料型別以及變數用作引數,由函式呼叫返回,出現在表示式中,並且可以分配給變數。
在 Haskell 中,當您想宣告一個值並將其附加到一個名稱時,您可以使用 = 命令,就像大多數其他語言一樣
.> years = 17
.> seconds = years * 365 * 24 * 60 * 60
.> name <- getLine
Kim
.> print("Hey " ++ name ++ ", you are " ++ show seconds ++ " seconds old")
"Hey Kim, you are 536112000 seconds old"
如果我們要宣告我們自己的pi 版本,我們可以寫
.> myPi = 3.14
.> myPi * 3
9.42
但是,正如您希望記住的那樣,函數語言程式設計中的變數是不可變的,我們不應該在程式中宣告 myPi 的值後對其進行編輯
| varyingTrouble.hs |
|---|
myPi = 3.14
myPi = 4
|
嘗試從命令列編譯此程式碼會產生錯誤,Haskell 不會允許您兩次宣告同一件事。換句話說,它不會允許您在宣告某件事的值後更改它的值,Haskell 中的變數是不可變的
.> :l varyingTrouble.hs
main.hs:2:1: error:
Multiple declarations of ‘myPi’
Declared at: varyingTrouble.hs:1:1
varyingTrouble.hs:2:1
|
2 | myPi = 4
| ^^^^
如果您從 GHCi 命令列多次宣告 myPi,那麼它似乎允許您重新定義 myPi 的值而不會出現任何錯誤。這實際上並非如此,並且不建議這樣做 |
|
擴充套件:Let 和 Where 本章對 Haskell 宣告進行了非常簡單的介紹,類似於其他語言中變數或常量的構造。 |
|
練習:函數語言程式設計基礎 描述域和陪域 答案
陪域 - 函式輸出選擇取值的資料集。並非所有陪域值都需要是輸出。
以下哪些說法是正確的
答案
編寫一個函式的函式型別描述,該函式以一個人的郵政編碼作為引數,並返回該地區的犯罪統計資料,該資料介於 1 到 5 之間。 答案
編寫一個函式的函式型別描述,該函式以年份作為數字,並告訴你它是否為閏年 答案
編寫以下函式的函式型別:ConvertMonthNumbertoName 答案
|
現在我們已經瞭解瞭如何使用 Haskell 的內建函式編寫簡單的函式式程式,我們將開始定義我們自己的函式。為此,我們寫下函式的名稱以及它可能接受的任何引數,然後將返回值設定為一個計算或一小段程式碼。例如
.> byThePowerOf x y = x^y
.>
.> byThePowerOf 2 3
8
.> isMagic x = if x == 3 then True else False
.>
.> isMagic 6
False
.> isMagic 6.5
False
.> isMagic 3
True
Haskell 使用靜態型別,每個引數和返回值的型別在編譯時都是已知的,這與 Python 等語言不同,Python 是動態型別的。當我們編寫函式時,我們可以宣告應該用於引數和輸出的型別,這意味著我們將更好地控制編譯器如何建立我們定義的函式。
Haskell 中有幾個基本型別(始終以大寫字母開頭),包括:Integer、Float、Double、Bool、Char
讓我們建立一個函式,告訴我們一個數字是否為魔法,其中三 是魔法數字。在編寫函式的程式碼之前,我們定義函式的名稱 isMagic,以及輸入的型別(Integer)和輸出的型別(Bool)。為此,我們將把程式碼寫在一個單獨的文字檔案中。
| definingDataTypes.hs |
|---|
isMagic :: Integer -> Bool
isMagic x = if x == 3
then True
else False
|
這意味著當我們載入此程式碼時,它只接受整數輸入,任何其他輸入都會返回錯誤。我們也知道,如果給定整數輸入,它將返回一個布林值。
.> :l definingDataTypes
[1 of 1] Compiling definingDataTypes ( definingDataTypes.hs, interpreted )
Ok, one module loaded.
.> isMagic 3
True
.> isMagic 7
False
.> isMagic 7.5
<interactive>:5:9: error:
• No instance for (Fractional Int) arising from the literal ‘7.5’
• In the first argument of ‘isMagic’, namely ‘7.5’
In the expression: isMagic 7.5
In an equation for ‘it’: it = isMagic 7.5
其他 Haskell 宣告函式型別和函式程式碼的示例包括
| 函式 | 示例呼叫 |
|---|---|
factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)
|
.> factorial 3
6
|
| 函式 | 示例呼叫 |
|---|---|
abc :: Char -> String
abc 'a' = "Apple"
abc 'b' = "Boat"
abc 'c' = "Cat"
|
.>abc 'c'
"Cat"
|
| 函式 | 示例呼叫 |
|---|---|
byThePowerOf :: Integer -> Integer -> Integer
byThePowerOf x y = x^y
|
.> byThePowerOf 2 3
8
|
輸出型別始終是->列表中指定的最後一個資料型別。所有其他資料型別定義輸入。在上面的byThePowerOf示例中,列出了三種類型Integer -> Integer -> Integer。前兩個Integer資料型別指定引數型別,最後一個列出的資料型別,即最後的Integer,指定輸出型別。
|
擴充套件:Haskell 型別變數 可以使用 .> :t 234.5111111
234.5 :: Fractional p => p
.> :t max
max :: Ord a => a -> a -> a
在 learn you a haskell 上了解更多資訊 |
|
練習:編寫你自己的函式 在 Haskell 中編寫一個函式,以及型別定義,以輸出給定半徑的任何圓形的面積 答案 circleArea :: Double -> Double
circleArea r = 3.14 * r^2
-- alternatively
circleArea r = pi * r^2
在 Haskell 中編寫一個函式,以及型別定義,以輸出三個帶有小數點的數字的平均值 答案 avgThree :: Double -> Double -> Double -> Double
avgThree x y z = (x + y + z) / 3
在 Haskell 中編寫一個函式,以及型別定義,以輸出某人是否可以觀看 18+ 電影,當他們給出自己的年齡(以年為單位)時。 答案 letThemIn :: Integer -> Bool
letThemIn y = if y >= 18
then True
else False
|
函式應用
[edit | edit source]將輸入作為引數傳遞給函式被稱為函式應用。del(7,9)是將函式 del(刪除)應用於引數 7 和 9。可以寫成
f: integer x integer -> integer它描述了由兩個整數的 笛卡爾積 定義的函式 f,integer x integer;這意味著可以接受任何兩個整數引數的組合(x 不等於乘法)。最後的箭頭 (->) 表明函式 f 在這些整數上的輸出也是一個整數。
雖然看起來 f 接受兩個引數,但由於引數被定義為笛卡爾積,所以 f 只接受一個引數,即一對值,例如 (7,9) 或 (101, 99)。
部分函式應用
[edit | edit source]
Haskell 似乎擁有接受多個引數的函式,例如 max 2 2,但實際上 Haskell 中的函式只能接受一個引數。這怎麼可能?檢視 min 函式的例子,我們可以用兩種不同的方式編寫相同的函式呼叫,兩種方式都可行
.> min 50 13
13
.> (min 50) 13
13
第二個例子 (min 50) 13 顯示了一個部分函式應用。正如你從數學和之前的程式設計中所知,內部括號總是先執行。這意味著 (min 50) 只用一個輸入執行,而它需要兩個?!括號內的程式碼返回一個函式,它仍在等待缺失的輸入才能返回值,這就是我們稱它為部分函式的原因,(min 50) 只是部分執行。為了更清楚地說明這一點,讓我們將 (min 50) 宣告為一個變數
.> LTL = min 50 -- assign a partial function as a variable
.> LTL 13 -- partial function LTL is completed by the argument 13
13 -- returning the result of (min 50) 13
.> LTL 8
8
.> LTL 90
50
雖然上面的例子相當簡單,但你可以看到,與其多次編寫 min 50,並可能包含所有錯誤,我們可以使用部分函式 LTL(Less Than L)來代替。
我們可以將上面使用的 min 函式描述為函式型別
min: Integer -> Integer -> Integer將單個 Integer 傳遞給 min 返回了一個函式,該函式接受另一個 Integer 作為引數並返回一個 Integer。可以使用括號(定義返回值函式)來寫成
min: Integer -> (Integer -> Integer)讓我們看一個更復雜的例子,它試圖計算盒子的體積。有三個 Integer 引數 x,y,z 和一個 Integer 輸出。
| partialBoxes.hs |
|---|
boxVol :: Integer -> Integer -> Integer -> Integer
boxVol x y z = x * y * z
|
也可以寫成
boxVol :: Integer -> (Integer -> (Integer -> Integer))
讓我們看看如何使用 boxVol 的部分函式。透過同時將兩個引數 4 和 5 傳遞給 boxVol,我們首先建立一個部分函式,其中 4 作為引數 x,Integer -> (Integer -> Integer) 作為預期引數和輸出。然後 Haskell 將 5 傳遞給這個部分函式,形成一個新的部分函式。這被宣告為 standardBase。standardBase 有 x = 4 和 y = 5,它接受 Integer -> Integer 作為預期引數和輸出。將 9 傳遞給部分函式 standardBase 然後完成該函式,它現在可以返回值 180(4 * 5 * 9)。
.> :l partialBoxes.hs
[1 of 1] Compiling partialBoxes( definingDataTypes.hs, interpreted )
Ok, one module loaded.
.> standardBase = boxVol 4 5
.> standardBase 9
180
.> squareBase = boxVol 4 4
.> squareBase 9
144
.> standardBase 9
120

我們建立部分函式,其中包含已內建一個或多個引數的計算。這些部分函式隨後準備接受其他引數以完成函式或建立更多部分函式。部分函式可以用作其他函式的構建塊。
函式組合
[edit | edit source]
有時你可能想要將兩個函式組合在一起,這被稱為函式組合。假設我們有一個函式 f,其函式型別為 f: A -> B,另一個函式 g,其函式型別為 g: B -> C。我們可以使用函式組合透過命令 g ∘ f 來建立一個新函式
g ∘ f: A -> C為了使函式組合起作用,f 的陪域必須與 g 的域匹配,例如 f: A -> B = g: B -> C。允許 f 的輸出對映到 g 的輸入。我們可以透過檢視這個例子來看到這一點
g(x) = x / 2
f(y) = y + 6
我們知道 g 的域與 f 的陪域匹配,並且透過 g ∘ f,我們將 g 的輸入作為 f 的輸出
input of g = x = f(y)
f(y) = y + 6
therefore x = y + 6
g(f(y)) = (y + 6) / 2
另一種寫 g ∘ f 的方法是 g(f(y)),它告訴我們應該首先計算 f,然後將 f 的輸出對映到 g 的輸入。
我們可以在 Haskell 中透過使用句號 “.” 來執行函式組合。以上面的例子為例,我們可以將其轉換為程式碼
.> g x = x/2
.> f y = y + 6
.> (g.f) 3
4.5
.> h = g.f
.> h 5
5.5
為了再次檢查它是否按預期工作
- 如果
y = 5, - 那麼
f(y) = 5 + 6 = 11, - 透過
g ∘ f將f的輸出對映到g的輸入得到x = 11, - 計算
g得到11 / 2 = 5.5。 - 這就是我們在上面看到的。
|
練習:部分函式和函式組合 為什麼我們可能想要使用部分函式? 答案
一家地毯銷售公司想要計算店內地毯的體積,他們編寫了以下程式碼,它輸出什麼? cylVol :: Double -> Double -> Double
cylVol h r = (pi * r^2) * h
fiveFooter = cylVol 5
sixFooter = cylVol 6
fiveFooter 4
sixFooter 4
答案
對於以下函式,哪些編號的函式組合是允許的?
答案
以下函式組合的輸出是什麼? funA :: Integer -> Integer
funA x = (x * 3)^2
funB :: Integer -> Integer
funB y = 4 * (y - 2)
(funA.funB) 2
(funA.funB) 3
(funB.funA) 3
答案
|