跳转到内容

函数式编程基础

来自维基教科书,开放世界中的开放书籍

论文 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


华夏公益教科书