Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
DaviRain-Su committed Jul 4, 2024
1 parent 6bf4f44 commit 592515e
Show file tree
Hide file tree
Showing 7 changed files with 1,067 additions and 0 deletions.
118 changes: 118 additions & 0 deletions src/ocaml_programming/ch4_beyond_lists.md
Original file line number Diff line number Diff line change
@@ -1 +1,119 @@
# Beyond Lists

<iframe width="791" height="445" src="https://www.youtube.com/embed/5Yyk-l-cUNI" title="Trees with Map and Fold | OCaml Programming | Chapter 4 Video 7" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

像 map 和 fold 这样的功能并不局限于列表。它们对几乎任何类型的数据收集都是有意义的。例如,回想一下这棵树的表示:

```
type 'a tree =
| Leaf
| Node of 'a * 'a tree * 'a tree
```

```
type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree
```

## Map on Trees

这很容易。我们只需要在每个节点上将函数 f 应用于值 v 。

```
let rec map_tree f = function
| Leaf -> Leaf
| Node (v, l, r) -> Node (f v, map_tree f l, map_tree f r)
```

```
val map_tree : ('a -> 'b) -> 'a tree -> 'b tree = <fun>
```

## Fold on Trees

这个稍微难一点。让我们为 'a tree 开发一个类似于我们在 'a list 上的 fold_right 的折叠功能。一种思考 List.fold_right 的方式是列表中的 [] 值被 acc 参数替换,每个 :: 构造函数被 f 参数的应用所替换。例如, [a; b; c] 是 a :: (b :: (c :: [])) 的语法糖。因此,如果我们用 0 替换 [] ,用 ( + ) 替换 :: ,我们得到 a + (b + (c + 0)) 。沿着这些思路,这是我们可以重写 fold_right 的一种方式,这将帮助我们更清晰地思考一下:

```
type 'a mylist =
| Nil
| Cons of 'a * 'a mylist
let rec fold_mylist f acc = function
| Nil -> acc
| Cons (h, t) -> f h (fold_mylist f acc t)
```

```
type 'a mylist = Nil | Cons of 'a * 'a mylist
```

```
val fold_mylist : ('a -> 'b -> 'b) -> 'b -> 'a mylist -> 'b = <fun>
```

算法是相同的。我们所做的只是改变了列表的定义,使用用字母字符编写的构造函数,而不是标点符号,并改变了 fold 函数的参数顺序。

对于树,我们希望将 acc 的初始值替换每个 Leaf 构造函数,就像它在列表中替换 [] 一样。我们希望每个 Node 构造函数被操作符替换。但现在操作符将需要是三元的而不是二元的 - 也就是说,它需要接受三个参数而不是两个 - 因为树节点有一个值,一个左子节点和一个右子节点,而列表的 cons 只有一个头部和一个尾部。

受到这些观察的启发,这里是树的折叠函数:

```
let rec fold_tree f acc = function
| Leaf -> acc
| Node (v, l, r) -> f v (fold_tree f acc l) (fold_tree f acc r)
```

```
val fold_tree : ('a -> 'b -> 'b -> 'b) -> 'b -> 'a tree -> 'b = <fun>
```

如果将该函数与 fold_mylist 进行比较,您会注意到它们几乎是相同的。第二个模式匹配分支中只多了一个递归调用,对应于在该类型定义中 'a tree 的一个额外出现。

我们可以使用 fold_tree 来实现我们之前看到的一些树函数:

```
let size t = fold_tree (fun _ l r -> 1 + l + r) 0 t
let depth t = fold_tree (fun _ l r -> 1 + max l r) 0 t
let preorder t = fold_tree (fun x l r -> [x] @ l @ r) [] t
```

```
val size : 'a tree -> int = <fun>
```

```
val depth : 'a tree -> int = <fun>
```

```
val preorder : 'a tree -> 'a list = <fun>
```

为什么我们选择 fold_right 而不是 fold_left 进行这个开发?因为 fold_left 是尾递归的,这是我们永远无法在二叉树上实现的。假设我们首先处理左分支;然后我们仍然必须在返回之前处理右分支。因此,在对一个分支进行递归调用后,总会有剩余的工作要做。因此,在树上, fold_right 的等价物是我们所能期望的最好的。

我们用来推导 fold_tree 的技术适用于任何 OCaml 变体类型 t :

- 编写一个递归 fold 函数,该函数接受 t 的每个构造函数的一个参数。
- 该 fold 函数与构造函数匹配,在遇到任何类型为 t 的值时会递归调用自身。
- 使用 fold 的适当参数来组合所有递归调用的结果,以及在每个构造函数中不属于 t 类型的所有数据。

