函数式编程Funcational Programming Haskell /Software Foundations 课程知识点复习—— 基础知识复习

在这里插入图片描述

前言:

Hi,好久不见,距离上一次更新还是三个月前,主要的原因是在忙碌学校的各种事情。这学期选了Software Foundations的课,后续会在复习备考过程中将相应的知识分享、更新在博客中。

本篇主要讲解Haskell的基础知识,类型、运算符、基础函数、函数定义、条件判断等等,过几天会在b站更新相应的练习讲解。

后续会打算挑选一些比较有意思的作业题目发布到博客上来一起学习。

除了课程外,还会重点更新关于LLM大模型框架下,其对传统3D打印、3D制造技术的影响,论文分享等等,敬请期待。

Haskell学习网站/书籍:
Haskell Documentaion https://www.haskell.org/
Haskell Github 资源网站https://github.com/tch0/notes/blob/master/Haskell.md
Types and Programming Languages 英语好的同学可以尝试下,书很不错

1.常用的Typeclass:

Eq 可判断相等性的类型
Ord 可比较大小的类型
Num 数字
Integral 整数,包括Int Integer
Floating 浮点数,包括Float Double

2.基本运算符

*: 乘法
/ : 浮点数除法,只能用于浮点型(Float,Double)
div : 整数除法,只能用于整数型(Int,Integer)
mod: 取余 5 'mod' 2 = 1

3.List:

有序性(Ordered)列表中的元素是有序的。

[1, 2, 3] -- 与 [3, 2, 1] 不同

同质性(Homogeneous): 列表中的所有元素必须是相同类型的。你不能将不同类型的元素混合在同一个列表中。

[1, 2, 3]     -- 正确:所有元素都是 Int
[1, "hello"]  -- 错误:不能将 Int 和 String 放在同一个列表中

递归定义: 列表的结构是递归的,每个列表都是由“空列表”([])或“一个元素和剩下的列表”(x:xs)组成。这个定义非常自然地配合递归函数。

-- 列表结构 [1,2,3] 等价于 1 : (2 : (3 : []))

不可变性(Immutable): Haskell 的列表是不可变的。一旦创建,列表的内容无法更改。如果需要对列表进行操作,例如添加或删除元素,实际上是创建了一个新的列表,而不是修改原列表。
Take CARE: Haskell中所有单位都不能改变,变量的值,list的值,tuple的值都不能改变!

let xs = [1, 2, 3]
let ys = 0:xs  -- ys 是 [0,1,2,3],但 xs 仍然是 [1,2,3]

2.1基本操作符: Haskell 为列表定义了一些常用的操作符:

:(cons 运算符):用于将一个元素添加到列表的开头。

1 : [2, 3] – 结果是 [1, 2, 3]
++(连接运算符):用于连接两个列表。

[1, 2] ++ [3, 4] – 结果是 [1, 2, 3, 4]

2.2模式匹配: 列表可以使用模式匹配来处理。常见的模式包括:

[ ] 匹配空列表。
x:xs 匹配一个非空列表,其中 x 是列表的第一个元素,xs 是剩余的列表。

myHead :: [a] -> a
myHead [] = error "空列表没有头元素"
myHead (x:xs) = x  -- x 是列表的头元素

2.3列表推导List Comprehension
也就是列表生成式:

[expression | ranges and constrait],|左侧是表达式,右侧是变量范围和约束条件。

[x^2 | x <- [1..10], x `mod` 2 == 0]
-- 结果: [4, 16, 36, 64, 100]

[x^2 | …] 表示我们想生成一个列表,其中每个元素是 x^2,即 x 的平方。

x <- [1…10] 表示 x 取值于列表 [1,2,3,…,10],即从 1 到 10 的所有整数。

x mod 2 == 0 是一个条件,它要求 x 必须是偶数(只有当 x 除以 2 的余数为 0 时,这个条件才为真)。

