深入理解Scala函数定义:从方法到函数的类型跨越

Java程序员第一次写Scala函数的时候,一定会困惑一件事:为什么Scala的函数写起来这么奇怪?

scala 复制代码
// Java写法
public static int add(int a, int b) {
  return a + b;
}

// Scala写法
def add(a: Int, b: Int): Int = a + b

看起来差不多------参数、返回值、函数体。但真正用下去,你会发现越来越多奇怪的地方:为什么函数可以当参数传?为什么函数可以当返回值?为什么_ + _就是一个函数?为什么map方法接收的不是一个函数名,而是一个函数?

很多Java程序员学Scala函数的时候,记住了语法,但没理解本质。结果就是:写出来的Scala代码还是Java风格------全是def方法,没有函数式味道。

这篇文章不讲"函数怎么定义"这种入门废话,直接说干货:Scala里方法和函数到底差在哪?为什么函数是一等公民?高阶函数到底是什么?


先从一个困惑说起

Java程序员都知道方法(method)------它是类的一部分,你调用它,它干活。

但Scala里多了一个概念叫"函数"(function)。函数和方法到底有什么区别?

scala 复制代码
// 这是一个方法(method)
def add(a: Int, b: Int): Int = a + b

// 这是一个函数(function value)
val addFunc = (a: Int, b: Int) => a + b

看起来都是"两个Int进去,一个Int出来"。但区别在哪?

区别在于:方法是属于类的,函数是一个值。

方法是类的成员------它绑定在某个类上,你通过对象调用它。函数是一个对象------它本身就是一个值,可以赋值给变量、可以作为参数传递、可以作为返回值。

scala 复制代码
// 方法:属于类,不能直接赋值给变量
// val f = add  // 这样写会报错吗?

// 函数:是值,可以随便传
val f = (a: Int, b: Int) => a + b
val g = f  // 函数可以赋值
List(1, 2, 3).map(f)  // 函数可以作为参数

这个区别看起来很小,但它是函数式编程的基石。如果函数不能作为值传递,那map、flatMap、filter这些高阶函数就根本不存在。


方法和函数的关系:它们是"可转换"的

Java程序员最困惑的点就在这:Scala里到底什么时候写方法,什么时候写函数?

答案是:日常开发里,你写def定义的是方法,但在需要函数的地方,Scala会自动把方法"提升"成函数。

scala 复制代码
// 定义一个方法
def add(a: Int, b: Int): Int = a + b

// 这里需要一个函数:(Int, Int) => Int
// Scala自动把add方法转成了函数
val list = List(1, 2, 3)
list.map(x => add(x, 1))  // 这里add是方法,但被当成函数用了

这个过程叫"eta展开"------Scala编译器自动把方法转换成函数值。所以你日常写代码的时候,不需要刻意区分"这是方法还是函数"------编译器会帮你处理。

但理解这个区别很重要:当你需要把函数存起来、传出去的时候,它就必须是函数值,不能是方法。

scala 复制代码
// 方法可以直接调用
add(1, 2)  // 3

// 但方法不能直接赋值给变量
// val f = add  // 这样写Scala会报错,因为add是方法不是函数

// 要把方法转成函数值,加个下划线
val f = add _  // eta展开:方法变成函数值

这个下划线_就是"把方法转成函数"的意思。很多人觉得这个语法奇怪------但它背后的设计意图是:明确告诉编译器,我要的是这个方法的函数值,不是调用这个方法。


函数的本质:它是一个接口

很多人以为函数是个很玄的东西------"函数式编程嘛,函数是一等公民"。但如果你从JVM的角度看,函数一点都不玄。

Scala的函数,本质上就是一个接口。

你定义一个函数(Int, Int) => Int,编译器背后给你生成的是:

scala 复制代码
// 你写的
val addFunc = (a: Int, b: Int) => a + b

// 编译器背后做的
val addFunc = new Function2[Int, Int, Int] {
  def apply(a: Int, b: Int): Int = a + b
}

看到了吗?所谓"函数值",其实就是一个实现了Function2接口的匿名类对象。

这就是为什么函数可以当参数传------因为它是一个对象,一个实现了FunctionN接口的对象。

scala 复制代码
// 你写的
List(1, 2, 3).map(x => x + 1)

// 编译器背后
List(1, 2, 3).map(new Function1[Int, Int] {
  def apply(x: Int): Int = x + 1
})

从这个角度看,Scala的函数和Java的匿名内部类,本质上是同一个东西------只不过Scala给了你一个更简洁的语法糖。

这就是Scala函数的真相:它不是什么魔法,它就是一个实现了FunctionN接口的对象。


函数定义的几种方式

1. 方法定义:def

最常用的方式。定义一个方法,属于当前类/对象。

scala 复制代码
def add(a: Int, b: Int): Int = a + b

def greet(name: String): Unit = {
  println(s"Hello, $name")
}

特点:有名字、有参数类型、有返回类型(可以省略,编译器推断)、函数体用=连接。

2. 函数值定义:val + lambda

定义一个函数值,它是一个对象,可以到处传。

scala 复制代码
val addFunc = (a: Int, b: Int) => a + b

val greetFunc = (name: String) => println(s"Hello, $name")

特点:用val定义(因为它是值),参数和返回类型写在括号里,=>后面是函数体。

