Scala语言基础:函数式编程

scala和Java、Python一样是面向对象语言,本文讲解scala中函数定义、高阶函数和函数式编程实例WordCount

1.函数定义与高阶函数

Scala是一门多范式编程语言,混合了面向对象编程和函数式编程的风格。在过去很多年,面向对象编程一直是主流,但是,随着大数据时代的到来,函数式编程开始迅速崛起,因为,函数式编程可以较好满足分布式并行编程的需求。

函数字面量

函数字面量可以体现函数式编程的核心理念。前面我们已经介绍过,字面量包括整数字面量、浮点数字面量、布尔型字面量、字符字面量、字符串字面量、符号字面量、函数字面量和元组字面量。

val i = 123  //123就是整数字面量
val i = 3.14 //3.14就是浮点数字面量
val i = true //true就是布尔型字面量
val i = 'A' //'A'就是字符字面量
val i = "Hello" //"Hello"就是字符串字面量

除了函数字面量我们会比较陌生以外,其他几种字面量都很容易理解。
在非函数式编程语言里,函数的定义包含了“函数类型”和“值”两种层面的内容。但是,在函数式编程中,函数是“头等公民”,可以像任何其他数据类型一样被传递和操作,也就是说,函数的使用方式和其他数据类型的使用方式完全一致了。这时,我们就可以像定义变量那样去定义一个函数,由此导致的结果是,函数也会和其他变量一样,开始有“值”。就像变量的“类型”和“值”是分开的两个概念一样,函数式编程中,函数的“类型”和“值”也成为两个分开的概念,函数的“值”,就是“函数字面量”。

函数的类型和值

下面我们一点点引导大家更好地理解函数的“类型”和“值”的概念。
我们现在定义一个大家比较熟悉的传统类型的函数,定义的语法和我们之前介绍过的定义“类中的方法”类似(实际上,定义函数最常用的方法是作为某个对象的成员,这种函数被称为方法):

def counter(value: Int): Int = { value += 1}

上面定义个这个函数的“类型”如下:

(Int) => Int

上面就得到了函数的“类型”,下面看看如何得到函数的“值”。
实际上,我们只要把函数定义中的类型声明部分去除,剩下的就是函数的“值”,如下:

(value) => {value += 1} //只有一条语句时,大括号可以省略

上面就是函数的“值”,需要注意的是,采用“=>”而不是“=”,这是Scala的语法要求。

现在,我们再按照大家比较熟悉的定义变量的方式,采用Scala语法来定义一个函数,如下:
声明一个变量时,我们采用的形式是:

val num: Int = 5 //当然,Int类型声明也可以省略,因为Scala具有自动推断类型的功能

照葫芦画瓢,我们也可以按照上面类似的形式来定义Scala中的函数:

val counter: Int => Int = { (value) => value += 1 }

从上面可以看出,在Scala中,函数已经是“头等公民”,单独剥离出来了“值”的概念,一个函数“值”就是函数字面量。这样,我们只要在某个需要声明函数的地方声明一个函数类型,在调用的时候传一个对应的函数字面量即可,和使用普通变量一模一样。

lambda函数与闭包

我们不需要给每个函数命名,这时就可以使用匿名函数,如下:

(num: Int) => num * 2

上面这种匿名函数的定义形式,我们经常称为“Lambda表达式”。“Lambda表达式”的形式如下:

(参数) => 表达式 //如果参数只有一个,参数的圆括号可以省略

我们可以直接把匿名函数存放到变量中,下面是在Scala解释器中的执行过程:

scala> val myNumFunc: Int=>Int = (num: Int) => num * 2 //这行是我们输入的命令,把匿名函数定义为一个值,赋值给myNumFunc变量
myNumFunc: Int => Int = <function1>  //这行是执行返回的结果
scala> println(myNumFunc(3)) //myNumFunc函数调用的时候,需要给出参数的值,这里传入3,得到乘法结果是6
6

实际上,Scala具有类型推断机制,可以自动推断变量类型,比如下面两条语句都是可以的:

val number: Int = 5
val number =5 //省略Int类型声明