2.4高阶函数: 列表在 Haskell 中非常适合与高阶函数(如 map, filter, foldr, foldl 等)配合使用,提供强大的函数式编程能力:

map:对列表的每个元素应用一个函数。

map (+1) [1, 2, 3]  -- 结果是 [2, 3, 4]

filter:根据给定的条件过滤列表。

filter (>2) [1, 2, 3, 4]  -- 结果是 [3, 4]

3.常用函数

fold 函数

fold 是一类用于**归约(reduction)**列表的函数,通常有两种形式:foldl(从左折叠)和 foldr(从右折叠)。

foldl(左折叠)

逐个元素从左到右应用一个函数,最终将列表归约成一个值。
它的类型签名(类型签名理解为函数类型,即输入什么类型的参数,输出什么类型的值)是:

foldl :: (b -> a -> b) -> b -> [a] -> b

第一个参数是一个二元函数 (b -> a -> b),b 是累积的结果,a 是列表的当前元素。
第二个参数是初始值 b,用于开始累积。
第三个参数是待处理的列表 [a]。
示例:

foldl (+) 0 [1, 2, 3, 4]
-- 结果是 10 (0 + 1 + 2 + 3 + 4)

foldr(右折叠)

逐个元素从右到左应用一个函数,归约列表成一个值。
它的类型签名是:

foldr :: (a -> b -> b) -> b -> [a] -> b

第一个参数是一个二元函数 (a -> b -> b),其中 a 是列表的当前元素,b 是累积的结果。
第二个参数是初始值 b。
第三个参数是待处理的列表 [a]。
示例:

foldr (:) [] [1, 2, 3, 4]
-- 结果是 [1, 2, 3, 4] (与输入相同,因为使用了 `(:)` 连接)

foldl 和 foldr 的区别:
foldl 是从左到右进行归约,而 foldr 是从右到左。
foldr 对于惰性列表(如无限列表)更有优势,因为它能在某些情况下提前终止计算,而 foldl 则需要遍历整个列表。

foldl 和 foldr 的应用
求和:

foldl (+) 0 [1, 2, 3, 4]  -- 结果: 10
foldr (+) 0 [1, 2, 3, 4]  -- 结果: 10

列表翻转:

foldl (\acc x -> x:acc) [] [1, 2, 3, 4]  -- 结果: [4, 3, 2, 1]

take 函数

take 函数用于从一个列表的开头获取指定数量的元素。

类型签名:

take :: Int -> [a] -> [a]

第一个参数是一个整数,表示要获取的元素数量。
第二个参数是列表。
返回值是从原列表开头获取的指定数量的元素,如果数量超过列表长度,则返回整个列表。
示例:

take 3 [1, 2, 3, 4, 5]
-- 结果是 [1, 2, 3]
take 10 [1, 2, 3]
-- 结果是 [1, 2, 3],因为列表长度不足 10

用于无限列表:
由于 Haskell 的惰性求值机制,take 可以从无限列表中提取出有限个元素,而不会导致程序崩溃。

take 5 [1..]
-- 结果是 [1, 2, 3, 4, 5]

takeWhile 函数

takeWhile 函数从列表的开头开始,根据条件提取元素,直到遇到第一个不满足条件的元素为止。

类型签名:

takeWhile :: (a -> Bool) -> [a] -> [a]

第一个参数是一个条件函数 (a -> Bool),它接受列表中的每个元素,并返回一个布尔值。
第二个参数是列表。
返回值是从开头开始满足条件的连续元素组成的列表。
示例:

takeWhile (< 5) [1, 2, 3, 4, 5, 6, 7]
-- 结果是 [1, 2, 3, 4],因为 5 不满足条件,提取到 4 为止

takeWhile (/= ' ') "Hello World"
-- 结果是 "Hello",提取到空格为止

4.Tuple元组

4.1元组的基本特性
固定长度元组的长度是固定的,创建之后不能添加或移除元素。

(1, "hello") -- 一个长度为 2 的元组

