有趣的 Scala 语言: 函数成了一等公民

2024-01-28 05:30

本文主要是介绍有趣的 Scala 语言: 函数成了一等公民,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


Scala 是一种有趣的语言。它一方面吸收继承了多种语言中的优秀特性,一方面又没有抛弃 Java 这个强大的平台,它运行在 JVM 之上,轻松实现和丰富的 Java 类库互联互通。它既支持面向对象的编程方式,又支持函数式编程。它写出的程序像动态语言一样简洁,但事实上它却是严格意义上的静态语言。Scala 就像一位武林中的集大成者,将过去几十年计算机语言发展历史中的精萃集于一身,化繁为简,为程序员们提供了一种新的选择。作者希望通过这个系列,可以为大家介绍 Scala 语言的特性,和 Scala 语言给我们带来的关于编程思想的新的思考。本文将带领大家一起回顾函数式编程的历史,清楚函数式编程的定义,并以一个例子,由易到难为大家展示函数式编程的优点,最后介绍了柯里化的概念。


  • expand内容

函数式编程是这几年很受欢迎的一个话题,即使你是一个刚刚踏入职场的新人,如果在面试时能有意无意地透露出你懂那么一点点函数式编程,也会让你的面试官眼前一亮。然而函数式编程并不是一个新的概念,它的源头可以追溯到计算机尚未发明之前。本文将带领大家回顾一下函数式编程的历史,并使用 Scala 语言为大家讲解函数式编程的基本概念。

函数式编程的历史

有机会看到这篇文章的读者,大概都会知道阿兰·图灵(Alan Turing)和约翰·冯·诺伊曼(John von Neumann)。阿兰·图灵提出了图灵机的概念,约翰·冯·诺伊曼基于这一理论,设计出了第一台现代计算机。由于图灵以及冯·诺伊曼式计算机的大获成功,历史差点淹没了另外一位同样杰出的科学家和他的理论,那就是阿隆佐·邱奇(Alonzo Church)和他的λ演算。阿隆佐·邱奇是阿兰·图灵的老师,上世纪三十年代,他们一起在普林斯顿研究可计算性问题,为了回答这一问题,阿隆佐·邱奇提出了λ演算,其后不久,阿兰·图灵提出了图灵机的概念,尽管形式不同,但后来证明,两个理论在功能上是等价的,条条大路通罗马。如果不是约翰·麦卡锡(John McCarthy),阿隆佐·邱奇的λ演算恐怕还要在历史的故纸堆中再多躺几十年,约翰·麦卡锡是人工智能科学的奠基人之一,他发现了λ演算的珍贵价值,发明了基于λ演算的函数式编程语言:Lisp,由于其强大的表达能力,一推出就受到学术界的热烈欢迎,以至于一段时间内,Lisp 成了人工智能领域的标准编程语言。很快,λ演算在学术界流行开来,出现了很多函数式编程语言:Scheme 、SML、Ocaml 等,但是在工业界,还是命令式编程语言的天下,Fortran、C、C++、Java 等。随着时间的流逝,越来越多的计算机从业人员认识到函数式编程的意义,爱立信公司于上世纪八十年代开发出了 Erlang 语言来解决并发编程的问题;在互联网的发展浪潮中,越来越多的语言也开始支持函数式编程:JavaScript、Python、Ruby、Haskell、Scala 等。可以预见,如果过去找一个懂什么是函数式编程的程序员很困难,那么在不久的将来,找一个一点也没听过函数式编程的程序员将更加困难。

什么是函数式编程

狭义地说,函数式编程没有可变的变量、循环等这些命令式编程方式中的元素,像数学里的函数一样,对于给定的输入,不管你调用该函数多少次,永远返回同样的结果。而在我们常用的命令式编程方式中,变量用来描述事物的状态,整个程序,不过是根据不断变化的条件来维护这些变量。

广义地说,函数式编程重点在函数,函数是这个世界里的一等公民,函数和其他值一样,可以到处被定义,可以作为参数传入另一个函数,也可以作为函数的返回值,返回给调用者。利用这些特性,可以灵活组合已有函数形成新的函数,可以在更高层次上对问题进行抽象。本文的重点将放在这一部分。

函数式编程有什么优点

