2018年第45周-scala入门-函数式编程

重头戏来了!
scala中的函数是java中完成没有的概念.
因为java是完全面向对象的编程语言, 没有任何面向过程编程语言的特性, 因此java中的一等公民是类和对象, 而且只有方法的概念, 即寄存和依赖于类和对象中的方法.
java中的方法是绝对不可能脱离类和对象独立存在的.
scala是一门既面向对象, 又面向过程的语言. 因此在scala中有非常好的面向对象的特性, 可以使用scala来基于面向对象的思想开发大型复杂的系统和工程; 而且scala也面向过程, 因此scala中有函数的概念.
在scala中, 函数与类, 对象等一样, 都是一等公民. scala中的函数可以独立存在, 不需要依赖任何类和对象.
scala的函数式编程, 就是scala面向过程的最好的佐证. 也正是因为函数式编程, 才让scala具备了java所不具备的更强大的功能和特性.

简单回顾下之前的函数定义

定义函数  

在scala中定义函数时, 需要定义函数的函数名, 参数, 函数体.
我们的第一个函数如下所示:

def sayHello(name:String, age:Int)={
  if(age>=18) {
     printf("hi %s, you are a big boy\n",name)
     age
  }
  else {
     printf("hi %s, you are a little boy\n",name)
     age
  }
}
sayHello("jevoncode",29)

scala要求必须给出所有参数的类型,但是不一定给出函数返回值的类型, 只要右侧的函数体中不包含递归的语句, scala就可以自己根据右侧的表达式推断出返回类型. 另外这里没有等号=也可以.

单行函数

单行的函数:

def sayHello(name: String)= print("Hello, "+name)   

在代码块中定义函数体

如果函数体中有多行代码, 则可以使用代码块的方式包裹多行代码, 代码块中最后一行的方绘制就是整个函数的返回值. 与Java不同, 不是使用return返回值的.
比如下面的函数, 实现累加的功能:

def sum(n: Int)={
  var sum=0;
  for(i <-1 to n) sum+=i
  sum
}

将函数赋值给变量

scala中的函数是一等公民, 可以独立定义, 独立存在, 而且可以直接将函数作为值赋值给变量.

//看, 没有等号=来定义函数
scala> def sayHello(name:String){println("Hello, "+name)}
sayHello: (name: String)Unit

// 不是直接赋值
scala> val sayHelloFunc = sayHello
<console>:12: error: missing argument list for method sayHello
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `sayHello _` or `sayHello(_)` instead of `sayHello`.
       val sayHelloFunc = sayHello
                          ^
// 而是由特定语法: 函数名+空格+下划线
scala> val sayHelloFunc = sayHello _
sayHelloFunc: String => Unit = $$Lambda$2966/1575180841@4173645a

scala> sayHelloFunc("jc")
Hello, jc

匿名函数

scala中, 函数也可以不需要命名, 此时函数被称为匿名函数.
可以直接定义函数之后, 将函数赋值给某个变量, 也可以将直接定义的匿名函数传入其他函数之中.

val sayHelloFunc = (name:String) => println("Hello, "+name)

scala定义匿名函数的语法规则就是, (参数名:参数类型)=>函数体

高阶函数

没错, 就是数学上的高阶函数的定义. 一个函数作为另外个函数的参数.
scala中, 由于函数是一等公民, 因此可以直接将某个函数传入其他函数, 作为参数. 这个功能是极其强大的, 也是java这种面向对象的编程语言所不具备的.
接受其他函数作为参数的函数, 也被称作高阶函数(higher-order function)

val sayHelloFunc=(name:String)=>println("Hello, "+name)
//高阶函数
def greeting(func:(String) => Unit, name:String){func(name)}  

greeting(sayHelloFunc,"jc")  


Array(1,2,3,4,5).map((num:Int)=> num * num)

高阶函数的另一个功能是将函数作为返回值

//注意这里别和上面的匿名函数搞混了, 它们俩很像, 但不是同一个东西, 一个是定义变量, 一个是定义函数
def getGreetingFunc(msg: String) = (name:String) => println(msg + ", "+name)  
val greetingFunc = getGreetingFunc("hello")
greetingFunc("jc")