异质性(Heterogeneous): 元组中的元素可以是不同类型的,允许组合不同类型的数据。

(1, "hello", True) -- 包含 Int、String 和 Bool 类型的元组

无嵌套要求: 元组可以包含元组(嵌套元组),以便组合更复杂的数据结构。

((1, “a”), (2, “b”)) – 一个嵌套的元组

不可变性: 元组中的值一旦创建后就不能修改,这与 Haskell 中的其他数据结构保持一致。

4.2元组的定义
元组的语法是将多个值用逗号隔开,并用括号包围:

(1, "hello")        -- 一个包含 Int 和 String 的 2 元组
(True, 42, 'a')     -- 一个包含 Bool、Int 和 Char 的 3 元组
(1, 2, (3, 4))      -- 嵌套元组

元组的类型由包含的元素的类型和数量确定。例如:

(Int, String) 是一个包含两个元素的元组,第一个是 Int 类型,第二个是 String 类型。
(Bool, Int, Char) 是一个包含三个元素的元组。

4.3元组的常见操作

访问元组的元素
Haskell 没有像列表那样的索引来直接访问元组的元素。相反,你可以使用内置的函数(如 fst 和 snd)访问二元组的元素,或者通过模式匹配解构元组。

fst 和 snd 函数:用于访问二元组的第一个和第二个元素。

fst (1, "hello")  -- 结果是 1
snd (1, "hello")  -- 结果是 "hello"

4.4模式匹配:通过模式匹配可以解构任意大小的元组。

let (x, y) = (1, 2) in x + y  -- 结果是 3
let (a, b, c) = (True, 42, 'a') in b  -- 结果是 42

4.5与函数的结合
元组常用于函数的参数或返回值,尤其是返回多个值时非常有用。

返回多个值:

-- 一个函数返回两个值
divMod :: Int -> Int -> (Int, Int)
divMod 10 3
-- 结果是 (3, 1)  -- 返回商和余数的二元组

函数中的模式匹配:

-- 解构元组的函数
addPair :: (Int, Int) -> Int
addPair (x, y) = x + y
addPair (3, 4)  -- 结果是 7

4.6元组与列表的区别
元组和列表在 Haskell 中有一些明显的区别:

长度:列表可以是任意长度,而元组的长度是固定的。
类型:列表中的所有元素必须是相同的类型,而元组可以包含不同类型的元素。
操作方式:列表有很多内置的处理函数(如 map、filter 等),而元组没有相同的灵活性,主要依赖模式匹配来操作。
常用元组函数
fst 和 snd:用于二元组,分别返回第一个和第二个元素。

fst (10, 20)  -- 结果是 10
snd (10, 20)  -- 结果是 20

zip 和 unzip:将两个列表合并为一个列表的二元组,或者将列表的二元组解构为两个列表。

zip [1, 2, 3] ['a', 'b', 'c']
-- 结果是 [(1,'a'), (2,'b'), (3,'c')]

unzip [(1,'a'), (2,'b'), (3,'c')]
-- 结果是 ([1, 2, 3], ['a', 'b', 'c'])

4.7元组的实际应用
函数返回多个值: Haskell 中没有类似于其他语言的多返回值功能,所以元组常用于返回多个值。

divMod 10 3  -- 结果是 (3, 1),返回商和余数

复杂数据的组合: 在处理多种类型的数据时,元组非常方便,比如在函数之间传递多个相关值时。

("Alice", 25, True)  -- 组合名称、年龄、是否已验证等信息

结合 zip 使用: 元组经常与 zip 函数结合使用,用来将两个列表合并为一个包含成对数据的列表。

zip [1, 2, 3] ['a', 'b', 'c']
-- 结果是 [(1,'a'), (2,'b'), (3,'c')]

4.8Haskell 的元组具有以下特性:

