这次来说说函数式的数据结构是什么样子的,本章会先用一个list来举例子说明,最后给出一个Tree数据结构的练习,放在公众号里面,练习里面给出了基本的结构,但代码是空缺的需要补上,此外还有预留的testcase可以验证。

关注公众号:哈尔的数据城堡,回复“函数式数据结构”可以获得。(写文章不容易,大哥大姐关注下吧[哭笑])

然后是这系列的索引:

Scala函数式编程指南(一) 函数式思想介绍

scala函数式编程(二) scala基础语法介绍

Scala函数式编程(三) scala集合和函数

1.什么是函数式的数据结构

还记得前面说过,函数式编程最大的特点是什么吗?就是没有副作用。那么函数式的数据结构自然也是如此。

无副作用的关键是:

  1. 一个函数无论调用多少次,只要输入参数相同,则结果也必然相同。
  2. 且这个函数执行过程中不会改变程序的任何外部状态,如全局变量,对象的属性等。
  3. 函数的结果也不依赖外部状态。

在java中,最经典的数据结构ArrayList,是通过一个全局的size变量,来控制ArrayList的大小的,这就说明ArrayList并非无副作用。

在scala中,集合(List,Map等)默认是不可变的,以链表List为例,是无法通过push等操作,往一个链表里面添加内容的。只能通过两个链表相加的方式,生成一个新链表(Map也是一样,通过两个Map相加,Key相同的会覆盖,以达到更新的目的)。这点倒是和String有点像。

不过其实这样有一个问题,那就是很耗费内存。但这个问题可以用懒加载来解决,限于篇幅,后面再介绍吧。

总结一下,函数式的数据结构,最大的特点,就是没有副作用。那么如何实现无副作用的数据结构呢,我们下面用链表的例子来展示。

不过在这之前,需要先回顾下一些语法知识。

2.scala知识回顾

我的一个观点是,语言的语法知识如果只是看,背,而没有实际用到,那是比较难记住的。这里就把这次会用到的语法知识做个简单介绍,如果有需要,可以查阅前面写的前两章。

我这里也有演示如果运用前面介绍的语法知识实现一个函数式的List()。

PS:如果不想看语法知识可以直接跳到第三节。

前面的语法索引:

scala函数式编程(二) scala基础语法介绍

Scala函数式编程(三) scala集合和函数

2.1 scala的模式匹配

模式匹配类似于swtch语法,不过它能匹配的不止是值,还有数据类型。同时,它是一个匿名函数,在scala里,函数不用return,能直接返回值。

  1. val times = 1
  2. //使用模式匹配来匹配值
  3. times match {
  4. case 1 => "one"
  5. case 2 => "two"
  6. case _ => "some other number"
  7. }
  8. //使用模式匹配,匹配类型,再判断值
  9. times match {
  10. case i:Int if i == 1 => "one"
  11. case i:Int if i == 2 => "two"
  12. case _ => "some other number"
  13. }

如果有小伙伴想了解更多,可以看看我这篇,scala模式匹配详细解析

2.2 object和apply

前面介绍到,object是一个类的伴生对象,而且相当于static类,内存里只能有一个对象。apply方法则是说,可以在使用object对象的时候,直接默认使用。别说了,看代码:

  1. scala> class Foo {}
  2. defined class Foo
  3. //有一个apply方法
  4. scala> object FooMaker {
  5. | def apply() = new Foo
  6. | }
  7. defined module FooMaker
  8. //新建object,自动得就调用了apply
  9. scala> val newFoo = FooMaker() //赋值的对象是Foo,因为调用了FooMaker()的apply
  10. newFoo: Foo = Foo@5b83f762

上面的代码,FooMaker相当于一个工厂。

2.3 scala的泛型

scala中的泛型,叫做型变或变性,英文叫variance。主要有三种情况:

假设Dog是Animal的子类。那么有如下关系:

  • 协变(covariant):List[Dog]是List[Animal]的子类,形态用一个+号表示,即List[+A](这里的A是泛指,类似java中的泛型,可以随便指定一个字母)。
  • 逆变(contravariant):与协变相反,List[Animal]是List[Dog]的子类,形态用一个-号表示,即List[-A]。
  • 不变(invariant):List[Dog]是List[Animal]的无关,不用任何表示,List[A]。

协变是比较符合正常逻辑思考的,一群狗确实也可以说是一群动物。逆变就比较反直觉了,不过这里先不讨论这点,后面有机会再讨论。