下面再来看一下什么是“闭包”?
闭包是一个函数,一种比较特殊的函数。为了解释闭包这个概念,我们在Linux系统的“/usr/local/scala/mycode”目录下新建test.scala代码文件,里面包含以下内容:

object MyTest{
    def main(args: Array[String]): Unit={    
        def plusStep(step: Int) = (num: Int) => num + step
        //给step赋值
        val myFunc = plusStep(3)
        //调用myFunc函数
        println(myFunc(10))     
    }
}

然后,在Linux系统的Shell命令提示符下,运行scala命令运行test.scala代码,如下:

scala test.scala
13  //这是运行结果

在上面的MyTest中, step是一个自由变量,它的值只有在运行的时候才能确定,num的类型是确定的,num的值只有在调用的时候才被赋值。这样的函数,被称为“闭包”,它反映了一个从开放到封闭的过程。

高阶函数

前面已经说过,函数在Scala中是“头等公民”,它的使用方法和任何其他变量是一样的。一个接受其他函数作为参数或者返回一个函数的函数就是高阶函数。

现在我们给出一个简单的高阶函数实例。假设有一个函数对给定两个数区间中的所有整数求和:

def sumInts(a: Int, b: Int): Int = {
if(a > b) 0 else a + sumInts(a + 1, b) 
}

现在,我们重新设计函数sumInts的实现方式,让一个函数作为另一个函数的参数:

//定义了一个新的函数sum,以函数f为参数
def sum(f: Int => Int, a: Int, b: Int): Int ={ 
  if(a > b) 0 else f(a) + sum(f, a+1, b)
}
//定义了一个新的函数self,该函数的输入是一个整数x,然后直接输出x自身
def self(x: Int): Int = x
//重新定义sumInts函数
def sumInts(a: Int, b: Int): Int = sum(self, a, b)

从上面定义可以看出,对于函数sum而言,它的参数类型是(Int=>Int, Int, Int),结果类型是Int,因此,函数sum的类型是:

(Int=>Int, Int, Int) => Int

也就是说,函数sum是一个接受函数参数的函数,因此,是一个高阶函数。

我们上面这种处理方式,看起来“很折腾”,原来很简单的函数sumInts,现在要绕一大圈来实现,这么曲折,到底有什么意义呢?
如果我们单纯只从sumInts的角度来看,上面这种折腾的做法,确实是得不偿失的,但是,如果存在下面这些应用场景,你就可以发现,这种折腾的做法是相当值得的。

比如,现在需要求出连续整数的平方和,代码如下:

def square(x: Int): Int = x * x
def sumSquares(a: Int, b: Int): Int = {
  if(a > b) 0 else square(a) + sumSquares(a + 1, b)
    }
再比如,现在需要求出连续整数的关于2的幂次和,代码如下:
def powerOfTwo(x: Int): Int = {
    if(x == 0) 1 else 2 * powerOfTwo(x-1)
}
def sumPowersOfTwo(a: Int, b: Int): Int = { 
  if(a > b) 0 else powerOfTwo(a) + sumPowersOfTwo(a+1, b)
}

上面的函数都是从a到b的f(n)的累加形式(其中a<=n<=b),唯一的区别就是各种场景下f(n)的具体实现不同,所以,我们可以抽取这些函数中共同的部分重新编写函数sum,并把定义的f(n)作为一个参数传入到高阶函数sum中,代码如下:
def sum(f: Int => Int, a: Int, b: Int): Int = {
  if(a > b) 0 else f(a) + sum(f, a+1, b)
}

def self(x: Int): Int = x
def square(x: Int): Int = x * x
def powerOfTwo(x: Int): Int = if(x == 0) 1 else 2 * powerOfTwo(x-1)

def sumInts(a: Int, b: Int): Int = sum(self, a, b)
def sumSquared(a: Int, b: Int): Int = sum(square, a, b)
def sumPowersOfTwo(a: Int, b: Int): Int = sum(powerOfTwo, a, b)

占位符语法

为了让函数字面量更加简洁,我们可以使用下划线作为一个或多个参数的占位符,只要每个参数在函数字面量内仅出现一次。
下面是一个实例(我们在Scala解释器中运行,从而可以立即看到运行结果):

