Scala函数式编程(四)函数式的数据结构 下

前情提要html

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

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

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

Scala函数式编程(四)函数式的数据结构 上编程

1.List代码解析

今天介绍的内容,主要是对上一篇介绍的scala函数式数据结构补充,主要讲代码。能够先看看上一节,主要讲的是函数式的list,Scala函数式编程(四)函数式的数据结构 上。这些代码我都放在个人公众号里面,包括函数式的List以及一个函数式的二叉搜索树,关注公众号:哈尔的数据城堡,回复“scala树数据结构”就能直接得到(写文章不容易,大哥大姐关注下吧 :) )。数据结构

话说回来,上一篇中,主要介绍了List的一些基础用法,包括定义基础的结构,节点Cons和结尾的Nil。以及使用一个object List来定义基础的List操做。app

//定义List为特质,Nil和Cons为结尾和中间的Node
sealed trait List[+A]

case object Nil extends List[Nothing]

case class Cons[+A](head: A, tail: List[A]) extends List[A] {
  override def toString: String = s"$head :: ${tail.toString}"
}


//Listc操做的定义方法,object至关于java中的静态类,里面的方法能够直接调用
object List {

  def sum(ints: List[Int]): Int = ints match {
    case Nil => 0
    case Cons(x,xs) => x + sum(xs)
  }

  def map[A,B](l: List[A],f: A => B): List[B] =l match {
    case Nil              => Nil
    case Cons(head, tail) =>Cons(f(head), map(tail,f))
  }
  def apply[A](a: A*): List[A] =
    if (a.isEmpty) Nil
    else Cons(a.head, apply(a.tail: _*))

  def empty[A]: List[A] = Nil


  object ops {
    //定义隐式转换,这个是为了扩充List的操做而准备的,能够看看最下面是若是使用的
    implicit def listOps[A](list: List[A]): ListOps[A] = new ListOps(list)
  }
}

关于节点Cons和Nil的定义和上一节同样,只是Cons多了个重写的toString方法。ide

简单再说下,这里呢,在object List里面,在里面咱们定义了apply方法,能够初始化生成一个List。以及上一节提到的sum和map方法。若是对这些看不明白能够看看上一节的内容。函数式编程

但这样的话当咱们要调用sum方法的时候,只能经过object List来调用,相似下面这样:函数

//使用object List里面的apply方法初始化,生成List
scala> val numList = List(1,2,3,4)
numList: List[Int] = 1 :: 2 :: 3 :: 4 :: Nil

//使用object List里面的sum方法
scala> List.sum(numList)
res0: Int = 10

可是呢,咱们平常使用的时候可不是这样呀,咱们更熟悉的应该是要这样:

//使用object List里面的apply方法初始化,生成List
scala> val numList = List(1,2,3,4)
numList: List[Int] = 1 :: 2 :: 3 :: 4 :: Nil

//直接使用numList内置的方法来处理
scala> numList.sum()
res0: Int = 10

更加通用的作法,应该是经过List自己,来调用方法,就像上面看到的那样。一般的作法,是直接加在Cons里面,但因为Cons是继承自trait List[+A],因此你们(包括)Nil里面都须要定义那一堆方法了,有没有别的办法呢?

有的,scala的又一个语法糖,隐式转换,就是上面object List里面的ops。

object ops {
    //定义隐式转换,这个是为了扩充List的操做而准备的,能够看看最下面是若是使用的
    implicit def listOps[A](list: List[A]): ListOps[A] = new ListOps(list)
  }

隐式转换主要是经过implicit这个关键字定义的,固然隐式转换还有其余用法,无论这里的用法算是最多见的用法了。

隐式转换函数,看的主要是参数,以及返回,函数名字(这里名字是listOps)是不重要的,起什么都不要紧。

隐式转换的做用这里很少解释,能够百度看看,简单说就是在须要的时候,将一个类型转换成另外一种类型。这里的做用,是在特定的状况下将咱们定义的List转成ListOps类型,而ListOps类,则在下面给出。

//扩充List的操做
private[list] final class ListOps[A](list: List[A]) {
//导入隐式转换函数,由于下面的处理也是须要隐式转换
  import List.ops._

  //使用递归实现,foldRight的实现就是调用了这个函数,这么作是为了复用
  //代码复用是函数式中很重要的一个特性,看下面append方法就能够明白
  def foldRightAsPrimary[B](z: B)(f: (A, B) => B): B = list match {
    case Nil              => z
    case Cons(head, tail) => f(head, tail.foldRightAsPrimary(z)(f))
  }

  def foldRight[B](z: B)(f: (A, B) => B): B = foldRightViaFoldLeft(z)(f)

  def map[B](f: A=> B): List[B] = list match {
    case Nil              => Nil
    case Cons(head, tail) => Cons(f(head), tail.map(f))
  }

}

有了这段代码后,当咱们须要使用map的时候,就能够不用再借助object List代劳,而能够直接使用,就像这样:

//使用object List里面的apply方法初始化,生成List
scala> val numList = List(1,2,3,4)
numList: List[Int] = 1 :: 2 :: 3 :: 4 :: Nil

//直接使用numList内置的方法来处理,而不是List.map(numList,function)
scala> numList.map(function)

当代码检测到List调用map方法,但List内部并无map方法,就会触发隐式转换,转换成ListOps类型,调用ListOps类型里面的map方法,而后返回一个List做为结果。虽然通过了诸多波折,但调用者是感觉不到的,反而感受就像是List里面自己的map方法同样。在Spark里面就有不少这样的操做。