3.构建函数式的List

OK,有了上面的基础,就能够来构建一个函数式的数据结构了,不过在此之前,先让我们回顾下传统的List数据结构。

3.1 传统的List

还记得以前数据结构是怎样设计的吗?

最普通的链表,通常都是由一个又一个的Node组成,一个Node中存储数据和下一个链表的变量。最后通过一个空值结尾,通常是Null。

在Java中,它的链表Linklist,是通过一个全局变量size来控制链表的。

通过for循环实现基础的增删查改等操作。而是,也是传统List的常见写法,但在函数式的List中可不能这样。还记得吗,函数式最大的特点就是无副作用。像java这里用一个全局的size来控制,那可真是万万不可啊,在多线程的情况下还不得崩溃。

关于为什么要写无副作用的代码,这里就不做探讨,详细内容可以看看这个系列的第一章。Scala函数式编程指南(一) 函数式思想介绍

3.2 scala实现函数式的List

我们要做的是写出无副作用的集合,那要怎么做呢?给5秒钟闭上眼睛好好想一想有没有什么思路。。。

可能有的同学会想得到,这个答案就是递归。通过递归,能够避免副作用的产生。如常用的增删查改,如果使用递归,就可以避免使用一个全局变量,当然递归通常都没有直接使用for循环那么直观,所以充满递归的代码初次看会比较晦涩。但如果用多了,你会发现其实函数式的代码,也是非常好懂的。

下面,我们来看看如果使用递归实现一个List。

3.1 定义基本的类型

首先,我们要定义每个节点Node的类型,以及结尾Nil。由于使用到了递归,我们需要让Node和Nil都有同样的父类,因为递归函数的返回都是一样的。

如果还是不明白为什么要让Node和Nil为啥要有同样的父类,那不妨先放一放,继续看下去吧。

  1. //定义自己的特质(相当于java的接口),泛型使用协变
  2. sealed trait List[+A]
  3. //定义一个case类,作为每一个List的结尾
  4. case object Nil extends List[Nothing]
  5. //定义List子类,也可以说是List中的每个Node,每个List都是由一个又一个的Cons组成,以Nil结尾
  6. case class Cons[+A](head: A, tail: List[A]) extends List[A]

注意第一行定义了List[+A]的特质,和scala集合中的List是区分开来的,只是名字叫一样而已。这个是我们自己的List!!

而后定义了Nil和Cons,分别作为List的结尾和Node节点,注意case class也是scala的语法糖,可以理解为java bean。

之所以先定义了一个List的特质(接口),再分别用Nil和Cons继承它,是因为在递归的情况下,要让节点和结尾保持同一类型,而这个就是通过多态实现的。

3.2 实现List工厂

前面说到,通常是用object来作为工厂,这里也是一样的,我们可以定义List工厂。

定义工厂方法如下:

  1. object List {
  2. //使用可变长度,如果传进来的参数是空,就返回Nil,否则使用递归返回Cons,注意,这里的apply方法就是使用了递归
  3. def apply[A](as: A*): List[A] = // Variadic function syntax
  4. if (as.isEmpty) Nil
  5. else Cons(as.head, apply(as.tail: _*))
  6. }

这里的apply[A](as: A),括号里面的A的意思,是多个参数的意思,就是说可以有很多个参数,是scala的一个语法糖。

在最后

else Cons(as.head, apply(as.tail: _*))

看到最后面的 _*了吗,这个的意思,是除了第一个参数以外的其他参数,也是语法糖。

在这一个小小的地方就用到了递归,不断调用apply方法去解析后面的参数,最终生成一个List。初次看可能会比较迷,可能放在编译器里面运行一下,方便理解。而这种操作在scala函数式编程中,是非常普遍的做法。

至此,我们就建立了一个List的数据结构,先来看看我们的成果

  1. //一个递归的List
  2. scala> List(1,2,3)
  3. res0: List[Int] = Cons(1,Cons(2,Cons(3,Nil)))

现在的List数据结构只是初具雏形,我们还得往里面加方法。

3.3 用函数式的方式实现List更多方法

通常来说,数据结构比较重要的是增删查改等操作,但因为是不可变的,同时函数式中通常是不改变对象信息的,所以这些基本操作反而不是首要的。