固定长度:元组长度不可变。
异质性:元组中的元素可以是不同类型。
不可变性:一旦创建,元组的内容不能改变。
灵活性:元组常用于传递和返回多个不同类型的值。
元组非常适合需要将多个值绑定在一起的场景,尤其是在函数式编程中,可以灵活地与模式匹配、zip、curry 等函数结合使用。

5.函数定义

5.1基本函数定义
Haskell 中函数的定义由函数名、参数和函数体组成。函数的类型可以显式地给出,虽然这不是强制的,但通常是一种好的习惯。

-- 函数名 参数 = 函数体
add :: Int -> Int -> Int
add x y = x + y

add 是函数名。
x 和 y 是参数。
x + y 是函数体,表示该函数返回两个参数的和。
:: 后面是函数的类型签名,Int -> Int -> Int 表示该函数接受两个 Int 参数,返回一个 Int。

5.2函数的部分应用
Haskell 的函数是柯里化的,意味着你可以将一个多参数的函数分解为多个单参数的函数。例如,add 1 会返回一个新的函数,它将一个数加上 1。
addOne = add 1
addOne 5 – 结果是 6

*5.3额外补充(可选)

在 Haskell 中,每个函数实际上只有一个参数,这种现象称为柯里化(Currying)。尽管表面上看起来 Haskell 函数可以接受多个参数,但实际上,Haskell 中的每个函数只接受一个参数,并返回一个新的函数,该新函数继续接受下一个参数。这是函数式编程中的一种重要概念。

柯里化(Currying)
柯里化的核心思想是:一个多参数的函数可以转化为一系列单参数函数的组合。换句话说,给定一个多参数函数,Haskell 会把它拆解为接受第一个参数的函数,然后返回另一个函数,这个函数再接受第二个参数,依此类推,直到所有参数都被处理完毕。

示例
假设我们有一个看似接受两个参数的函数 add,其定义如下:

add :: Int -> Int -> Int
add x y = x + y

虽然表面上看 add 接受两个参数 x 和 y,但在 Haskell 的底层,这个函数是通过柯里化的方式来实现的。更具体地讲,add 的类型签名可以理解为:

add :: Int -> (Int -> Int)

这意味着 add 是一个接受一个 Int 类型的参数,并返回另一个接受 Int 类型参数的函数,最终该函数会返回一个 Int 类型的结果。

分解过程
add x y = x + y 的实际执行过程可以分为以下步骤:

add 首先接受第一个参数 x,返回一个新函数:

add x = \y -> x + y

这里 \y -> x + y 是一个匿名函数,它接受参数 y,并返回 x + y。

这个新函数可以继续接受第二个参数 y,然后计算出最终的结果 x + y。

所以,当你调用 add 2 3 时,实际上执行的是:

add 2 3 == (\y -> 2 + y) 3 == 2 + 3 == 5

6. 条件表达式

6.1 if 表达式

if 表达式用于根据条件返回不同的结果。注意,if 在 Haskell 中总是要求提供 else 部分,因为 Haskell 的每个表达式必须有返回值。

abs :: Int -> Int
abs x = if x >= 0
        then x
        else -x

if x >= 0 是条件。
then x 是当条件为 True 时的返回值。
else -x 是当条件为 False 时的返回值。
Haskell 中的 if 语句可以嵌套,但如果条件过多,推荐使用 guard 来代替。

6.2 guard(守卫)

guard 是 Haskell 中一种强大的条件表达方式,通常与模式匹配配合使用。它的写法类似于 if,但是更加简洁和清晰,尤其当有多个条件时。

语法结构:

函数名 参数
| 条件1 = 结果1
| 条件2 = 结果2
| otherwise = 默认结果
otherwise 是 True 的别名,表示默认情况。
示例:定义绝对值函数:

abs :: Int -> Int
abs x
  | x >= 0    = x
  | otherwise = -x

另一个例子:根据学生成绩给出评价:

grade :: Int -> String
grade score
  | score >= 90 = "A"
  | score >= 80 = "B"
  | score >= 70 = "C"
  | score >= 60 = "D"
  | otherwise   = "F"

