函数式编程基础
此页面的材料最初是为 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
答案
|