在学习了这么多函数式编程的概念之后,我们现在可以更进一步,让我们去重构一些我们的知识观点。这一次,我们将重新来看待一下数据结构这一在我们编程学习中中非常重要概念。
我们能将函数式编程的思想融入数据结构吗?
要想使用函数式编程的特性,我们需要回顾第一讲中所提到的内容,函数式编程中最重要的两个法则,不可变性与表达式。想办法遵守这些规则后,我们就能将传统的数据结构重新修改的得到属于我们新的数据结构。
在上一期的内容中,我们特别介绍了有关于C#当中有关于不可变数据类型的内容。
【C#探索】C#中三种只读/不可变集合有什么用?-优快云博客
其中Immutable系列的类型,就是C#官方提供的函数式集合数据类型。其重要的特性与我们所需要的法则的完全一致,一旦声明,该数据的内容就不会再发生改变,是一个确定的数据,满足不可变的规则。如果试图对其修改,不会修改原有的数据,而是返回一个全新的集合,其内部数据会尽可能共用原有数据,满足表达式规则。
那么接下来我们来看一些具体的实现两个例子
链表
这是最经典的数据结构,也是一个非常容易函数式化的数据结构。
最早接触的时候,我们认为的链表就是一个一个节点,每个节点拥有其数据和指向下一个节点的指针。

不过其实还有另一种理解方式

不需要把链表看成多个元素的串联,而是将其看成一个类型,其由一个元素,和另一个链表类型组成。
这样我们就可以写出
定义类型List<T>为链表类型,其有两个可能的取值。
Empty: 代表空链表
Cons: 代表由两个值组成的的非空链表,两个元素分别为
- head,链表的第一个元素,其类型为T
- tail, 链表剩余所有元素,其类型为List<T>
用我们之前的可区分联合来定义的话就是
type List<'T> =
| Empty
| Cons of 'T * List<'T>
如果使用C#定义的话
interface List<T>;
record Empty<T>: List<T>;
record Cons<T>(T Head, List<T> Tail): List<T>;
可以看到的是,链表递归的定义了自己,这在函数式编程中非常常见的做法之一,递归能在很多时候发挥出意想不到的作用。
借由这个定义,我们便就可以定义出任意长度的链表!
再来看一下我们的定义
其一,不可变性
这不仅意味着链表的元素应该是不可变的,链表本身也应该是不可变的。
如果你是第一次看到函数式编程相关的内容,可能会疑惑,链表不可变那还有意义吗?
其二,表达式
在函数式编程中,不变的其实意味是确定性,并不是完全无法改变。我们当然可以声明一个全新的变量(其实不是变量)基于原有链表变化而来的一个全新的链表, 由于其元素是不可变的,当然可以共享大多数元素。就和表达式一样,旧的输入,新的输出
例如如果我们希望在列表首部添加一个元素,我们只需要新建一个Cons,其元素是想添加的元素,而其尾部则是我们的旧列表。这样就完成了我们的修改。
如果想更新第三个元素,与上面一样,依然创建一个新的节点,将其tail配置为原第三个的后续的元素。之后,创建第二个节点的副本,将其tail设置为新的第三元素,以此类推。
可以想象到的是,如果修改的是最后的一个元素,或是在末尾添加一个元素,这都将会是o(n)的操作。
所以在函数式的链表当中, 事实上我们不期望在列表之后进行添加操作,更期望是从头部的递归操作(即不断处理head以及递归处理tail)。
综上所述,我们也能发现函数式编程数据结构的特点,更擅长与处理多读少写的场景,并且可以轻易的回溯至任意一个状态!(当然,也继承了函数式通用的高级抽象数据处理能力)
二叉树
树的定义本身其实就更贴近递归定义,相对列表来说,可能还更容易理解其心智。从我们一开始在学习二叉树的时候,就在说一个节点其有左右子树和节点数据 三个部分构成, 根据这个定义,我们很容易就能定义出结构
Tree<T>
Empty: 代表空节点
Nodes: 代表由左子树,右子树,数据构成的节点,其中
- left,节点的左子树
- right,节点的右子树
- value, 节点的数据
用F#定义
type Tree<'T> =
| Empty
| Node of 'T * Tree<'T> * Tree<'T>
用C#定义的话则是
interface Tree<T>;
record Empty<T>: Tree<T>;
record Node<T>(T Value, Tree<T> Left, Tree<T> Right): Tree<T>;
与列表同样,递归的定义了自己,当左右子树均为空的时候,那就是叶子节点了。
对于不可变字典(ImmutableDictionary)内部实现来说,其内部就用了一些特殊的树的结构,用于提升数据结构性能。
因为其函数式的方式实现,我们还能获得一个能力
看看我们现在类型定义的形式
List<T>, Tree<T>
这是不是与我们所说的高级值很像?如果我们对类型实现了单子或者函子的函数,那这些类型本质和其他高级值也可以实现类似的效果(对于多个值的集合,Bind可能会产生类似数组平铺的效果)。只是一类值的高级抽象而已。当然对于树这种结构来说,要实现高效的操作也是一门学问,在这里我们不对其继续讨论。
高效的数据结构实现一向也是一个挑战,函数式的数据结构因其不会就地更新,会产生一点性能损失。但因其没有资源竞争等优势也会带来一定的性能优化,在适合的场景使用函数式数据结构,可以带来非常多的优势。
有关于Tree的Map函数,以及Tree的Insert(添加一个值)函数,有兴趣的读者,可以独立实现一次,体验一下函数式数据结构的实现方式。
那么今天的内容先到这里
学会了吗?学会了
微信公众号: @scixing的炼丹房
Bilibili: @无聊的年 【函数式编程】【C#/F#】(七) 数据结构_哔哩哔哩_bilibili
https://www.bilibili.com/video/BV1UQnhz6Eo6/?spm_id_from=333.1387.upload.video_card.click


1万+

被折叠的 条评论
为什么被折叠?



