Scala | 教程 | 学习手册 --- 首类函数

2021-12-14 18:37:01 浏览数 (1)

First Class Functions

函数式编程的核心就是函数应当是首类的。首类表示函数不仅能得到声明和调用,还可以作为一个数据类型用在这个语言的任何地方。

首类函数与其他数据类型一样,可以采用字面量创建;或者存储在值、变量、或数据结构等容器中;还可以作为一个函数的参数或返回值。

如果一个函数接收其他函数作为参数或使用函数作为返回值,就称为高阶函数(higher-order function)。比如map和reduce。map取一个函数参数,用它处理一个或多个项转换为新值。reduce取一个函数参数,将包含多项的集合规约为一项。

函数类型和值

函数类型

函数类型就是输入与输出的简单组合

格式

代码语言:javascript复制
([<type>, ...]) => <type>
scala> def double(x: Int): Int = x * 2
double: (x: Int)Int

// 只有单个参数的函数可以省略小括号
scala> val myDouble: (Int) => Int = double                                (1)
myDouble: Int => Int = <function1>

scala> myDouble(5)                                                        (2)
res1: Int = 10

在上面这个例子中,定义了一个函数,然后赋值给一个identifier “myDouble“。这个myDouble是一个函数值了,可以调用刚刚的函数。

与定义value的格式是一样的:val myDouble: 参数类型 = double。这里的参数类型就是函数类型。

用通配符为函数赋值

通配符下划线相当于占位符,表示将来的一个函数调用。要么使用显式的类型,要么使用通配符_定义函数值以及用函数赋值。才能区分这是个函数值function value(存入函数的value)还是函数调用。

格式

代码语言:javascript复制
val <identifier> = <function name> _
scala> def double(x: Int): Int = x * 2
double: (x: Int)Int

// 与上面val myDouble: (Int) => Int = double 是一致的
scala> val myDouble = double _
myDouble: Int => Int = <function1>

scala> val amount = myDouble(20)
amount: Int = 40

scala> def max(a: Int, b: Int) = if (a > b) a else b
max: (a: Int, b: Int)Int

scala> val maximize = max _
maximize: (Int, Int) => Int = $$Lambda$1199/2029885969@d0993f0

scala> maximize(20,30)
res9: Int = 30

以上介绍了如何指定函数type以及把函数保存在值中

高阶函数

高阶函数也是函数,包含一个函数类型的值作为输入或返回值

代码语言:javascript复制
scala> def safeStringOp(s: String, f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String, f: String => String)String

scala> def reverser(s: String) = s.reverse
reverser: (s: String)String

scala> safeStringOp(null, reverser)
res4: String = null

scala> safeStringOp("Ready", reverser)
res5: String = ydaeR

函数字面量

代码语言:javascript复制
scala> val doubler = (x: Int) => x * 2
doubler: Int => Int = <function1>

scala> val doubled = doubler(22)
doubled: Int = 44

上面这个例子中,函数字面量为(x:Int) => x*2,定义了一个有类型的输入和函数体。

以下是等价的两种函数定义方法:定义函数并赋值到一个函数值;函数字面量重新定义。

代码语言:javascript复制
scala> val maximize: (Int, Int) => Int = max                                (2)
maximize: (Int, Int) => Int = <function2>

scala> val maximize = (a: Int, b: Int) => if (a > b) a else b               (3)
maximize: (Int, Int) => Int = <function2>
scala> val start = () => "=" * 50   "nStarting NOWn"   "=" * 50
start: () => String = <function0>

scala> println( start() )
===================================================
Starting NOW
===================================================

以下stringOp接收一个函数值参数,可以接收一个函数字面量。

代码语言:javascript复制
scala> def safeStringOp(s: String, f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String, f: String => String)String

scala> safeStringOp(null, (s: String) => s.reverse)
res7: String = null

事实上,f的类型定义完之后,可以从函数字面量中删除显式类型。不用写(s: String)

代码语言:javascript复制
scala> safeStringOp("Ready", s => s.reverse)
res10: String = ydaeR

占位符语法

Placeholder Syntax占位符语法是函数字面量。将命名参数替换为通配符“_"。可以在以下情况使用:

  1. 函数的显式类型在字面量之外指定
  2. 参数最多只用1次
代码语言:javascript复制
scala> val doubler: Int => Int = _ * 2
doubler: Int => Int = <function1>
scala> def safeStringOp(s: String, f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String, f: String => String)String

// 等价于一个字面量s => s.reverse
scala> safeStringOp(null, _.reverse)
res11: String = null

如果有多个占位符的话,按位置替换输入参数

代码语言:javascript复制
scala> def combination(x: Int, y: Int, f: (Int,Int) => Int) = f(x,y)
combination: (x: Int, y: Int, f: (Int, Int) => Int)Int

scala> combination(23, 12, _ * _)
res13: Int = 276