我们先来看一个简单些的例子吧,让一个List[Int]中的数据累加。

  1. object List {
  2. ......
  3. //传入参数是一个Int类型的List,使用模式匹配
  4. def sum(ints: List[Int]): Int = ints match {
  5. case Nil => 0
  6. //使用递归累加
  7. case Cons(x,xs) => x + sum(xs)
  8. }
  9. ......
  10. }

这里主要传入的参数是一个Int类型的List,然后使用模式匹配,如果是结尾,则返回0,如果是中间节点,则使用递归累加。

上面那个例子比较简单,明白后可以来看看如何为List构建更加通用的方法。通常比较常用的是前面介绍过的诸如map,filter等操作,下面先用一个map来说明一下吧。

  1. object List {
  2. ......
  3. //Map操作,使用模式匹配
  4. def map[A,B](list: List[A],f: A => B): List[B] =list match {
  5. case Nil Nil
  6. //使用递归
  7. case Cons(head, tail) Cons(f(head), map(tail,f))
  8. }
  9. ......
  10. }

map函数,需要传进入一个待处理的list,以及一个函数作为参数,用以对List中每个元素做处理。

比如说想让List中每个元素+1,那就可以传入

val addOne = (num:Int) => num+1

还记得之前说,在scala中,函数也能当作变量嘛。将addOne这个函数作为参数,这样就会让List中每个元素都+1,然后返回一个新的List,当然,这个也是用递归实现的。

实现代码看起来很简洁,也是用模式匹配,匹配每个元素的类型,就是是Node还是结尾。如果是结尾,直接返回,如果是Node,那么处理完当前数据,递归去处理后面的数据,并返回新的处理后的Node。

熟悉以后,会发现这样的处理方式看着很舒服,代码写得也很少,非常简洁。

在我看来,这就是递归的魅力所在。

除了map之外,还有其他操作处理,包括filter,foldLeft,reduce等操作。我把代码放在我的公众号中,限于篇幅这里就不讲太多。关注公众号:哈尔的数据城堡,回复“函数式数据结”可以获得。

代码中使用了隐式转换来扩充List的操作,并演示了如何使用隐式转换,以及如何使用复用来组合功能以实现新的功能。有同学可能不明白为什么简单的List要搞这么复杂,看了代码可能会更加理解。

4.函数式的二叉搜索树

这部分我是作为练习的,连同List代码放在一块,里面有基本的结构,但一些缺失的内容需要你来补充。相信我,做了一遍,肯定能够对函数式的数据结构有更深的理解。

对了,二叉搜索树的练习还有几个test case,做完跑一遍了,如果全过那基本上你写的代码就不会有太大的问题,good luck~

再说一遍我把练习的代码放在了我的公众号中,关注公众号:哈尔的数据城堡,回复“函数式数据结构”就能免费获得啦。

下一篇会再针对List和Tree的代码来讲一讲,有不明白的地方到时候也可以看看。

以上~~


推荐阅读:

通俗得说线性回归算法(一)线性回归初步介绍

通俗得说线性回归算法(二)线性回归初步介绍

大数据存储的进化史 --从 RAID 到 Hadoop Hdfs

C,java,Python,这些名字背后的江湖!

