Scala编程语言中的函数式编程特性
Scala 高阶函数介绍
什么是高阶函数
高阶函数(Higher-Order Function)就是操作其他函数的函数。在Scala中,函数是“一等公民”
,它和Int、String、Class等其他类型处于同等的地位,可以像其他类型的变量一样被传递和操作。也就是说,如果一个函数的参数列表可以接收函数对象,或者一个函数的返回值是另一个函数,那么这个函数就被称之为高阶函数
。
高阶函数的分类
根据高阶函数使用其他函数的方式,我们可以将高阶函数分为以下几类:
作为值的函数
:可以像任何其他数据类型一样被传递和操作的函数,每当你想要给算法传入具体动作时这个特性就会变得非常有用。匿名函数
:在Scala中,你不需要给每一个函数命名,没有将函数赋给变量的函数叫做匿名函数。匿名函数可以简化代码,避免定义过多的中间变量。闭包
:闭包是一个函数,返回值依赖于声明在函数外部的一个或多个变量。闭包可以访问其定义时所处的作用域中的变量。柯里化
:柯里化是一种将接收多个参数的函数转换为接收单一参数(最初函数的第一个参数)并且返回接受余下参数且返回结果的新函数的技术。
高阶函数的优势有以下几点:
- 高阶函数可以提高代码的抽象程度,使得代码更简洁、清晰和易读。
- 高阶函数可以实现一些通用的功能,比如映射、过滤、排序等,避免重复编写相似的代码。
- 高阶函数可以支持函数式编程的风格,利用不可变数据和纯函数来编写无副作用的代码,提高程序的可靠性和可测试性。
- 高阶函数可以灵活地组合和复用其他函数,实现更复杂的逻辑和功能。
作为值的函数
概述
作为值的函数是指可以将函数赋值给变量或常量,或者作为参数传递给其他函数,或者作为返回值返回的函数。在 Scala 语言中,所有的函数都是作为值的函数。
作用
作为值的函数可以提高代码的复用性和灵活性,可以实现高阶函数(higher-order function)的功能,例如 map、filter、reduce 等。
使用方法
要将一个函数作为值使用,只需要在定义或调用时指定其类型,即参数类型和返回类型。例如,定义一个类型为 (Int) => Int 的函数 square:
// 定义一个类型为 (Int) => Int 的函数 square
val square = (x: Int) => x * x
// 将 square 函数赋值给一个变量
val f = square
// 调用 f 函数
println(f(5)) // 输出 25
例子
以下是一些使用作为值的函数的例子:
// 定义一个类型为 (Int, Int) => Int 的函数 add
val add = (x: Int, y: Int) => x + y
// 定义一个类型为 ((Int, Int) => Int, Array[Int]) => Array[Int] 的函数 applyToAll
// 该函数接受一个函数和一个整数数组作为参数,返回一个新的数组,其中每个元素都是用该函数处理原数组对应元素的结果
val applyToAll = (f: (Int, Int) => Int, array: Array[Int]) => {
val result = new Array[Int](array.length)
for (i <- array.indices) { //array.indices 是一个方法,它返回一个 Range 对象,表示 array 的所有有效索引12。例如,如果 array 的长度是 3,那么 array.indices 就返回 Range(0, 1, 2)。
result(i) = f(array(i), i) //这里没看懂不要紧,下面有详细解释
}
result
}
// 调用 applyToAll 函数,传入 add 函数和一个数组 Array(1, 2, 3)
val array = Array(1, 2, 3)
val newArray = applyToAll(add, array)
println(newArray.mkString(", ")) // 输出 1, 3, 5
// 定义一个类型为 (String) => String 的函数 reverse
val reverse = (s: String) => s.reverse
// 定义一个类型为 ((String) => String, Array[String]) => Array[String] 的函数 mapStrings
// 该函数接受一个函数和一个字符串数组作为参数,返回一个新的数组,其中每个元素都是用该函数处理原数组对应元素的结果
val mapStrings = (f: (String) => String, array: Array[String]) => {
val result = new Array[String](array.length)
for (i <- array.indices) {
result(i) = f(array(i))
}
result
}
// 调用 mapStrings 函数,传入 reverse 函数和一个数组 Array("hello", "world", "scala")
val strings = Array("hello", "world", "scala")
val newStrings = mapStrings(reverse, strings)
println(newStrings.mkString(", ")) // 输出 olleh, dlrow, alacs
详细解释
详细解释一下为什么结果是 1, 3, 5。请看以下内容:
首先,我们定义了一个数组 array,其元素为 1, 2, 3。
然后,我们调用了 applyToAll 函数,传入了一个函数 (x: Int, y: Int) => x + y 和 array 作为参数。
applyToAll 函数的定义如下:
val applyToAll = (f: (Int, Int) => Int, array: Array[Int]) => {
val result = new Array[Int](array.length)
for (i <- array.indices) {
result(i) = f(array(i), i)
}
result
}
该函数的作用是接受一个函数和一个整数数组作为参数,返回一个新的数组,其中每个元素都是用该函数处理原数组对应元素和其索引的结果。
在调用 applyToAll 函数时,它会创建一个新的数组 result,其长度和 array 相同。
然后,它会遍历 array 的所有索引 i,对于每个索引 i,它会调用传入的函数 f,并将 array(i) 和 i 作为参数传入。
匿名函数 f 的定义是 (x: Int, y: Int) => x + y,它的作用是返回两个整数的和。
因此,当 i = 0 时,result(0) = f(array(0), 0) = f(1, 0) = 1 + 0 = 1。
当 i = 1 时,result(1) = f(array(1), 1) = f(2, 1) = 2 + 1 = 3。
当 i = 2 时,result(2) = f(array(2), 2) = f(3, 2) = 3 + 2 = 5。
最后,applyToAll 函数会返回 result 数组,其元素为 1, 3, 5。
因此,当我们打印出 result 数组时,输出就是 1, 3, 5。
使用的时机有哪些
一般来说,当我们需要将一个函数作为参数传递给另一个函数,或者需要将一个函数作为返回值返回时,就可以使用作为值的函数。例如,在实现高阶函数时,就需要使用作为值的函数。另外,当我们需要定义一些简单的匿名函数(也称闭包)时,也可以使用作为值的函数。例如,在使用 Scala 的内置高阶函数时,就可以使用匿名函数作为参数。例如:
// 使用 Scala 的内置高阶函数 map,传入一个匿名函数作为参数
val numbers = Array(1, 2, 3)
val squares = numbers.map(x => x * x)
println(squares.mkString(", ")) // 输出 1
匿名函数
概述
匿名函数是指没有名字的函数,也称为闭包(closure)。在 Scala 语言中,匿名函数是一种特殊的作为值的函数,可以直接定义在代码中,而不需要事先声明。
作用
匿名函数可以简化代码的编写,避免重复定义一些只使用一次的函数。
使用方法
要定义一个匿名函数,只需要使用 => 符号将参数列表和函数体分隔开。例如,定义一个类型为 (Int) => Int 的匿名函数:
// 定义一个类型为 (Int) => Int 的匿名函数
(x: Int) => x * x
要调用一个匿名函数,可以直接在其后加上括号和实参。例如,调用上面定义的匿名函数:
// 调用上面定义的匿名函数
((x: Int) => x * x)(5) // 输出 25
也可以将一个匿名函数赋值给一个变量或常量,然后再调用。例如:
// 将一个匿名函数赋值给一个变量
val f = (x: Int) => x * x
// 调用 f 函数
f(5) // 输出 25
例子
以下是一些使用匿名函数的例子:
// 定义一个类型为 (Int, Int) => Int 的匿名函数
(x: Int, y: Int) => x + y
// 将一个匿名函数作为参数传递给另一个函数
val array = Array(1, 2, 3)
val newArray = applyToAll((x: Int, y: Int) => x + y, array)
println(newArray.mkString(", ")) // 输出 1, 3, 5
// 定义一个类型为 (String) => String 的匿名函数
(s: String) => s.reverse
// 将一个匿名函数作为参数传递给另一个函数
val strings = Array("hello", "world", "scala")
val newStrings = mapStrings((s: String) => s.reverse, strings)
println(newStrings.mkString(", ")) // 输出 olleh, dlrow, alacs
// 定义一个类型为 () => Unit 的匿名函数
() => println("Hello")
// 调用该匿名函数
(() => println("Hello"))() // 输出 Hello
代码中,定义了一个类型为 () => Unit 的匿名函数,它没有参数,只有一个函数体,就是打印 “Hello”。可以用一个变量来存储这个匿名函数,比如:
val hello = () => println("Hello")
然后可以用这个变量来调用这个匿名函数,比如:
hello() // 输出 Hello
代码中,没有用变量来存储匿名函数,而是直接在后面加了一对括号 () 来调用它。这相当于:
(() => println("Hello")).apply()
因为在Scala中,如果一个对象有一个叫做 apply 的方法,那么可以直接用括号来调用它,而不需要写 apply。所以代码就相当于创建了一个匿名函数对象,并立即调用了它的 apply 方法,从而打印出了 “Hello”。
使用的时机有哪些
一般来说,当我们需要定义一些简单的只使用一次的函数时,就可以使用匿名函数。例如,在使用 Scala 的内置高阶函数时,就可以使用匿名函数作为参数。例如:
// 使用 Scala 的内置高阶函数 map,传入一个匿名函数作为参数
val numbers = Array(1, 2, 3)
val squares = numbers.map(x => x * x)
println(squares.mkString(", ")) // 输出 1, 4, 9
// 使用 Scala 的内置高阶函数 filter,传入一个匿名函数作为参数
val numbers = Array(1, 2, 3, 4, 5)
val evens = numbers.filter(x => x % 2 == 0)
println(evens.mkString(", ")) // 输出 2, 4
闭包
概述
闭包是指可以捕获其定义时的环境变量的匿名函数。闭包是一种纯粹的函数式编程机制,可以模拟对象,因为它允许开发者传递一个封装了一些上下文的函数。
作用
闭包可以实现词法作用域(lexical scope)的功能,即使在定义时的环境变量发生变化,也能保持对原始环境变量的访问。词法作用域是指函数的作用域是由函数定义时的位置决定的,而不是函数执行时的位置。闭包是由函数和其声明时的词法环境组合而成的,这个环境包含了函数能访问的所有局部变量。闭包可以用来封装数据,暂存数据,模拟私有方法等。
使用方法
要定义一个闭包,只需要在匿名函数中使用其定义时的环境变量。例如,定义一个类型为 () => Int 的闭包:
// 定义一个类型为 () => Int 的闭包
val a = 10
val b = 20
val f = () => a + b
这个闭包 f 捕获了其定义时的变量 a 和 b 的值,即使在后面改变了它们的值,也不会影响 f 的返回值。
要调用一个闭包,可以直接在其后加上括号。例如,调用上面定义的闭包:
// 调用上面定义的闭包
f() // 输出 30
// 改变环境变量的值
val a = 30
val b = 40
// 再次调用该闭包,结果不变,说明捕获了定义时的环境变量的值
f() // 输出 30
例子
以下是一些使用闭包的例子:
// 定义一个类型为 (Int) => Int 的闭包
var factor = 2
val multiplier = (i: Int) => i * factor
// 调用该闭包
multiplier(3) // 输出 6
// 改变环境变量的值
factor = 3
// 再次调用该闭包,结果改变,说明捕获了环境变量的引用,而不是值
multiplier(3) // 输出 9
// 定义一个类型为 () => Unit 的闭包
var message = "Hello"
val sayHello = () => println(message)
// 调用该闭包
sayHello() // 输出 Hello
// 改变环境变量的值
message = "Hi"
// 再次调用该闭包,结果改变,说明捕获了环境变量的引用,而不是值
sayHello() // 输出 Hi
// 定义一个类型为 (Int) => Int 的高阶函数,它接受一个整数 n,并返回一个类型为 (Int) => Int 的闭包,该闭包可以计算 n 的幂次方
def power(n: Int) = (x: Int) => scala.math.pow(x, n)
// 调用该高阶函数,并将返回的闭包赋值给一个变量
val square = power(2)
// 调用该闭包
square(5) // 输出 25.0
// 调用该高阶函数,并将返回的闭包赋值给另一个变量
val cube = power(3)
// 调用该闭包
cube(5) // 输出 125.0
使用的时机有哪些
一般来说,当我们需要定义一些依赖于外部状态的函数时,就可以使用闭包。例如,在实现柯里化(currying)或偏函数(partial function)时,就需要使用闭包。另外,当我们需要模拟对象或私有成员时,也可以使用闭包。例如:
// 定义一个类型为 (Int) => (() => Int, () => Unit) 的高阶函数,它接受一个整数 balance,并返回两个类型为 () => Int 和 () => Unit 的闭包,分别表示获取余额和存款操作
def bankAccount(balance: Int) = {
var currentBalance = balance // 定义一个私有变量 currentBalance
// 定义一个类型为 () => Int 的闭包 getBalance,它可以获取 currentBalance 的值
val getBalance = () => currentBalance
// 定义一个类型为 () => Unit 的闭包 deposit,它可以修改 currentBalance 的值
val deposit = (amount: Int) => currentBalance += amount
// 返回两个闭包
(getBalance, deposit)
}
// 调用该高阶函数,并将返回的两个闭包赋值给两个变量
val (getBalance, deposit) = bankAccount(100)
// 调用 getBalance 闭包
getBalance() // 输出 100
// 调用 deposit 闭包
deposit(50)
// 再次调用 getBalance 闭包,结果改变,说明两个闭包共享了同一个私有变量 currentBalance
getBalance() // 输出 150
Scala柯里化
概述
柯里化(Currying)是一种将原来接受多个参数的函数变成新的接受一个参数的函数的过程,新的函数返回一个以原有第二个参数为参数的函数。柯里化是函数式编程中的一种常用技巧,它可以让函数更灵活,更具有表现力,也可以实现一些高阶函数。
作用
柯里化有以下几个作用:
- 提高函数灵活性和可读性。柯里化后的函数可以像普通函数一样调用,也可以只传递部分参数,得到一个新的函数,这样可以避免重复传递相同的参数,也可以根据不同的场景选择不同的参数组合。
- 实现控制抽象。柯里化可以让我们自定义一些控制结构,并且使得它们在语法上看起来和Scala内置的控制结构一样。例如,我们可以利用柯里化实现一个withFile方法,让我们可以使用花括号代替圆括号来操作文件。
- 实现惰性求值。柯里化可以让我们延迟计算某些表达式,直到真正需要它们的时候才求值。例如,我们可以利用柯里化实现一个unless方法,让我们可以根据条件来决定是否执行某个代码块。
使用方法
柯里化的使用方法是在函数定义时,将原来的多个参数拆分成多个单一参数的列表,例如:
// 原来的函数
def add(x: Int, y: Int) = x + y
// 柯里化后的函数
def add(x: Int)(y: Int) = x + y
柯里化后的函数可以像普通函数一样调用,也可以只传递部分参数,得到一个新的函数,例如:
// 调用柯里化后的函数
val sum = add(1)(2) // sum = 3
// 只传递部分参数,得到一个新的函数
val addOne = add(1) _ // addOne是一个接受一个Int参数并返回Int结果的函数
val sum2 = addOne(2) // sum2 = 3
例子
柯里化的例子有很多,比如Scala中的集合操作中就有很多使用了柯里化的高阶函数,例如:
// foldLeft是一个柯里化的高阶函数,它接受一个初始值和一个二元操作符,然后对集合中的元素进行累积操作
val array = Array(1, 2, 3, 4, 5)
val result = array.foldLeft(0)(_ + _) // result = 15
// map是一个柯里化的高阶函数,它接受一个一元操作符,然后对集合中的每个元素应用该操作符,并返回一个新的集合
val array2 = array.map(_ * 2) // array2 = Array(2, 4, 6, 8, 10)
下面是一些使用柯里化实现控制抽象和惰性求值的例子:
// 使用柯里化实现控制抽象
def withFile(filename: String)(op: PrintWriter => Unit) {
val writer = new PrintWriter(filename)
try {
op(writer) // 调用传入的函数,对文件进行操作
} finally {
writer.close() // 关闭文件
}
}
// 调用时可以使用花括号代替圆括号,类似于内置控制结构
withFile("test.txt") { writer =>
writer.println("Hello Scala") // 向文件中写入内容
}
// 使用柯里化实现惰性求值
def unless(condition: => Boolean)(block: => Unit) {
if (!condition) block // 如果条件为假,执行代码块
}
// 调用时可以使用花括号代替圆括号,类似于内置控制结构
val x = 10
unless(x > 100) {
println("x is less than 100") // 打印结果
}
什么时候该使用
柯里化应该在需要提高函数灵活性和可读性时使用,比如当需要实现一些控制抽象或者惰性求值时,柯里化就很有用。柯里化也可以让我们利用Scala的类型推断来让代码更加简洁,例如:
val numbers = List(1, 2, 3, 4, 5)
numbers.foldLeft(0)(_ + _) // 类型推断让我们可以省略参数类型和圆括号
柯里化的缺点是可能会增加函数调用的开销,因为每次调用都会创建一个新的函数对象。另外,柯里化也可能会降低代码的可读性,因为柯里化后的函数可能不容易理解其含义和作用。因此,柯里化应该谨慎使用,建议的使用场景包括:
- 单一的函数参数。在某些情况下存在单一的函数参数时,例如上述例子中的foldLeft和map中的op,多参数列表可以使得传递匿名函数作为参数的语法更为简洁。
- 隐式(implicit)参数。如果要指定参数列表中的某些参数为隐式(implicit),应该使用多参数列表。例如:
def execute(arg: Int)(implicit ec: ExecutionContext) = ??? // 指定ec为隐式参数
Scala控制抽象
概述
控制抽象是一种特殊的函数,它的参数是函数,而且这个函数参数没有输入值也没有返回值。控制抽象可以让我们自定义一些看起来像编程语言的关键字的函数,例如while,if等。控制抽象可以让我们把一段代码作为参数传递给另一个函数,而且这段代码可以根据需要多次或者延迟执行。
作用
控制抽象有以下几个作用:
- 提高代码的可读性和灵活性。控制抽象可以让我们用更自然的语法来表达逻辑,而不是用普通的函数调用。例如,我们可以用花括号代替圆括号来传递代码块,使得代码看起来更像内置的控制结构。
- 实现自定义的控制结构。控制抽象可以让我们模拟一些编程语言中不存在的或者不方便使用的控制结构,例如until,repeat等。这样可以让我们根据不同的需求来设计合适的控制流程。
- 实现惰性求值。控制抽象可以让我们延迟计算某些表达式,直到真正需要它们的时候才求值。这样可以提高性能,避免不必要的计算。
使用方法
控制抽象的使用方法是在函数定义时,将原来的普通参数改成函数参数,并且这个函数参数没有输入值也没有返回值,例如:
// 原来的函数
def runInThread(block: () => Unit) {
new Thread {
override def run() {
block() // 调用传入的函数
}
}.start()
}
// 控制抽象后的函数
def runInThread(block: => Unit) {
new Thread {
override def run() {
block // 直接写block
}
}.start()
}
控制抽象后的函数可以像普通函数一样调用,也可以使用花括号代替圆括号来传递代码块,例如:
// 调用控制抽象后的函数
runInThread {
println("Hi")
Thread.sleep(1000)
println("Bye")
}
例子
控制抽象的例子有很多,比如Scala中就有很多内置或者隐式转换实现了控制抽象的高阶函数,例如:
// foreach是一个控制抽象的高阶函数,它接受一个对集合中每个元素进行操作的函数
val list = List(1, 2, 3, 4, 5)
list.foreach(println) // 打印每个元素
// byNameAssert是一个控制抽象的高阶函数,它接受一个断言表达式,并且只在断言模式开启时才执行
var assertionEnabled = true
def byNameAssert(predicate: => Boolean) =
if (assertionEnabled && !predicate)
throw new AssertionError
byNameAssert(5 > 3) // 不会抛出异常
byNameAssert(3 > 5) // 抛出异常
assertionEnabled = false
byNameAssert(3 > 5) // 不会执行断言
下面是一些使用控制抽象实现自定义控制结构的例子:
// 实现一个类似while的控制结构
def myWhile(condition: => Boolean)(block: => Unit): Unit = {
if (condition) {
block
myWhile(condition)(block)
}
}
var x = 10
myWhile(x > 0) {
x -= 1
println(x)
}
// 实现一个类似until的控制结构
def myUntil(condition: => Boolean)(block: => Unit): Unit = {
if (!condition) {
block
myUntil(condition)(block)
}
}
var y = 10
myUntil(y == 0) {
y -= 1
println(y)
}
(😏)没看懂对吧,来看一个简单的例子
需求
- 定义一个函数myShop, 该函数接收一个无参数无返回值的函数(假设: 函数名叫f1).
- 在myShop函数中调用f1函数. 3. 调用myShop函数.
object ClassDemo05 {
def main(args: Array[String]): Unit = {
//1. 定义一个控制抽象函数myShop,它的参数是一个没有输入值也没有返回值的函数
val myShop = (f1: () => Unit) => {
println("Welcome in!") //打印欢迎语
f1() //调用传入的函数参数,执行一系列的语句
println("Thanks for coming!") //打印感谢语
}
//2. 调用控制抽象函数myShop,用大括号传递多条语句作为参数
myShop { () =>
println("我想买一个笔记版电脑") //打印第一个购物意向
println("我想买一个平板电脑") //打印第二个购物意向
println("我想买一个手机") //打印第三个购物意向
}
}
}
什么时候该使用
控制抽象应该在需要提高代码的可读性和灵活性时使用,比如当需要实现一些自定义的控制结构或者惰性求值时,控制抽象就很有用。控制抽象也可以让我们利用Scala的类型推断来让代码更加简洁,例如:
val list = List(1, 2, 3, 4, 5)
list.foreach(println) // 类型推断让我们可以省略参数类型和圆括号
控制抽象的缺点是可能会增加函数调用的开销,因为每次调用都会创建一个新的函数对象。另外,控制抽象也可能会降低代码的可读性,因为控制抽象后的函数可能不容易理解其含义和作用。因此,控制抽象应该谨慎使用,建议的使用场景包括:
- 需要传递一段代码作为参数。在某些情况下,我们需要将一段代码作为参数传递给另一个函数,而且这段代码可能会多次或者延迟执行。例如,我们可以利用控制抽象实现一个withFile方法,让我们可以在打开和关闭文件的过程中执行一些操作。
- 需要模拟一些编程语言中不存在或者不方便使用的控制结构。在某些情况下,我们需要实现一些编程语言中不存在或者不方便使用的控制结构,例如until,repeat等。这样可以让我们根据不同的需求来设计合适的控制流程。