约翰·巴克斯(John Backus)为人熟知的两项成就是 FORTRAN 语言和用于描述形式系统的巴克斯范式,因为这两项成就,他获得了 1977 年的图灵奖。有趣的是他在获奖后,做了一个关于函数式编程的讲演:Can Programming Be Liberated From the von Neumann Style? 1977 Turing Award Lecture。他认为像 FORTRAN 这样的命令式语言不够高级,应该有新的,更高级的语言可以摆脱冯诺依曼模型的限制,于是他又发明了 FP 语言,虽然这个语言未获成功,但是约翰·巴克斯关于函数式编程的论述却得到了越来越多的认可。下面,我们就罗列一些函数式编程的优点。

首先,函数式编程天然有并发的优势。由于工艺限制,摩尔定律已经失效,芯片厂商只能采取多核策略。程序要利用多核运算,必须采取并发,而并发最头疼的问题就是共享数据,狭义的函数式编程没有可变的变量,数据只读不写,并发的问题迎刃而解。这也是前面两篇文章中,一直建议大家在定义变量时,使用 val 而不是 var 的原因。爱立信公司发明的 Erlang 语言就是为解决并发的问题而生,在电信行业取得了不俗的成绩。

其次,函数式编程有迹可寻。由于不依赖外部变量,给定输入函数的返回结果永远不变,对于复杂的程序,我们可以用值替换的方式(substitution model)化繁为简,轻松得出一段程序的计算结果。为这样的程序写单元测试也很方便,因为不用担心环境的影响。

最后,函数式编程高屋建瓴。写程序最重要的就是抽象,不同风格的编程语言为我们提供了不同的抽象层次,抽象层次越高,表达问题越简洁,越优雅。读者从下面的例子中可以看到,使用函数式编程,有一种高屋建瓴的感觉。

抽象,抽象,再抽象!

说了这么多,相信很多性急的读者都等不及想看看怎么使用 Scala 进行函数式编程了吧。那么,先请大家暂时忘掉以前命令式编程的经验,用一个全新的大脑来开始这段函数式编程之旅。

故事从我上初中的外甥小龙身上开始,像所有聪明的孩子一样,小龙身上具备了懒,不耐烦以及妄自尊大这些优秀特质。他厌倦了数学作业上那些大量没有意义的,重复的练习题。还好他有个当程序员的姨夫:在电脑上装个 Scala,写程序做吧。于是小龙把 Scala 当作一个计算器,写出了他有生以来第一段程序:

清单 1. 求立方
        
35 * 35 * 35
68 * 68 * 68
// 以下省去大量重复的,没有意义的练习题

作业做完了,虽然大脑得到了休息,但是小龙的手累坏了!作为一个懒人,小龙是不会满足于不动脑,但要动手这种状况的。于是,我教给了他最基本的抽象方式:将算法抽象为一个函数。小龙很快做完作业,高高兴兴跟小伙伴们打篮球去了。

清单 2. 求立方函数
        
def cube(n: Int) = n * n * n
// 有了这个函数,小龙做起作业轻松多了
// 以下省去大量重复的,没有意义的练习题
cube(35)
cube(68)

随着教学进度的加快,小龙的作业也越来越难了,很快,小龙遇到了这样的题目:求出 1 到 10 的立方和。聪明如小龙,或者说懒惰如小龙,在前一个函数基础之上,很快又定义了个新函数,还是个递归函数!没错,在小龙还没看见过循环之前,我先教会了他递归,他理解起来毫不费力:对 a 到 b 之间的数求立方和,等于 a 的立方和,加上 (a + 1) 到 b 之间的数的立方和。如果读者对于递归还有疑惑,请参考作者的前一篇文章《使用递归的方式去思考》。小龙又很快做完作业,高高兴兴跟着小伙伴们打球去了。

清单 3. 求立方和
        
def cube(n: Int) = n * n * n
def sumCube(a: Int, b: Int): Int =
if (a > b) 0 else cube(a) + sumCube(a + 1, b)
// 有了这个函数,小龙做起作业轻松多了
sumCube(1, 10)