Scala函数式编程(四)函数式的数据结构 上的更多相关文章

  1. Scala 函数式编程思想

    Spark 选择 Scala 作为开发语言 在 Spark 诞生之初,就有人诟病为什么 AMP 实验室选了一个如此小众的语言 - Scala,很多人还将原因归结为学院派的高冷,但后来事实证明,选择 S ...

  2. Scala函数与函数式编程

    函数是scala的重要组成部分, 本文将探讨scala中函数的应用. scala作为支持函数式编程的语言, scala可以将函数作为对象即所谓"函数是一等公民". 函数定义 sca ...

  3. scala函数式编程(二) scala基础语法介绍

    上次我们介绍了函数式编程的好处,并使用scala写了一个小小的例子帮助大家理解,从这里开始我将真正开始介绍scala编程的一些内容. 这里会先重点介绍scala的一些语法.当然,这里是假设你有一些ja ...

  4. paip.函数式编程方法概述以及总结

    paip.函数式编程方法概述以及总结 1     函数式编程:函数式风格..很多命令式语言里支持函数式编程风格 1.1      起源 (图灵机,Lisp机器, 神经网络计算机) 1.2      函 ...

  5. 【大前端攻城狮之路】JavaScript函数式编程

    转眼之间已入五月,自己毕业也马上有三年了.大学计算机系的同学大多都在北京混迹,大家为了升职加薪,娶媳妇买房,熬夜加班跟上线,出差pk脑残客户.同学聚会时有不少兄弟已经体重飙升,开始关注13号地铁线上铺 ...

  6. Java中的函数式编程(六)流Stream基础

    写在前面 如果说函数式接口和lambda表达式是Java中函数式编程的基石,那么stream就是在基石上的最富丽堂皇的大厦. 只有熟悉了stream,你才能说熟悉了Java 的函数式编程. 本文主要介 ...

  7. (转)现代C++函数式编程

    本文转自:http://geek.csdn.net/news/detail/96636     现代C++函数式编程 C++ 函数式编程 pipeline 开发经验 柯里化 阅读2127    作者简 ...

  8. javascript 函数式编程

    编程范式 编程范式是一个由思考问题以及实现问题愿景的工具组成的框架.很多现代语言都是聚范式(或者说多重范式): 他们支持很多不同的编程范式,比如面向对象,元程序设计,泛函,面向过程,等等. 函数式编程 ...

  9. swift 之函数式编程(一)

    1. 什么是函数式编程? 函数式编程是阿隆佐思想的在现实世界中的实现, 它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及异变物件. 函数式编程的最重要基础是λ演算.而且λ演算的函數可以接受函 ...

  10. 拥抱函数式编程 I - 基本概念

    函数编程与命令性编程 为支持使用纯函数方法解决问题,特此创建了函数编程范例. 函数编程是一种声明性编程形式.相比之下,大多数主流语言,包括面向对象的编程 (OOP) 语言(如 C#.Visual Ba ...

随机推荐

  1. Solr入门(一)

    一丶Solr入门1.Solr的启动Solr各版本下载老版本的时候,需要将war包放到tomcat中,现在只需解压,由于自带jetty容器,可以直接启动 [root@aaa bin]# ./solr s ...

  2. WordCloud安装

    1,下载 https://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud 2,安装 (window环境安装) 找的下载文件的路径 安装 1 pip inst ...

  3. Apache Solr Velocity模板远程代码执行复现

    0x01漏洞描述 2019年10月31日,国外安全研究员s00py在Github公开了一个Apache Solr Velocity模板注入远程命令执行的poc. 经过研究,发现该0day漏洞真实有效并 ...

  4. 学习笔记55_Nhibernate

    另一种ORM框架 1.添加各种dll 2.添加配置信息,根据文档直接复制粘贴.config //一般下载Nhibernate-3.0.0.Alpha2-bin包,会有Configuration_Tem ...

  5. [考试反思]0815NOIP模拟测试22

    40分,15名. 1-4:120 75 70 70 35分20名...总之差距极小不想说了 昨天教练说:以后的考试还是联赛知识点,但是难度比联赛高. 没听进去,以为是对于所有人而言的,也就是T1难度变 ...

  6. 通俗易懂了解Vue的计算属性

    1.前言 之前在学习vue的过程中,一直没有搞明白计算属性是个怎么回事,以及为什么要有计算属性,使用计算属性有什么好处.今天花时间翻了翻官方文档,才搞清楚其中一二,现将学习心得总结记录如下. 2.为什 ...

  7. day 2 DP专场

    上午讲了一上午背包,从01背包到完全背包到多重背包,感觉也没说什么,旁边的大佬一直在飞鸽里说让老师讲快点,然而最后也没人敢跟老师说.... 例题真的各个都是神仙题, 挂饰 好像就是一上午最简单的... ...

  8. Apache Flink任意Jar包上传导致远程代码执行漏洞复现

    0x00 简介 Apache Flink是由Apache软件基金会开发的开源流处理框架,其核心是用Java和Scala编写的分布式流数据流引擎.Flink以数据并行和流水线方式执行任意流数据程序,Fl ...

  9. css3软键盘不盖住输入框的方法

    css3软键盘不盖住输入框的方法 弹出软键盘的时候 最外面的容器高度就发生了变化 要减去软键盘高度了<pre>var bodyheight bodyheight = $('body').h ...

  10. SAP HCM 评估路径

    一.评估路径的配置方法: 1)IMG菜单路径:人事管理-〉组织管理-〉基本设置-〉维护评估路径:   2)首先定义评估路径的名称和描述,客户自定义的评估路径的名称编码可以采用字母数字编码,最大长度是八 ...