高阶函数的类型判断

高阶函数可以自动推断出参数类型, 而不需要写明类型; 而且对于只有一个参数的函数, 还可以省去其小括号; 如果仅有的一个参数在右侧的函数体内只使用一次, 则还可以将接受参数省略, 并且将参数使用_来替代.

//下面一步一步的将调用高阶函数语法变得更简短
//定义高阶函数
def greeting(func: (String) => Unit, name:String){ func(name)}  
//调用高阶函数
greeting((name:String) => println("Hello, "+name), "jc")
//更简短的调用高阶函数
greeting((name) => println("Hello, "+name), "jc")
greeting(name => println("Hello, "+name), "jc")

//最简短的调用高阶函数, 这个得熟练理解
def triple(func: (Int)=>Int) = {func(3)}
triple( 3 * _)

scala常用的高阶函数

//map: 对传入的每个元素都进行映射, 返回一个处理后的元素
Array(1,2,3,4,5).map(2 * _)

//foreach: 对传入的每个元素都进行处理, 但是没有返回值
//哇, 这个用法得很理解高阶函数才行
(1 to 9).map( "*" * _).foreach(println _)

//filter: 对传入的每个元素都进行类型判断, 如果对元素返回true, 则保留该元素, 否则过滤该元素  
(1 to 20).filter(_ % 2 ==0)

//reducelet: 从左侧元素开始, 进行reduce操作, 即先对元素1和元素2进行处理, 然后将结果与元素3处理, 再将结果与元素4处理, 以此类推, 即为reduce  
//下面的操作就相当于 1 * 2 * 3 * 4 * 5 * 7 * 8 * 9  
(1 to 9).reduceLeft(_*_)

//sortWith: 对元素进行两两对比, 进行排序 
Array(3,2,5,4,10,1).sortWith(_<_)

闭包

闭包最简洁的解释: 函数在变量不处于其有效作用域时, 还能够对变量进行访问, 即为闭包

//定义高阶函数, 返回函数
scala> def getGreetingFunc(msg: String) = (name:String) => println( msg + "," + name)  
getGreetingFunc: (msg: String)String => Unit

//定义匿名函数
scala> val greetingFuncHello = getGreetingFunc("hello")
greetingFuncHello: String => Unit = $$Lambda$3155/319998722@ba52adc

//定义匿名函数
scala> val greetingFuncHi = getGreetingFunc("hi")
greetingFuncHi: String => Unit = $$Lambda$3155/319998722@455fd816

//调用匿名函数
scala> greetingFuncHello("jc")
hello,jc
//调用匿名函数
scala> greetingFuncHi("jc")
hi,jc

这里使用就高级咯, 两次定义匿名函数, 由于传入不同的msg, 所以创建返回的是不同函数.
msg只是一个局部变量, 却在getGreetingFunc高阶函数执行完后, 还可以继续存在创建的匿名函数之中; greetingFuncHello(“jc”), 调用时, 值为”Hello”的msg被保留在了函数内部, 可以反复的使用.
这种变量超出了其作用域, 还可以使用的情况, 即为闭包
scala通过每个函数创建对象来实现闭包, 实际上对于getGreetingFunc函数创建的匿名函数, msg是作为此匿名函数对象的变量存在的, 因此每个函数才可以拥有不同的msg.
scala编译器会确保上述闭包机制.

SAM转换

在Java中, 不支持直接将函数传入一个方法作为参数, 通常来说, 唯一的办法就是定义一个实现了某个接口的类的实例对象, 该对象只有一个方法; 而这些接口都只有单个的抽象方法, 也就是single abstract method, 简称SAM
由于scala是可以调用java的代码的, 因此当我们调用java的某个方法时, 可能就不得不创建SAM传递给方法, 非常麻烦; 但是scala又是支持直接传递函数的. 此时就可以使用scala提供的, 在调用java方法时, 使用的功能, SAM转换, 即将SAM转换为scala函数
要使用SAM转换, 需要使用scala提供的特性, 隐式转换