塞翁失马,焉知非福,好事很快变坏事,由于小龙数学作业做得又快又好,被老师选拔为奥数培养对象,除过作业,小龙每天还要做大量的额外练习:求 1 到 10 的和,求 1 到 10 的平方和,求 1 到 10 的阶乘和等等。这时,小龙已经对定义函数很熟练了,三下五除二,小龙又定义出一堆函数出来。

清单 4. 各种求和函数
        
def cube(n: Int) = n * n * n
def id(n: Int) = n
def fact(n: Int): Int =
def square(n : Int) = n * n
def sumCube(a: Int, b: Int): Int =
if (n == 0) 1 else n * fact(n - 1)
def sumSquare(a: Int, b: Int): Int =
if (a > b) 0 else cube(a) + sumCube(a + 1, b)
def sumFact(a: Int, b: Int): Int =
if(a > b) 0 else square(a) + sumSquare(a + 1, b)
if(a > b) 0 else id(a) + sumInt(a + 1, b)
if (a > b) 0 else fact(a) + sumFact(a + 1, b) def sumInt(a: Int, b: Int): Int =
sumFact(1, 10)
// 有了这些函数,小龙做起作业轻松多了sumCube(1, 10) sumInt(1, 10)
sumSquare(1, 10)

问题解决了,但小龙总觉得哪里不对劲,(这时,一个画外音高喊:Don ’ t Repeat Yourself!),是的,仔细观察小龙定义的这四个求和函数,几乎是一模一样的。能不能也将这些一模一样的东西抽象出来?我觉得是时候教给他第二项本领了:高阶函数(Higher-Order Function),所谓高阶函数,就是操作其他函数的函数。以求和为例,我们可以定义一个新的求和函数,该函数接受另外一个函数作为参数,这个作为参数的函数代表了某种对数据的操作。使用高阶函数后,抽象层次提高,代码变得更简单了。

清单 5. 使用高阶函数定义求和函数
        
def cube(n: Int) = n * n * n
def id(n: Int) = n
def fact(n: Int): Int =
def square(n : Int) = n * n
def sum(f: Int => Int, a: Int, b: Int): Int =
if (n == 0) 1 else n * fact(n - 1) // 高阶函数
def sumCube(a: Int, b: Int): Int = sum(cube, a, b)
if (a > b) 0 else f(a) + sum(f, a + 1, b) // 使用高阶函数重新定义求和函数
def sumFact(a: Int, b: Int): Int = sum(fact, a, b)
def sumSquare(a: Int, b: Int): Int = sum(square, a, b) def sumInt(a: Int, b: Int): Int = sum(id, a, b)
sumFact(1, 10)
// 有了这些函数,小龙做起作业轻松多了sumCube(1, 10) sumInt(1, 10)
sumSquare(1, 10)

对于简单的函数,我们还可以将其转化为匿名函数,让程序变得更简洁一些。在高阶函数中使用匿名函数,这是函数式编程中经常用到的一个技巧,多数情况下,我们关心的是高阶函数,而不是作为参数传入的函数,所以为其单独定义一个函数是没有必要的。值得称赞的是 Scala 中定义匿名函数的语法很简单,箭头左边是参数列表,右边是函数体,参数的类型是可省略的,Scala 的类型推测系统会推测出参数的类型。使用匿名函数后,我们的代码变得更简洁了:

清单 6. 在高阶函数中使用匿名函数
        
def fact(n: Int): Int =
if (n == 0) 1 else n * fact(n - 1)
// 高阶函数
def sum(f: Int => Int, a: Int, b: Int): Int =
if (a > b) 0 else f(a) + sum(f, a + 1, b)
def sumCube(a: Int, b: Int): Int = sum(x => x * x * x, a, b)
// 使用高阶函数重新定义求和函数
def sumFact(a: Int, b: Int): Int = sum(fact, a, b)
def sumSquare(a: Int, b: Int): Int = sum(x => x * x, a, b)
sumCube(1, 10)
def sumInt(a: Int, b: Int): Int = sum(x => x, a, b) // 有了这些函数,小龙做起作业轻松多了sumInt(1, 10)
sumFact(1, 10)
sumSquare(1, 10)

小龙的故事到此就结束了,希望读者能从这一例子中,体会出函数式编程的一些精妙之处。下面我们将进入函数式编程的另一个概念:柯里化(Currying)

柯里化