3. 匿名函数:_ 简写

当函数参数只有一个,而且只用一次的时候,可以用下划线简写。

scala 复制代码
List(1, 2, 3).map(_ + 1)  // 等价于 map(x => x + 1)

List("a", "b", "c").map(_.toUpperCase)  // 等价于 map(s => s.toUpperCase)

这个写法简洁,但要注意:**下划线代表"参数",而且只能用一次。**如果函数体里同一个参数要用两次,就不能用下划线简写了。

scala 复制代码
// 不能这样写:_用了两次,编译器不知道你指的是哪个
// List(1, 2, 3).reduce(_ + _)  // 这个可以,两个不同的_

// 这个也可以:_出现两次,但reduce需要两个参数
List(1, 2, 3).reduce(_ + _)  // 等价于 (a, b) => a + b

4. 带隐式参数的函数

这个是Scala特有的,在大数据开发里经常用到。

scala 复制代码
def greet(name: String)(implicit prefix: String): Unit = {
  println(s"$prefix, $name")
}

// 调用的时候,第二个参数列表自动找implicit值
implicit val prefix: String = "你好"
greet("Alice")  // 自动传入prefix

这个语法看起来奇怪,但它是Scala做依赖注入的方式------Spark里的SparkSession就是靠implicit传的。


高阶函数:函数当参数传

理解了"函数是一个对象"之后,高阶函数就很好理解了。

高阶函数,就是接收一个函数作为参数,或者返回一个函数的函数。

scala 复制代码
// map就是一个高阶函数
// 它接收一个函数:T => U,然后对每个元素应用这个函数
def map[U](f: T => U): List[U] = ...

// 你用的时候
List(1, 2, 3).map(x => x * 2)
//              ^^^^^^^^^^^^
//              这就是传进去的函数

Java 8也有Lambda,也有Stream.map------那Scala的高阶函数和Java的有什么区别?

区别在于:Scala的函数是从一开始就设计成一等公民的,Java的Lambda是后来加的。

具体来说:

  1. Java的Lambda只能是函数式接口的实例 ------你传个Lambda,背后必须有个@FunctionalInterface接口
  2. Scala的函数就是FunctionN接口 ------它是统一的,不管几个参数,都是Function1、Function2...
  3. Scala支持函数作为返回值------这个Java也有,但Scala用得更自然
scala 复制代码
// 函数作为返回值:柯里化
def multiplyBy(factor: Int): Int => Int = {
  (x: Int) => x * factor
}

val double = multiplyBy(2)  // 返回一个函数:Int => Int
double(5)  // 10

val triple = multiplyBy(3)
triple(5)  // 15

这个叫"函数工厂"------你给它一个参数,它给你返回一个函数。这种写法在Java里也能写,但Scala的语法让它自然得多。


写在最后

我写这篇文章的时候,特意没罗列函数有多少种写法------因为那些语法东西,查文档就有。

真正值得花时间理解的是:Scala为什么要有函数这个概念?

不是因为"函数式编程嘛,函数是一等公民"------这句话说了等于没说。而是因为:当你需要对一组数据做变换的时候,你需要一个"可以传进去的操作"。

Java里你怎么做?你定义一个接口,然后实现它,然后传进去------这就是匿名内部类。Scala给了你一个更简洁的方式:lambda表达式。但本质是一样的------都是传一个"操作"进去。

函数的本质,就是一个可以传递的操作。

理解了这个,你就理解了Scala函数的设计哲学。你不会再纠结"为什么Scala的函数写起来这么奇怪"------因为你知道,那不是奇怪,那是在JVM之上,给函数式编程搭的一层抽象。

Java 8加了Lambda,加了Stream------但Java的设计思路是"给对象加函数能力",Scala的设计思路是"函数本身就是对象"。出发点不一样,写出来的代码感觉也不一样。

这就是为什么Spark用Scala写起来那么优雅------因为你可以把一个函数直接传给RDD,不需要定义接口,不需要实现匿名类,一行lambda就搞定了。

函数不是魔法,它就是一个实现了FunctionN接口的对象。理解了这一点,你就理解了Scala函数的全部。

相关推荐
starzy199014 分钟前
深入理解Scala递归函数:从栈溢出到尾调用优化
开发语言·scala
三*一23 分钟前
Mapbox‑GL JS 多边形折点编辑工具:实现原理、功能拆解与踩坑总结
开发语言·javascript·ecmascript
YSL07012435 分钟前
C++基础
开发语言·c++
若亦_42 分钟前
Java中关于this的三种用法
java·开发语言·intellij-idea
2601_968793141 小时前
Python Requests 轻量自动化:无需浏览器,实现真人级账号浏览模拟
开发语言·python·自动化
eggwyw2 小时前
关于C++为什么不加入垃圾回收机制解析
开发语言·c++
来让爷抱一个13 小时前
Python 异步里混用同步库,事件循环被卡住的全过程
开发语言·python
心祭丫丫15 小时前
【每天一个 Python 小案例17】Python 实现通讯录
开发语言·人工智能·笔记·python·学习
露天赏雪16 小时前
第3篇:Sentinel熔断降级实战:大模型接口抖动时的服务兜底方案
java·开发语言·人工智能·spring boot·spring·servlet·sentinel