import javax.swing._
import java.awt.event._

val button = new JButton("Click")  
//原java方法调用
button.addActionListener(new ActionListener{
  override def actionPerformed(event: ActionEvent){
     println("Click Me!!")
  }
})

//隐式转换, 注意, 这里使用了高阶函数. 依然实现了ActionListener, 但也就隐式这次使用了, 后面调用addActionListener就不用了写实现ActionListener的代码, 只需将actionProcessFunc: (ActionEvent)=>Unit 这样的函数参数传入即可.
implicit def getActionListener(actionProcessFunc: (ActionEvent)=>Unit) = new ActionListener{
  override def actionPerformed(event: ActionEvent){
      actionProcessFunc(event)
  }
}

//题外话, 有没发现, 定义函数和与定义高阶函数参数不一样actionProcessFunc: (ActionEvent)=>Unit
button.addActionListener((event:ActionEvent)=>print("Click Me!!"))

Currying函数

Currying函数, 指的是, 将原来接受两个参数的一个函数 ,转换为两个函数, 第一个函数接受原先的第一个参数, 然后返回接受原先第二个参数的第二个函数.
在函数调用的过程中, 就变为了两个函数连续调用的形式

//演变过程, 原本两个参数的函数
def sum(a: Int, b: Int) = a + b
sum(1,1)

//演变过程2, 用等于号连接两个参数
def sum2(a: Int) = (b:Int) => a + b
sum2(1)(1)

//最后形态
def sum3(a: Int)(b:Int) = a + b

从演变过程2, 我们看出, 也就是闭包的使用, 而闭包的使用也是建立在匿名函数中.

return

在scala中, 不需要使用return来返回函数的值, 函数最后一行语句的值, 就是函数的返回值.
在scala中, return用于在嵌套函数中返回值给包含嵌套函数的函数, 并作为函数的返回值.
使用return的嵌套函数, 是必须给出返回类型的, 否则无法通过编译.

def greeting(name: String) = {
  def sayHello(name: String):String = {
    return "Hello, " + name
  }
  sayHello(name)
}

函数式编程之集合操作

 scala的集合体系结构

scala中的集合体系主要包括: Iterable, Seq, Set, Map. 其中Iterable是所有集合Trait的跟Trait. 这个结构跟Java的集合体系非常类似.
scala的集合是分成可变(mutable)不可变(immutable)两类集合, 其中可变集合就是集合的元素可以动态修改, 而不可变集合的元素在初始化之后, 就无法修改了. 分别对应scala.collection.mutable和scala.collection.immutable两个包.
Seq下包含了Range, ArrayBuffer, List等子trait. 其中Range就代表了一个序列, 通常可以使用”1 to 10″ 这种语法来产生一个Range. ArrayBuffer就类似于java的ArrayList.

List

List代表一个不可变的列表
List的创建, val list=List(1,2,3,4)
List有head和tail, head代表list的第一个元素, tail代表第一个元素之后的所有元素, list.head, list.tail
List有特殊的::操作符, 可以用于将head和tail合并成一个List, 0::list
List的空集合是Nil, 如果List只有一个元素, 则它的tail是Nil

案例: 用递归函数来给List中每个元素都加上指定前缀, 并打印
def decorator(l: List[Int], prefix: String){
if(l != Nil){ //用Nill判断是否为空

println(prefix+l.head)
decorator(l.tail, prefix)

}
}

LinkedList

LinkedList代表一个可变列表, 使用elem可以引用其头部, 使用next可以引用其尾部.

scala> val l = scala.collection.mutable.LinkedList(1,5,3,2,4) 
warning: there was one deprecation warning (since 2.11.0); for details, enable `:setting -deprecation' or `:replay -deprecation'
l: scala.collection.mutable.LinkedList[Int] = LinkedList(1, 5, 3, 2, 4)

scala> l.elem
res1: Int = 1

scala> l.next
res2: scala.collection.mutable.LinkedList[Int] = LinkedList(5, 3, 2, 4)

案例: 使用while循环列表中的每个元素都乘以2