使用 guard 处理多个条件
相比 if,guard 的优势在于处理多个条件时代码更加清晰。

maxValue :: Int -> Int -> Int
maxValue x y
  | x > y     = x
  | otherwise = y

6.3模式匹配(Pattern Matching)

Haskell 支持模式匹配,它允许根据传入参数的结构进行匹配,并执行不同的代码。模式匹配不仅可以用于简单的常量匹配,还可以用于解构复杂数据结构(如列表、元组等)。

示例:递归计算阶乘。

factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)

在这个例子中,factorial 0 = 1 是一个特例(基准情况),factorial n = n * factorial (n - 1) 是递归的通用情况。

6.4列表模式匹配

列表的模式匹配是 Haskell 函数定义的常见方式,可以使用空列表 [] 和 x:xs 的方式匹配列表的第一个元素和剩余部分。

sumList :: [Int] -> Int
sumList []     = 0             -- 空列表的和是 0
sumList (x:xs) = x + sumList xs -- 非空列表的和是第一个元素加上其余元素的和

在这里,(x:xs) 代表列表的第一个元素是 x,剩下的列表是 xs。

6.5局部定义 (where 和 let)

6.5.1 where 绑定
where 可以在函数体中定义局部变量或函数,使得代码更加清晰。

bmiTell :: Double -> Double -> String
bmiTell weight height
  | bmi <= 18.5 = "Underweight"
  | bmi <= 25.0 = "Normal"
  | bmi <= 30.0 = "Overweight"
  | otherwise   = "Obese"
  where bmi = weight / height ^ 2

在这个例子中,bmi 是一个局部变量,它被定义在 where 子句中,并在上面的 guard 中使用。

6.5.2 let 绑定
let 也是用于定义局部变量的方式,不过 let 可以出现在任意地方(表达式中),而 where 只能出现在函数体的末尾。

cylinder :: Double -> Double -> Double
cylinder r h =
  let sideArea = 2 * pi * r * h
      topArea = pi * r^2
  in  sideArea + 2 * topArea

在 let … in 表达式中,let 定义局部变量,in 后面是计算结果。

6.5.3 case 表达式
case 表达式用于模式匹配,也可以用于替代 if 或 guard,但它更灵活,因为可以匹配多种模式。

describeList :: [a] -> String
describeList xs = case xs of
  []  -> "The list is empty"
  [x] -> "The list has one element"
  _   -> "The list has multiple elements"

在这个例子中,case 对列表 xs 进行模式匹配,分别处理空列表、单元素列表和多元素列表。

7.递归

递归函数有两个主要部分:

基准情况(Base Case):递归的终止条件,表示递归不再继续,返回结果。
递归情况(Recursive Case):函数调用自身,逐步缩小问题的规模,直到基准情况为止。
递归的基本例子

7.1计算阶乘

阶乘(factorial)是递归的一个经典例子:

阶乘的定义是:n! = n * (n - 1)!,并且 0! = 1。
当 n = 0 时,返回 1 作为基准情况。
否则返回 n * (n - 1)!,这就是递归情况。

factorial :: Int -> Int
factorial 0 = 1        -- 基准情况
factorial n = n * factorial (n - 1)  -- 递归情况

调用 factorial 5 时,递归展开过程如下:

factorial 5 = 5 * factorial 4
             = 5 * (4 * factorial 3)
             = 5 * (4 * (3 * factorial 2))
             = 5 * (4 * (3 * (2 * factorial 1)))
             = 5 * (4 * (3 * (2 * (1 * factorial 0))))
             = 5 * (4 * (3 * (2 * (1 * 1))))
             = 120

7.2列表求和

递归可以用于处理列表结构,例如求和一个整数列表。

sumList :: [Int] -> Int
sumList []     = 0         -- 基准情况:空列表的和是 0
sumList (x:xs) = x + sumList xs  -- 递归情况:列表的和等于第一个元素加上剩余元素的和