这种技术构建了一种称为 catamorphism 的东西,又称为广义折叠操作。要了解更多关于 catamorphisms 的信息,请参加范畴论课程。

## Filter on Trees

这可能是最难设计的一个。问题是:如果我们决定过滤一个节点,那么我们应该怎么处理它的子节点?

- 我们可以对子节点进行递归。如果在筛选它们后只剩下一个子节点,我们可以将其提升为其父节点的位置。但如果两个子节点都保留下来,或者一个都没有呢?那么我们就必须以某种方式重塑树形结构。如果不了解树应该如何使用——也就是说,它代表什么样的数据,我们就会陷入困境。
- 相反,我们可以完全消除子节点。因此,过滤节点的决定意味着修剪以该节点为根的整个子树。

后者易于实施:

```
let rec filter_tree p = function
| Leaf -> Leaf
| Node (v, l, r) ->
if p v then Node (v, filter_tree p l, filter_tree p r) else Leaf
```

```
val filter_tree : ('a -> bool) -> 'a tree -> 'a tree = <fun>
```
63 changes: 63 additions & 0 deletions src/ocaml_programming/ch4_currying.md
Original file line number Diff line number Diff line change
@@ -1 +1,64 @@
# Currying

我们已经看到,一个接受类型为 t1 和 t2 的两个参数并返回类型为 t3 的值的 OCaml 函数具有类型 t1 -> t2 -> t3 。在 let 表达式中函数名后面我们使用两个变量:

```
let add x y = x + y
```

```
val add : int -> int -> int = <fun>
```

定义一个接受两个参数的函数的另一种方法是编写一个接受元组的函数:

```
let add' t = fst t + snd t
```

```
val add' : int * int -> int = <fun>
```

不使用 fst 和 snd ,我们可以在函数定义中使用元组模式,从而导致第三种实现:

```
let add'' (x, y) = x + y
```

```
val add'' : int * int -> int = <fun>
```

