zoukankan      html  css  js  c++  java
  • Scala函数式编程(三) scala集合和函数

    前情提要:

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

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

    前面已经稍微介绍了scala的常用语法以及面向对象的一些简要知识,这次是补充上一章的,主要会介绍集合和函数。

    注意噢,函数和方法是不一样的,方法是在类里面定义的,函数是可以单独存在的(严格来说,在scala内部,每个函数都是一个类)

    一.scala集合介绍

    还记得上一章介绍的object的apply方法吗,很多数据结构其实都用到了它,从而让我们可以直接用List(...)这样来新建一个List,而不用自己手动new一个。

    PS:注意,scala默认的数据结构都是不可变的,就是说一个List,没法删除或增加新的元素。当然,也有“可变”的数据结构,后面会介绍到。

    1.1 List

    得益于apply方法,我们可以不通过new来新建数据结构。

    //通过工厂,新建一个List,这个List是不可变的
    scala> val numbers = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
    numbers: List[Int] = List(1, 2, 3, 4, 5, 1, 2, 3, 4, 5)
    

    1.2 元组Tuple

    Tuple这个概念在python应用比较广泛,它可以将多种数据类型(Int,String,Double等)打包在一起

    scala> val tup = (1,1,2.1,"tuple",'c')  //将多种不同数据结构打包一起,可以有重复
    tup: (Int, Int, Double, String, Char) = (1,1,2.1,tuple,c)
    

    但在scala中,Tuple是有长度限制的,那就是一个Tuple最多只能有22个元素。

    scala> val tup = (0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)
    tup: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) = (0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)
    
    //一个Tuple超过22个元素,报错了
    scala> val tup = (0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)
    <console>:1: error: too many elements for tuple: 23, allowed: 22
    val tup = (0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)
    

    可以看到上面,一但一个Tuple超过22个元素,就会报错了。至于为什么是22这个神奇的数字,好像一直没有一个统一的论调。有人开玩笑的说23才对,因为有部电影的名字叫《The Number 23》~~

    1.3 Map和Option

    在说Map之前,需要先介绍Option,因为Map里面存的就是Option,这个后面介绍。

    Option翻译过来是选项,本质上,它也确实是一个选项,用来告诉你存不存在。还是用实例说明吧:

    //Option里面可以存普通数据类型
    scala> val optionInt = Option(1)
    optionInt: Option[Int] = Some(1)
    
    scala> optionInt.get
    res8: Int = 1
    //但一个Option也可能为空
    scala> val optionNone = Option(null)
    optionNone: Option[Null] = None
    //当Option里面是空的时候,是get不出东西的,还会报错
    scala> optionNone.get
    java.util.NoSuchElementException: None.get
      at scala.None$.get(Option.scala:347)
      at scala.None$.get(Option.scala:345)
      ... 32 elided
    //这个时候可以判断它就是空的
    scala> optionNone.isEmpty
    res11: Boolean = true
    //但可以用getOrElse()方法,当Option里面有东西的时候,就返回那个东西,如果没有东西,就返回getOrElse()的参数的内容
    scala> optionNone.getOrElse("this is null")  //里面没东西
    res12: String = this is null
    scala> optionInt.getOrElse("this is null") //里面有东西
    res15: Any = 1
    

    再说Map,Map里面的value的类型并不是你赋的那个数据类型,而是Option。即Map(key -> Option,key1 -> Option)。

    scala> val map = Map("test1" -> 1,"test2" -> 2)
    map: scala.collection.immutable.Map[String,Int] = Map(test1 -> 1, test2 -> 2)
    
    scala> map.get("test1")
    res16: Option[Int] = Some(1)
    
    scala> map.get("test3")
    res17: Option[Int] = None
    
    scala> map.get("test3").getOrElse("this is null")
    res18: Any = this is null
    

    这样的好处是什么呢?还记得在java里面,每次都得为java为空的情况做判断的痛苦吗。在scala,这些烦恼通通不存在。有了Option,妈妈再也不用担心NullPointerException啦。

    1.4 常用函数组合子

    匿名函数

    将集合的函数组合子,那肯定得先将匿名函数。如果有用过python中的lambda表达式,那应该就很了解这种方式了。

    前面说到,在scala中,函数就是对象,匿名函数也是函数。举个简单的例子:

    //创建一个匿名函数
    scala> val addOne = (x: Int) => x + 1
    addOne: (Int) => Int = <function1>
    
    scala> addOne(1)
    res4: Int = 2
    

    注意,函数里面是可以不用return的,最后面的那个x+1就是匿名函数的返回值了。

    map,reduce

    因为hadoop的出现,MapReduce都被说烂了。虽然Hadoop的Mapreduce起源自函数式的map和reduce,但两者其实是不一样的。感兴趣的可以看看我之前写过的一篇:从分治算法到 Hadoop MapReduce

    函数式里面的map呢,粗略的说,其实相当一个有返回值的for循环。

    scala> val list = List(1,2,3)
    list: List[Int] = List(1, 2, 3)
    
    scala> list.map(_ + 1)   //这里的(_+1)其实就是一个匿名函数 //让List中每一个元素+1,并返回
    res29: List[Int] = List(2, 3, 4)
    

    至于reduce呢,也是像for循环,只是不像map每次循环是当前元素,reduce除了当前元素,还有上一次循环的结果,还是看看例子吧:

    scala> list.reduce((i,j) => i + j)  //两个两个一起循环,这里是让两个相加
    res28: Int = 6
    

    比如上面的例子,有1,2,3三个数。第一次循环的两个是1,2,1+2就等于3,第二次循环就是上次的结果3和原本的3,3+3等于6,结果就是6。

    filter

    filter故名思意,就是过滤的意思,可以在filter中传进去一个匿名函数,返回布尔值。返回true的则保留,返回false的丢弃。

    scala> val numbers = List(1, 2, 3, 4)
    numbers: List[Int] = List(1, 2, 3, 4)
    
    //过滤出余2等于0的
    scala> numbers.filter((i: Int) => i % 2 == 0)
    res0: List[Int] = List(2, 4)
    

    foldLeft

    这个和reduce类似,也是遍历,除了当前元素,还有上一次迭代的结果。区别在于foldLeft有一个初始值。

    scala> val numbers = List(1, 2, 3, 4)
    numbers: List[Int] = List(1, 2, 3, 4)
    
    //(m: Int, n: Int) => m + n这部分是一个匿名函数
    scala> numbers.foldLeft(0)((m: Int, n: Int) => m + n)
    res30: Int = 10
    

    二.scala函数

    在最前面有介绍到,函数就是对象。那为什么函数能直接运行呢?这其实得益于object的apply这个语法糖。

    偏函数

    偏函数(PartialFunction),从某种意义上来说,偏函数也是scala中挺重要的一个语法糖。

    偏函数它长这样:

    PartialFunction[A, B] //接收一个A类型的参数,返回B类型的参数

    值得一提的是,scala中有一个关键字case,就是使用偏函数。继续举例子:

    //下面的case就是一个偏函数PartialFunction[Int, String]
    scala> val one: PartialFunction[Int, String] = { case 1 => "one" }
    one: PartialFunction[Int,String] = <function1>
    
    scala> one(1)
    res11: String = Int
    
    scala> one("one")
    <console>:13: error: type mismatch;
     found   : String("one")
     required: Int
           one("one")
    

    case关键字会匹配符合条件的类型或值,如果不符合,会报错。内部实现就不介绍了,知道是个常用语法糖就好。

    而这个case关键字也正是实现scala模式匹配中,必不可少的一环,有兴趣的童鞋可以看看我这篇:scala模式匹配详细解析

    这里再说一个常见应用吧,函数组合子中有一个collect,它需要的参数就是一个偏函数。下面看个有意思的例子:

    //这个list里面的Any类型
    scala> val list:List[Any] = List(1, 3, 5, "seven")
    list: List[Any] = List(1, 3, 5, seven)
    
    //使用map会报错,因为map接收的参数是普通函
    scala> list.map { case i: Int => i + 1 }
    scala.MatchError: seven (of class java.lang.String)
      at $anonfun$1.apply(<console>:13)
      at $anonfun$1.apply(<console>:13)
      at scala.collection.immutable.List.map(List.scala:277)
      ... 32 elided
    
      //但如果用collect函数就可以,因为collect接收的参数是偏函数,它会自动使用偏函数的一些特性,所以可以自动过滤掉不符合的数据类型
    scala> list.collect { case i: Int => i + 1 }
    res15: List[Int] = List(2, 4, 6)
    

    因为collect接收的参数是偏函数,它会自动使用偏函数的特性,自动过滤不符合的数据类型,而map就做不到。

    部分应用函数

    所谓部分应用的意思,就是说,当调用一个函数时,可以仅传递一部分参数。而这样会生成一个新的函数,来个实例看看吧:

    //定义一个打印两个输出参数的函数
    scala> def partial(i:Int,j:Int) : Unit = {
         |     println(i)
         |     println(j)
         | }
    partial: (i: Int,j: Int)Unit
    
    //赋一个值给上面那个函数,另一个参数不赋值,生成一个新的函数
    scala> val partialFun = partial(5,_:Int)
    partialFun: Int => Unit = <function1>
    
    //只要一个参数就可以调用啦
    scala> partialFun(10)
    5
    10
    

    部分应用函数,主要是作用是代码复用,同时也能够增加一定的代码可读性。

    当然还有更多有意思的用法,后面有机会说到再说。

    函数柯里化

    刚开始,听到柯里化的时候很奇怪。柯里?啥玩意?

    后来才知道,其实柯里是从curry音译过来的,这个是个人名,就是发明了柯里化的发明人。

    柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

    看看具体是怎么使用吧:

    //我们知道函数可以这样定义,让它接收两个参数
    scala> def curring(i:Int)(j: Int): Boolean = {false}
    curring: (i: Int)(j: Int)Boolean
    
    //可以把这个函数赋值给一个变量,注意变量的类型
    scala> val curringVal:(Int => (Int => Boolean)) = curring _
    curringVal: Int => (Int => Boolean) = <function1>
    
    //可以让这个变量接收一个参数,又变成另一个函数了
    scala> val curringVal_1 = curringVal(5)
    curringVal_1: Int => Boolean = <function1>
    
    //再用这个变量接收一个参数,终于能返回结果了
    scala> curringVal_1(10)
    res32: Boolean = false
    
    

    柯里化其实是把一个函数变成一个调用链的过程,和上面的部分应用函数看起来有点像。

    这几个部分初次看可能不知道它究竟有什么用,其实这些功能的一个主要用途是函数式的依赖注入。通过这部分技术可以把被依赖的函数以参数的形式传递给上层函数。限于篇幅这里就先省略,后面再介绍。

    结语:

    此次介绍的是scala集合的一些内容,以及一些函数的特性,再说一遍,函数其实就是对象。

    我一直有一种观点,在学习新的东西的时候,一些偏固定规则的东西,比如语法。不用全部记熟,只要知道大概原理,有个映像就行。

    比如说scala的函数式编程,或是java的OOP,不需要抱有先把语法学完,再学习相关的编程理念,这在我看来是有点本末倒置了。

    我一般的做法,是先熟悉大概的语法,然后去学习语言的精髓。当碰到不懂的时候,再反过来查询具体的语法,有了目标之后,语法反而变得不是那么枯燥了。

    以上只是我的一些个人看法,那么本篇到此就结束了。

    以上~~

  • 相关阅读:
    齐文词根词缀---1、第一讲
    元音发音规则
    英语重读规则
    英语单词重读规则
    unicode,ansi,utf-8,unicode big endian编码的区别
    Netty中粘包和拆包的解决方案
    【转载】 DirectByteBuffer内存释放
    Netty 中ChannelOption的含义以及使用的场景Netty 中ChannelOption的含义以及使用的场景
    Connection reset原因分析和解决方案
    Netty实现Socket
  • 原文地址:https://www.cnblogs.com/listenfwind/p/11593498.html
Copyright © 2011-2022 走看看