使用两个类型参数(表示两个类型(可以不同也可以相同),但是可以代表任何具体类型int,double,boolean......)

代码语言:javascript复制
scala> def tripleOp[A,B](a: A, b: A, c: A, f: (A, A, A) => B) = f(a,b,c)
tripleOp: [A, B](a: A, b: A, c: A, f: (A, A, A) => B)B

scala> tripleOp[Int,Int](23, 92, 14, _ * _   _)
res15: Int = 2130

scala> tripleOp[Int,Double](23, 92, 14, 1.0 * _ / _ / _)
res16: Double = 0.017857142857142856

部分应用一个函数和柯里化

partially apply 这个函数:表示重用一个函数调用,而且希望保留一些参数不想再次输入。

代码语言:javascript复制
scala> def factorOf(x: Int, y: Int) = y % x == 0
factorOf: (x: Int, y: Int)Boolean

scala> val f = factorOf _
f: (Int, Int) => Boolean = <function2>

scala> val x = f(7, 20)
x: Boolean = false

// 使用通配符替代其中一个参数,后面这个:Int可以不加。
scala> val multipleOf3 = factorOf(3, _: Int)
multipleOf3: Int => Boolean = <function1>

scala> val y = multipleOf3(78)
y: Boolean = true

另外一种partially apply的方法是使用有多个参数表的函数。apply一个参数表中的参数二另一个不应用,方便区分。这种技术叫柯里化currying

代码语言:javascript复制
scala> def factorOf(x: Int)(y: Int) = y % x == 0
factorOf: (x: Int)(y: Int)Boolean

scala> val isEven = factorOf(2) _
isEven: Int => Boolean = <function1>

scala> val z = isEven(32)
z: Boolean = true

传名参数

by-name parameter

这个参数可以传进去一个值也可以传进去一个参数。

但要注意函数传入到这个传名参数是,一定要明明白白反复调用这个函数参数可能带来的开销。

格式

代码语言:javascript复制
<identifier>: => <type>
// 函数字面量是 <identifier>:<type> => <type>
// 值是 <identifier>: <type>
scala> def doubles(x: => Int) = {
     |   println("Now doubling "   x)                           
     |   x * 2
     | }

// 可传一个值
scala> doubles(5)                                               
Now doubling 5
res18: Int = 10

scala> def f(i: Int) = { println(s"Hello from f($i)"); i }
f: (i: Int)Int

// 也可以传一个函数。doubles函数调用了x两次,调用了这个传名参数两次。
scala> doubles( f(8) )                                         
Hello from f(8)
Now doubling 8
Hello from f(8)                                                
res19: Int = 16

偏函数

Partial Functions

偏函数可以对输入应用一系列case模式的函数字面量,要求输入至少与给定的pattern之一匹配。如果不匹配,抛出一个错误。

用函数字面量块调用高阶函数

用一个表达式块调用工具函数。我们传入了一个值参数uuid,还传入了一个多行函数字面量。(函数字面量就是s=>输出)

代码语言:javascript复制
scala> val timedUUID = safeStringOp(uuid, { s =>
     |   val now = System.currentTimeMillis          (2)
     |   val timed = s.take(24)   now                (3)
     |   timed.toUpperCase
     | })
timedUUID: String = BFE1DDDA-92F6-4C7A-8BFC-1394546043987

将safeStringOp中的参数分为两个单独的参数组。第二个参数组可以用表达式块语法来调用(第四章 用表达式块调用函数 表达式块计算一个输入)

代码语言:javascript复制
scala> def safeStringOp(s: String)(f: String => String) = {
     |   if (s != null) f(s) else s
     | }
safeStringOp: (s: String)(f: String => String)String

scala> val timedUUID = safeStringOp(uuid) { s =>
     |   val now = System.currentTimeMillis
     |   val timed = s.take(24)   now
     |   timed.toUpperCase
     | }
timedUUID: String = BFE1DDDA-92F6-4C7A-8BFC-1394546915011

来看一个例子,它用到了传名参数,利用类型参数作为这个传名参数(可输入函数或值)的返回类型以及主函数的返回类型。

代码语言:javascript复制
// timer():A= 表示timer主函数的返回类型 
scala> def timer[A](f: => A): A = {                                         (1)
     |   def now = System.currentTimeMillis                                 (2)
     |   val start = now; val a = f; val end = now
     |   println(s"Executed in ${end - start} ms")
     |   a
     | }
timer: [A](f: => A)A

// 用一个表达式块来调用函数,表达式块里是一个函数字面量,作为输入
scala> val veryRandomAmount = timer {                                       (3)
     |   util.Random.setSeed(System.currentTimeMillis)
     |   for (i <- 1 to 100000) util.Random.nextDouble                      (4)
     |   util.Random.nextDouble
     | }
Executed in 13 ms
veryRandomAmount: Double = 0.5070558765221892

0 人点赞