scala> val numList = List(-3, -5, 1, 6, 9)
numList: List[Int] = List(-3, -5, 1, 6, 9)
scala> numList.filter(x => x > 0 )
res1: List[Int] = List(1, 6, 9)
scala> numList.filter(_ > 0)
res2: List[Int] = List(1, 6, 9)

从上面运行结果可以看出,下面两个函数字面量是等价的。
x => x>0
_ > 0

当采用下划线的表示方法时,对于列表numList中的每个元素,都会依次传入用来替换下划线,比如,首先传入-3,然后判断-3>0是否成立,如果成立,就把该值放入结果集合,如果不成立,则舍弃,接着再传入-5,然后判断-5>0是否成立,依此类推。

2.函数式编程WordCount实例

通过前面的函数式编程的基础知识的学习,现在让我们通过一个单词统计的实例来加深对前面知识的消化理解。

任务:按照函数式编程的风格,编写一个程序,对某个目录下所有文件中的单词进行词频统计
做法:请进入Linux系统,打开“终端”,进入Shell命令提示符状态,然后,在“/usr/local/scala/mycode”目录下,新建一个wordcount子目录,并在“/usr/local/scala/mycode/wordcount”目录下新建两个包含了一些语句的文本文件word1.txt和word2.txt(你可以在文本文件中随意输入一些单词,用空格隔开),我们会编写Scala程序对该目录下的这两个文件进行单词词频统计。
请在“/usr/local/scala/mycode”目录下新建一个test.scala文件,下面是test.scala中包含的进行词频统计的Scala程序代码:

import java.io.File
import scala.io.Source
object WordCount {
  def main(args: Array[String]): Unit = {
    val dirfile=new File("/usr/local/scala/mycode/wordcount")
    val files=dirfile.listFiles
    for(file <- files) println(file)
    val listFiles=files.toList
    val wordsMap=scala.collection.mutable.Map[String,Int]()
    listFiles.foreach( file =>Source.fromFile(file).getLines().foreach(line=>line.split(" ").
                  foreach(
                      word=>{
                        if (wordsMap.contains(word)) {
                          wordsMap(word)+=1
                        }else {
                          wordsMap+=(word->1)
                        }
                      }
                  )
            )
    )
    println(wordsMap)
    for((key,value)<-wordsMap) println(key+": "+value)
  }
}

然后,在Shell命令提示符状态下,运行下面命令执行:

cd /usr/local/scala/mycode  //要确保当前工作目录是mycode目录
scala test.scala

然后你就可以看到屏幕给出的词频统计结果。

下面我们在Scala解释器中运行前面几行代码,我们就可以看到每条语句运行产生的结果:

sala> import java.io.File
import java.io.File

scala> import scala.io.Source
import scala.io.Source

scala> val dirfile=new File("/usr/local/scala/mycode/wordcount")
dirfile: java.io.File = /usr/local/scala/mycode/wordcount

scala> val files=dirfile.listFiles
files: Array[java.io.File] = Array(/usr/local/scala/mycode/wordcount/word2.txt, /usr/local/scala/mycode/wordcount/word1.txt)

scala> for(file <- files) println(file)
/usr/local/scala/mycode/wordcount/word2.txt
/usr/local/scala/mycode/wordcount/word1.txt

scala> val listFiles=files.toList
listFiles: List[java.io.File] = List(/usr/local/scala/mycode/wordcount/word2.txt, /usr/local/scala/mycode/wordcount/word1.txt)

参考链接:

http://dblab.xmu.edu.cn/blog/spark/

https://www.runoob.com/scala/scala-tutorial.html

历史推荐

妈妈再也不用担心双系统安装了!

Spark机器学习:模型评估指标

Spark机器学习:频繁模式挖掘

爬虫实战:Selenium爬取京东商品

爬虫实战:豆瓣电影top250爬取

爬虫实战:Scrapy框架爬取QQ音乐

数据分析与挖掘

数据结构与算法

机器学习与大数据组件

欢迎关注,感谢“在看”,随缘稀罕~

 

猜你喜欢

转载自blog.csdn.net/qq_36936730/article/details/106205418