作为一个程序员,应该永远有一颗追求完美的心,上面使用匿名函数后的高阶函数还有什么地方值得改进呢?希望大家还会想起那句话:Don ’ t Repeat Yourself !求和函数的两个上下限参数 a,b被重复得传来传去。我们试着重新定义 sum函数,让它接受一个函数作为参数,同时返回另外一个函数。看到没?使用新的 sum函数,我们再定义各种求和函数时,完全不需要这两个上下限参数了,我们的程序又一次得到了简化。

清单 7. 返回函数的高阶函数
        
def fact(n: Int): Int =
if (n == 0) 1 else n * fact(n - 1)
// 高阶函数
def sum(f: Int => Int): (Int, Int) => Int = {
def sumF(a: Int, b: Int): Int =
}
if (a > b) 0 else f(a) + sumF(a + 1, b) sumF // 使用高阶函数重新定义求和函数
def sumFact: Int = sum(fact)
def sumCube: Int = sum(x => x * x * x) def sumSquare: Int = sum(x => x * x) def sumInt: Int = sum(x => x)
sumFact(1, 10)
// 这些函数使用起来还和原来一样 ! sumCube(1, 10) sumInt(1, 10)
sumSquare(1, 10)

能不能再简单一点呢?既然 sum返回的是一个函数,我们应该可以直接使用这个函数,似乎没有必要再定义各种求和函数了。

清单 8. 直接调用高阶函数
        
def fact(n: Int): Int =
if (n == 0) 1 else n * fact(n - 1)
// 高阶函数
def sum(f: Int => Int): (Int, Int) => Int = {
def sumF(a: Int, b: Int): Int =
}
if (a > b) 0 else f(a) + sumF(a + 1, b) sumF // 这些函数没有必要了
//def sumSquare: Int = sum(x => x * x)
//def sumCube: Int = sum(x => x * x * x) //def sumFact: Int = sum(fact) //def sumInt: Int = sum(x => x)
sum(x => x) (1, 10) //=> sumInt(1, 10)
// 直接调用高阶函数 ! sum(x => x * x * x) (1, 10) //=> sumCube(1, 10) sum(x => x * x) (1, 10) //=> sumSquare(1, 10)
sum(fact) (1, 10) //=> sumFact(1, 10)

这种返回函数的高阶函数极为有用,因此 Scala 为其提供了语法糖,上面的 sum函数可以简写为:

清单 9. 高阶函数的语法糖
        
// 没使用语法糖的 sum 函数
def sum(f: Int => Int): (Int, Int): Int = {
def sumF(a: Int, b: Int): Int =
sumF
if (a > b) 0 else f(a) + sumF(a + 1, b) } // 使用语法糖后的 sum 函数
if (a > b) 0 else f(a) + sum(f)(a + 1, b)
def sum(f: Int => Int)(a: Int, b: Int): Int =

读者可能会问:我们把原来的 sum函数转化成这样的形式,好处在哪里?答案是我们获得了更多的可能性,比如刚开始求和的上下限还没确定,我们可以在程序中把一个函数传给 sumsum(fact)完全是一个合法的表达式,待后续上下限确定下来时,再把另外两个参数传进来。对于 sum 函数,我们还可以更进一步,把 a,b 参数再转化一下,这样 sum 函数就变成了这样一个函数:它每次只能接收一个参数,然后返回另一个接收一个参数的函数,调用后,又返回一个只接收一个参数的函数。这就是传说中的柯里化,多么完美的形式!在现实世界中,的确有这样一门函数式编程语言,那就是 Haskell,在 Haskell 中,所有的函数都是柯里化的,即所有的函数只接收一个参数!

清单 10. 柯里化
        
// 柯里化后的 sum 函数
def sum(f: Int => Int)(a: Int) (b: Int): Int =
if (a > b) 0 else f(a) + sum(f)(a + 1)(b)
sum(x => x * x * x)(1)(10) //=> sumCube(1, 10)
// 使用柯里化后的高阶函数 !
sum(x => x)(1)(10) //=> sumInt(1, 10)

结束语