调用 sumList [1, 2, 3, 4] 时的递归展开过程:

sumList [1, 2, 3, 4] = 1 + sumList [2, 3, 4]
                     = 1 + (2 + sumList [3, 4])
                     = 1 + (2 + (3 + sumList [4]))
                     = 1 + (2 + (3 + (4 + sumList [])))
                     = 1 + (2 + (3 + (4 + 0)))
                     = 10

7.3递归处理列表

递归不仅可以处理数字运算,还可以操作复杂的数据结构,比如列表和树。

列表长度
可以通过递归计算列表的长度:

lengthList :: [a] -> Int
lengthList []     = 0         -- 基准情况:空列表长度为 0
lengthList (_:xs) = 1 + lengthList xs  -- 递归情况:列表长度是剩余部分长度加 1

7.4反转列表

反转列表也是递归的一个经典例子:

reverseList :: [a] -> [a]
reverseList []     = []           -- 基准情况:空列表的反转是空列表
reverseList (x:xs) = reverseList xs ++ [x]  -- 递归情况:反转剩余列表并将第一个元素放在末尾

调用 reverseList [1, 2, 3] 时:

reverseList [1, 2, 3] = reverseList [2, 3] ++ [1]
                      = (reverseList [3] ++ [2]) ++ [1]
                      = ((reverseList [] ++ [3]) ++ [2]) ++ [1]
                      = ([3] ++ [2]) ++ [1]
                      = [3, 2] ++ [1]
                      = [3, 2, 1]

7.5递归处理树

Haskell 的递归不仅适用于列表,还可以用于更复杂的递归数据结构,比如二叉树。

定义二叉树数据类型
我们可以定义一个简单的二叉树数据类型:

data Tree a = Empty | Node a (Tree a) (Tree a)

在这个定义中,Tree 是一个递归的数据结构,Node 包含一个值以及左右子树,Empty 表示空树。

树的递归遍历
例如,我们可以定义一个函数来计算二叉树中所有节点的总和:

treeSum :: Num a => Tree a -> a
treeSum Empty = 0
treeSum (Node val left right) = val + treeSum left + treeSum right

这个递归函数对于空树返回 0,否则返回节点值加上左右子树的和。

树的深度计算
我们还可以使用递归来计算树的深度:

treeDepth :: Tree a -> Int
treeDepth Empty = 0
treeDepth (Node _ left right) = 1 + max (treeDepth left) (treeDepth right)

这里,我们递归地计算左右子树的深度,然后取较大的那个,再加上根节点的深度。

8. lamda函数

Lambda 表达式主要用于需要传递函数作为参数的场景中,它简化了代码,让函数不必先有名字就可以直接使用。常见应用包括高阶函数、列表操作等。

8.1与高阶函数结合

高阶函数是接受函数作为参数或返回函数的函数。

在 map 函数中使用 lambda 表达式来对列表中的每个元素进行操作:

map (\x -> x * 2) [1, 2, 3, 4]
-- 结果是 [2, 4, 6, 8]

这个例子中,map 接受一个函数 \x -> x * 2 作为参数,它将该函数应用到列表中的每个元素上。

8.2在 filter 中使用 lambda 表达式

filter 函数用于筛选列表中的元素,lambda 表达式可以用来定义筛选条件:

filter (\x -> x `mod` 2 == 0) [1, 2, 3, 4, 5, 6]
-- 结果是 [2, 4, 6]

这里,lambda 表达式 \x -> x mod 2 == 0 用来筛选出偶数。

8.3 与 fold 函数结合

foldl 和 foldr 是 Haskell 中的高阶函数,用于对列表进行归约操作。lambda 表达式可以很方便地嵌入其中。

例如,计算列表的乘积:

foldl (\acc x -> acc * x) 1 [1, 2, 3, 4]
-- 结果是 24

在这里,lambda 表达式 \acc x -> acc * x 接受累积值 acc 和列表的当前元素 x,并返回它们的乘积。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值