val list = scala.collection.mutable.LinkedList(1,5,3,2,4)
var currentList = list
while(currentList!=Nil){
  currentList.elem = currentList.elem * 2
  currentList = currentList.next
}
scala> list
res5: scala.collection.mutable.LinkedList[Int] = LinkedList(2, 10, 6, 4, 8)

案例: 使用while循环将列表中每隔一个元素就乘以2

val list = scala.collection.mutable.LinkedList(1,2,3,4,5,6,7,8,9,10)  
var currentList = list
var first = true
while(currentList != Nil && currentList.next != Nil){
   if(first){ currentList.elem = currentList.elem * 2; first = false }
   currentList = currentList.next.next
   currentList.elem = currentList.elem * 2
}
scala> list
res8: scala.collection.mutable.LinkedList[Int] = LinkedList(2, 2, 6, 4, 10, 6, 14, 8, 18, 10)

Set

Set代表一个没有重复元素的集合
将重复元素加入Set是没有用的, 比如val s = Set(1,2,3); s+1; s+4;
而Set是不保证插入顺序的, 也就是说, Set中的元素是乱序的.

val s = new scala.collection.mutable.HashSet[Int]()
scala> s+=1
res3: s.type = Set(1)

scala> s+=2
res4: s.type = Set(1, 2)

scala> s+=5
res5: s.type = Set(1, 5, 2)

LinkedHashSet会用一个链表维护插入顺序

val s = new scala.collection.mutable.LinkedHashSet[Int]()

scala> s+=1
res6: s.type = Set(1)

scala> s+=2
res7: s.type = Set(1, 2)

scala> s+=5
res8: s.type = Set(1, 2, 5)

SrotedSet会自动根据key来进行排序

scala> val s = scala.collection.mutable.SortedSet("orange","apple","banana")
s: scala.collection.mutable.SortedSet[String] = TreeSet(apple, banana, orange)

集合的函数式编程

用到的都是高阶函数的知识点
高阶函数的使用, 也是scala和java最大的不同!! 因为java里面还没有函数式编程的, 也肯定没有高阶函数, 也肯定无法直接将函数传入一个方法, 或者让一个方法返回一个函数
对scala高阶函数的理解, 掌握和使用, 可以大大拓展你技术的事业, 这也是scala最有魅力, 最有优势的一个功能.
map案例实战: 为List每个元素都添加一个前缀
注意, 每个map函数的返回值都会返回其之前的类型, 如List.map, 返回的就是List

List(1,2,3,4).map("a"+_)  

faltMap案例实战: 将List中的多行句子拆分成单词

List("Helle World","You and Me").flatMap(_.split(" "))

foreach案例实战: 打印List中的每个单词

List("I","have","a","beautiful","house").foreach(println(_))

zip案例实践: 对学生姓名和学生成绩进行关联

scala> List("Jc","Jen","Peter","Jack").zip(List(100,90,75,83))
res14: List[(String, Int)] = List((Jc,100), (Jen,90), (Peter,75), (Jack,83))

综合案例实战: 单词计数(不考虑重复)

val lines01 = scala.io.Source.fromFile("/home/gucci/Desktop/hello1").mkString
val lines02 = scala.io.Source.fromFile("/home/gucci/Desktop/hello2").mkString

val lines = List(lines01,lines02)
// " |\n"是正则表达式, 表示以空格和换行隔开的字符串
//高阶函数的链式调用是重点, 以及大量的下划线使用
//这是scala编程的精髓所在, 这就是函数式变长, 也是scala相较于java等编程语言最大的功能优势所在
//spark的源码大量使用了这种复杂的链式调用的函数式编程
//spark本身提供的api, 也是完成沿用了scala的函数式编程, 比如spark自身的pai就提供了map, flatmap, reduce, foreach, 以及更高级的reduceByKey, groupByKey等高阶函数
lines.flatMap(_.split(" |\n")).map((_,1)).map(_._2).reduceLeft(_+_)
    原文作者:黄小数
    原文地址: https://segmentfault.com/a/1190000016975589
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