本文和大家一起回顾了函数式编程的历史,并使用了大量示例代码帮助大家理解函数式编程中的基本概念。在 Scala 类库中,使用函数式编程的例子比比皆是,特别是对于列表的操作,将高阶函数的优势展示得淋漓尽致,限于篇幅,不能在本文中为大家作以介绍,作者将在后面的系列文章中,以 Scala 中的列表为例,详细介绍高阶函数在实战中的应用。


转自转载链接


参考资料

学习

  • Programming in Scala, First Edition,学习 Scala 的参考书籍,可在网上免费阅读。
  • Learning Scala in small bites,提供了很多例子展示 Scala 的语法。

这篇关于有趣的 Scala 语言: 函数成了一等公民的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/652597

相关文章

从基础到高级详解Go语言中错误处理的实践指南

《从基础到高级详解Go语言中错误处理的实践指南》Go语言采用了一种独特而明确的错误处理哲学,与其他主流编程语言形成鲜明对比,本文将为大家详细介绍Go语言中错误处理详细方法,希望对大家有所帮助... 目录1 Go 错误处理哲学与核心机制1.1 错误接口设计1.2 错误与异常的区别2 错误创建与检查2.1 基础

Python函数作用域与闭包举例深度解析

《Python函数作用域与闭包举例深度解析》Python函数的作用域规则和闭包是编程中的关键概念,它们决定了变量的访问和生命周期,:本文主要介绍Python函数作用域与闭包的相关资料,文中通过代码... 目录1. 基础作用域访问示例1:访问全局变量示例2:访问外层函数变量2. 闭包基础示例3:简单闭包示例4

Python中isinstance()函数原理解释及详细用法示例

《Python中isinstance()函数原理解释及详细用法示例》isinstance()是Python内置的一个非常有用的函数,用于检查一个对象是否属于指定的类型或类型元组中的某一个类型,它是Py... 目录python中isinstance()函数原理解释及详细用法指南一、isinstance()函数

python中的高阶函数示例详解

《python中的高阶函数示例详解》在Python中,高阶函数是指接受函数作为参数或返回函数作为结果的函数,下面:本文主要介绍python中高阶函数的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录1.定义2.map函数3.filter函数4.reduce函数5.sorted函数6.自定义高阶函数

Go语言中json操作的实现

《Go语言中json操作的实现》本文主要介绍了Go语言中的json操作的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录 一、jsOChina编程N 与 Go 类型对应关系️ 二、基本操作:编码与解码 三、结构体标签(Struc

Python中的sort方法、sorted函数与lambda表达式及用法详解

《Python中的sort方法、sorted函数与lambda表达式及用法详解》文章对比了Python中list.sort()与sorted()函数的区别,指出sort()原地排序返回None,sor... 目录1. sort()方法1.1 sort()方法1.2 基本语法和参数A. reverse参数B.

python语言中的常用容器(集合)示例详解

《python语言中的常用容器(集合)示例详解》Python集合是一种无序且不重复的数据容器,它可以存储任意类型的对象,包括数字、字符串、元组等,下面:本文主要介绍python语言中常用容器(集合... 目录1.核心内置容器1. 列表2. 元组3. 集合4. 冻结集合5. 字典2.collections模块

基于Go语言开发一个 IP 归属地查询接口工具

《基于Go语言开发一个IP归属地查询接口工具》在日常开发中,IP地址归属地查询是一个常见需求,本文将带大家使用Go语言快速开发一个IP归属地查询接口服务,有需要的小伙伴可以了解下... 目录功能目标技术栈项目结构核心代码(main.go)使用方法扩展功能总结在日常开发中,IP 地址归属地查询是一个常见需求:

Python函数的基本用法、返回值特性、全局变量修改及异常处理技巧

《Python函数的基本用法、返回值特性、全局变量修改及异常处理技巧》本文将通过实际代码示例,深入讲解Python函数的基本用法、返回值特性、全局变量修改以及异常处理技巧,感兴趣的朋友跟随小编一起看看... 目录一、python函数定义与调用1.1 基本函数定义1.2 函数调用二、函数返回值详解2.1 有返

Python Excel 通用筛选函数的实现

《PythonExcel通用筛选函数的实现》本文主要介绍了PythonExcel通用筛选函数的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着... 目录案例目的示例数据假定数据来源是字典优化:通用CSV数据处理函数使用说明使用示例注意事项案例目的第一