跳轉到內容

函數語言程式設計基礎

來自華夏公益教科書

論文 2 - ⇑ 函數語言程式設計基礎 ⇑

← 什麼是函數語言程式設計 函數語言程式設計基礎 列表處理 →



函式 - 將每個給定輸入分配特定輸出的規則


- 構成函式輸入的資料集


陪域 - 從中選擇函式輸出的資料集


函式 是將一組輸入連線到給定輸出的規則。我們可以將輸入建模為 SetA,其中輸出是從 SetB 中選擇的。SetB 中的每個成員都不一定都對映到 SetA 中的輸入。在下面的例子中,SetA 將輸入描述為自然數(正整數和零,),函式 是將輸入加倍。輸出來自 SetB,自然數集,請注意,SetB 中並非所有元素都從 SetA 對映,例如,將正整數加倍永遠不會對映到數字 3。

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

解釋函式工作原理的另一種方法是描述函式型別。上面加倍函式的規則可以寫成 f: A → B,這描述了函式 f 是一個將引數(輸入)型別 A 對映到結果(輸出)型別 B 的規則。將 AB 替換為它們的資料型別,將 f 替換為一個名稱,我們得到

doubleNumber: integer -> integer

我們也可以將 isPrime 函式寫成函式型別

isPrime: integer -> boolean

函式型別的其他示例包括

isCorrectPassword: string -> boolean

MD5HashingAlgorithm: string -> string

phoneNumberSearch: string -> integer

一等公民

[編輯 | 編輯原始碼]

在函數語言程式設計語言中,函式是一等公民。這意味著它們可以以以下方式使用

  • 分配給變數
  • 分配為引數
  • 在函式呼叫中返回
  • 出現在表示式中

讓我們看看它的實際應用(在您學習本章的過程中,您將更多地瞭解這段程式碼的作用),

.> 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 宣告

[編輯 | 編輯原始碼]

在 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
  | ^^^^
擴充套件:Let 和 Where

本章對 Haskell 宣告進行了非常簡單的介紹,類似於其他語言中變數或常量的構造。letwhere 命令提供了更好的功能,可以在learn you a haskell 中瞭解更多關於它們的資訊。

練習:函數語言程式設計基礎

描述域和陪域

答案


域 - 函式輸入取值的資料集。

陪域 - 函式輸出選擇取值的資料集。並非所有陪域值都需要是輸出。


以下哪些說法是正確的

  1. 域和陪域的型別始終相同
  2. 陪域始終小於域
  3. 域中的每個值在陪域中都有一個唯一的匹配值
  4. 函式的輸出並不總是必須與陪域中的每個值匹配

答案


4.


編寫一個函式的函式型別描述,該函式以一個人的郵政編碼作為引數,並返回該地區的犯罪統計資料,該資料介於 1 到 5 之間。

答案


postCodeCrime: string -> integer


編寫一個函式的函式型別描述,該函式以年份作為數字,並告訴你它是否為閏年

答案


leapYear: Integer -> Boolean


編寫以下函式的函式型別:ConvertMonthNumbertoName

答案


ConvertMonthNumbertoName: integer -> string

編寫我們自己的函式

[編輯 | 編輯原始碼]

現在我們已經瞭解瞭如何使用 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 中有幾個基本型別(始終以大寫字母開頭),包括:IntegerFloatDoubleBoolChar

讓我們建立一個函式,告訴我們一個數字是否為魔法,其中 是魔法數字。在編寫函式的程式碼之前,我們定義函式的名稱 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命令找出 Haskell 中某事物的型別。例如

.> :t 234.5111111
234.5 :: Fractional p => p
.> :t max
max :: Ord a => a -> a -> a

a是奇數,因為它是一種不以大寫字母開頭的型別,與其他日期型別名稱不同,它是一個型別變數。這意味著max函式不繫結使用任何特定型別,但它有三次a,這意味著我們必須將相同型別的變數傳遞給max函式的兩個引數,並且max函式將返回與引數型別相同的型別的值。

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

它描述了由兩個整數的 笛卡爾積 定義的函式 finteger 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,並可能包含所有錯誤,我們可以使用部分函式 LTLLess 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 傳遞給這個部分函式,形成一個新的部分函式。這被宣告為 standardBasestandardBase 有 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
Demonstration of how partial functions are used to solve a multi argument function call
部分函式如何用於解決多引數函式呼叫的演示

我們建立部分函式,其中包含已內建一個或多個引數的計算。這些部分函式隨後準備接受其他引數以完成函式或建立更多部分函式。部分函式可以用作其他函式的構建塊。

函式組合

[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

為了再次檢查它是否按預期工作

  1. 如果 y = 5
  2. 那麼 f(y) = 5 + 6 = 11
  3. 透過 g  ∘ ff 的輸出對映到 g 的輸入得到 x = 11
  4. 計算 g 得到 11 / 2 = 5.5
  5. 這就是我們在上面看到的。
練習:部分函式和函式組合

為什麼我們可能想要使用部分函式?

答案

  • 它允許我們用預先製作的塊或部分函式來構建其他函式
  • 它可以減少對常見語句的誤輸入


一家地毯銷售公司想要計算店內地毯的體積,他們編寫了以下程式碼,它輸出什麼?

cylVol :: Double -> Double -> Double
cylVol h r = (pi * r^2) * h

fiveFooter = cylVol 5
sixFooter = cylVol 6
fiveFooter 4
sixFooter 4

答案

  • 251.32742
  • 301.5929


對於以下函式,哪些編號的函式組合是允許的?

f: Integer -> Bool

g: Integer -> Float

h: Integer -> Integer

i: Float -> Bool

  1. f ∘ g
  2. g ∘ h
  3. h ∘ f
  4. h ∘ h
  5. g ∘ g
  6. g ∘ i


答案

  1. f ∘ g NO - f 的域是 Integer,g 的陪域是 Float
  2. g ∘ h YES - g 的域是 Integer,h 的陪域是 Integer
  3. h ∘ f NO - h 的域是 Integer,f 的陪域是 Bool
  4. h ∘ h YES - h 的域是 Integer,h 的陪域是 Integer
  5. g ∘ g NO - g 的域是 Integer,g 的陪域是 Float
  6. g ∘ i NO - g 的域是 Integer,i 的陪域是 Bool


以下函式組合的輸出是什麼?

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

答案

  • 0
  • 144
  • 316


華夏公益教科書