如上面的代码,如今咱们能够直接使用numList.map(function)这样的方式,就像List里面自己就有map函数同样来使用了。

2.二叉搜索树

在上一篇末尾,给出了一份还未完成的数据结构,二叉搜索树看成练习。这一节就来说讲这个。

其实若是把以前的List都看懂的话,其实二叉搜索树并无什么难点。

二叉搜索树,是树,天然就有叶节点和叶子节点(就是末尾)。不过此次和List不同的是,没有使用隐式转换,因此咱们定义的就不是特质了,而是先定义一个抽象类。而后让叶节点和叶子节点继承它。

//定义一个二叉树的抽象类
  sealed abstract class TreeMap[+A] extends AbstractMap[Int, A] {

    def add[B >: A](kv: (Int, B)): TreeMap[B] = ???
    def deleteMin: ((Int, A), TreeMap[A]) = ???
    def delete(key: Int): TreeMap[A] = ???
    def get(key: Int): Option[A] = ???
    def +[A1 >: A](kv: (Int, A1)): TreeMap[A1] =  ???
    def -(k: Int): TreeMap[A] = ???
    override def toList: List[(Int, A)] = ???
    def iterator: Iterator[(Int, A)] =???
  }
  
  //叶子节点,也就是每一个分支的末尾,继承了上面的抽象类
  case class Leaf() extends TreeMap[Nothing]
  //叶节点,包含左右和内容,继承了上面的抽象类
  case class Node[+A](key: Int, value: A,
                      left: TreeMap[A], right: TreeMap[A]) extends TreeMap[A]

二叉树中有有基础的增删查操做,还重载了两个符号,+和-分别表明增长和删除。对了,这里的???,其实和python里面的pass是同样的,就充当个占位符,告诉编译器这里会有东西的,先别报错。

而后主要就是要实现二叉树里面空缺的代码,其实熟悉树结构的同窗应该都知道,递归是树天生的基因。因此这里天然都是要经过递归实现的。不过在编写前,仍是要提一下,通常函数式编程里面,不会使用可变变量(var),也不会使用可变的数据结构(ListBuff)。

实现过程也没什么好解释的,其实就是经过递归,以及scala的模式匹配,若是碰到叶子节点就挂掉,不是就递归去进行。直接看代码。这里主要介绍add方法,其余的基本都是相似的:

sealed abstract class TreeMap[+A] extends AbstractMap[Int, A] {
	......
    //使用模式匹配,实现递归操做,主要是找到对应的位置,插入数据
    def add[B >: A](kv: (Int, B)): TreeMap[B] = {

      val (key, value) = kv
	  //this就是当前的类型,多是叶节点,也多是叶子节点
      this match {
        case Node(nodeKey, nodeValue, left, right) => {
		  //按照二叉搜索树的规则,进行递归
          if(nodeKey > key)
            Node(nodeKey, nodeValue, left.add((key,value)), right)
          else if(nodeKey < key)
            Node(nodeKey, nodeValue, left, right.add((key,value)))
          else
            Node(nodeKey, value, left, right)
        }
		//若是是叶子节点,则新生成一个叶节点,返回
        case Leaf() => {
          Node(key, value, Leaf(), Leaf())
        }
      }

	  ......
    }

根据二叉搜索树的规则,新键大于节点的键的时候,插入右边,小于节点的键的时候,插入到左边。而后约定好结束条件,也就是碰到叶子节点的时候返回。这样一来就完成了插入的操做。后面不管是删除,仍是查找,都是一样的思路。

而重载运算符方法,好比重载+方法,就是直接调用上面的add方法,即直接复用。而后看看object TreeMap。

object TreeMap {

    def empty[A]: TreeMap[A] = Leaf()

    def apply[A](kvs: (Int, A)*): TreeMap[A] = {
      kvs.toSeq.foldLeft(empty[A])(_ + _)
    }
  }

这个object主要做用有两个,一个是生成叶子节点,一个是初始化一棵树(注意是apply方法)。和List同样,这里也是用多参数的输入方式,不一样的是这里没有用递归,而是直接把多个参数转化成一个序列,而后用foldLeft,逐个累加。从而实现初始化树。

OK,到这里就结束了,最后仍是但愿你可以本身试着写下tree的代码,写完再用test case测试下,编程功底就是这样一步一步打下的。

3.小结

函数式的数据结构篇到此就结束,但愿在这里,你能明白函数式的数据结构与咱们最开始接触到的数据结构的实现有哪些不一样,又为什么要大费周章用函数式的方式实现!!

不少scala的教程介绍到这里就一句话,scala的默认数据结构是不可变的,若是可变的要怎样巴拉巴拉,这样容易让人陷入知其然不知其因此然的地步。

同时我也一直决定,学习语言的话,语法知识最表层的东西。真正深刻学习一门语言,你须要逐渐知道这门语言在设计上的取舍,甚至是设计上的哲学,好比python的至简哲学。

而在深刻这些东西的过程当中,语法天然而然就掌握了,好比较为晦涩的隐式转换。在这里就会知道隐式转换是这样用的,原来spark里面一直都有这个东西参与!!!

接下来一篇将介绍scala中的错误处理方式,依旧是函数式的处理方式,像java中的try{}catch{}确定是非函数式的,那么scala是怎么实现的呢,下一篇就来介绍:)

若是有什么疑问,也欢迎留言。

以上~

相关文章
相关标签/搜索