使用第一种风格编写的函数(带有类型 t1 -> t2 -> t3 )称为柯里化函数,而使用第二种风格(带有类型 t1 * t2 -> t3 )的函数称为非柯里化函数。比喻地说,柯里化函数更“辛辣”,因为您可以部分应用它们(这是您无法对非柯里化函数做到的:您无法传入一对的一半)。实际上,“柯里”一词并不是指香料,而是指逻辑学家 [Haskell Curry](https://en.wikipedia.org/wiki/Haskell_Curry)(他是极少数以名字和姓氏命名编程语言的人之一)。


有时候你会遇到一些库提供了一个非柯里化版本的函数,但你想要一个柯里化版本来在自己的代码中使用;或者反过来。因此,了解如何在这两种函数之间进行转换是很有用的,就像我们在上面使用 add 时所做的那样。

您甚至可以编写一对高阶函数来为您进行转换:

```
let curry f x y = f (x, y)
let uncurry f (x, y) = f x y
```

```
val curry : ('a * 'b -> 'c) -> 'a -> 'b -> 'c = <fun>
```

```
val uncurry : ('a -> 'b -> 'c) -> 'a * 'b -> 'c = <fun>
```

```
let uncurried_add = uncurry add
let curried_add = curry add''
```

```
val uncurried_add : int * int -> int = <fun>
```

```
val curried_add : int -> int -> int = <fun>
```
125 changes: 125 additions & 0 deletions src/ocaml_programming/ch4_exercises.md
Original file line number Diff line number Diff line change
@@ -1 +1,126 @@
# Exercises

大多数练习的解决方案都可以[获得](https://github.com/cs3110/textbook-solutions)。2022 年秋季是这些解决方案的首次公开发布。尽管康奈尔大学的学生已经可以获得这些解决方案几年了,但更广泛的传播将揭示可以进行改进的地方是不可避免的。我们很乐意添加或更正解决方案。请通过 GitHub 进行贡献。


## Exercise: twice, no arguments []

考虑以下定义:

```
let double x = 2 * x
let square x = x * x
let twice f x = f (f x)
let quad = twice double
let fourth = twice square
```

使用 toplevel 确定 quad 和 fourth 的类型。解释为什么 quad 不是以语法形式编写为接受参数的函数,但其类型显示它实际上是一个函数。

## Exercise: mystery operator 1 [★★]

以下运算符是做什么用的?

```
let ( $ ) f x = f x
```

提示:调查 square $ 2 + 2 与 square 2 + 2 之间的差异。

## Exercise: mystery operator 2 [★★]

以下运算符是做什么用的?

```
let ( @@ ) f g x = x |> g |> f
```

提示:研究 String.length @@ string_of_int 应用于 1 , 10 , 100 等。

## Exercise: repeat [★★]

将 twice 概括为一个函数 repeat ,使得 repeat f n x 将 f 应用于 x 共 n 次。也就是说,

- repeat f 0 x yields x
- repeat f 1 x yields f x
- repeat f 2 x yields f (f x) (which is the same as twice f x)
- repeat f 3 x yields f (f (f x))
- and so on.

## Exercise: product []

使用 fold_left 编写一个函数 product_left ,用于计算一组浮点数的乘积。空列表的乘积是 1.0 。提示:回想一下我们在讲座中如何用一行代码实现 sum 。

使用 fold_right 编写一个函数 product_right ,用于计算一个浮点数列表的乘积。相同的提示适用。

## Exercise: terse product [★★]

你能将对产品练习的解决方案简化到多少?提示:每个解决方案只需要一行代码,且不需要使用 fun 关键字。对于 fold_left ,你的函数定义甚至不需要显式地接受一个列表参数。如果你使用 ListLabels ,对于 fold_right 也是同样的情况。

## Exercise: sum_cube_odd [★★]

编写一个函数 sum_cube_odd n ,计算介于 0 和 n 之间(包括 0 和 n )所有奇数的立方和。不要编写任何新的递归函数。而是使用函数式编程中的 map、fold 和 filter,以及管道化讨论中定义的 ( -- ) 运算符。

## Exercise: sum_cube_odd pipeline [★★]

重写函数 sum_cube_odd 以使用管道运算符 |> 。

## Exercise: exists [★★]

考虑编写一个函数 exists: ('a -> bool) -> 'a list -> bool ,使得 exists p [a1; ...; an] 返回列表中至少有一个元素满足谓词 p 的情况。也就是说,它的评估结果与 (p a1) || (p a2) || ... || (p an) 相同。当应用于空列表时,它的评估结果为 false 。

写出三个解决这个问题的方法,就像我们之前所做的那样:

- exists_rec 必须是一个递归函数,不能使用 List 模块
- exists_fold ,使用 List.fold_left 或 List.fold_right ,但不使用任何其他 List 模块函数,也不使用 rec 关键字
- exists_lib ,使用除 fold_left 或 fold_right 之外的任何 List 模块函数的组合,并且不使用 rec 关键字。


## Exercise: account balance [★★★]

编写一个函数,给定一个代表借方的数字列表,从账户余额中扣除它们,最后返回余额中剩余的金额。编写三个版本: fold_left , fold_right ,以及一个直接的递归实现。

## Exercise: library uncurried [★★]

这是 List.nth 的非柯里化版本:

```
let uncurried_nth (lst, n) = List.nth lst n
```

以类似的方式,编写这些库函数的非柯里化版本:

- List.append
- Char.compare
- Stdlib.max

## Exercise: map composition [★★★]

展示如何用一个调用 List.map 仅一次的等效表达式替换任何形式为 List.map f (List.map g lst) 的表达式。

## Exercise: more list fun [★★★]

编写执行以下计算的函数。 您编写的每个函数都应该使用 List.fold , List.map 或 List.filter 中的一个。 要选择使用哪个,请考虑计算正在做什么:组合、转换或过滤元素。

- 找出字符串列表中长度严格大于 3 的元素。
- 将 1.0 添加到浮点数列表的每个元素。
- 给定一个字符串列表 strs 和另一个字符串 sep ,生成包含 strs 的每个元素以 sep 分隔的字符串。例如,给定输入 ["hi";"bye"] 和 "," ,生成 "hi,bye" ,确保不会在结果字符串的开头或结尾产生额外的逗号。

## Exercise: association list keys [★★★]

回想一下,关联列表是一种字典的实现,它是一对列表,我们将每对中的第一个组件视为键,第二个组件视为值。

编写一个函数 keys: ('a * 'b) list -> 'a list ,该函数返回一个关联列表中唯一键的列表。由于它们必须是唯一的,在输出列表中不应出现多次相同的值。输出值的顺序不重要。您能使解决方案多么紧凑和高效?您能在一行中以线性对数空间和时间完成吗?提示: List.sort_uniq 。

## Exercise: valid matrix [★★★]

数学矩阵可以用列表表示。在行主表示中,这个矩阵

[ [1; 1; 1]; [9; 8; 7]]

将其表示为列表 [[1; 1; 1]; [9; 8; 7]] 。让我们将行向量表示为 int list 。例如, [9; 8; 7] 是一个行向量。

一个有效的矩阵是一个至少有一行、至少有一列,并且每列具有相同行数的 int list list 。有许多 int list list 类型的无效值,例如,

- []
- [[1; 2]; [3]]
Loading

0 comments on commit 592515e

Please sign in to comment.