有趣的 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

相关文章

C语言中位操作的实际应用举例

《C语言中位操作的实际应用举例》:本文主要介绍C语言中位操作的实际应用,总结了位操作的使用场景,并指出了需要注意的问题,如可读性、平台依赖性和溢出风险,文中通过代码介绍的非常详细,需要的朋友可以参... 目录1. 嵌入式系统与硬件寄存器操作2. 网络协议解析3. 图像处理与颜色编码4. 高效处理布尔标志集合

Go语言开发实现查询IP信息的MCP服务器

《Go语言开发实现查询IP信息的MCP服务器》随着MCP的快速普及和广泛应用,MCP服务器也层出不穷,本文将详细介绍如何在Go语言中使用go-mcp库来开发一个查询IP信息的MCP... 目录前言mcp-ip-geo 服务器目录结构说明查询 IP 信息功能实现工具实现工具管理查询单个 IP 信息工具的实现服

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

C 语言中enum枚举的定义和使用小结

《C语言中enum枚举的定义和使用小结》在C语言里,enum(枚举)是一种用户自定义的数据类型,它能够让你创建一组具名的整数常量,下面我会从定义、使用、特性等方面详细介绍enum,感兴趣的朋友一起看... 目录1、引言2、基本定义3、定义枚举变量4、自定义枚举常量的值5、枚举与switch语句结合使用6、枚

shell编程之函数与数组的使用详解

《shell编程之函数与数组的使用详解》:本文主要介绍shell编程之函数与数组的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录shell函数函数的用法俩个数求和系统资源监控并报警函数函数变量的作用范围函数的参数递归函数shell数组获取数组的长度读取某下的

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

MySQL中FIND_IN_SET函数与INSTR函数用法解析

《MySQL中FIND_IN_SET函数与INSTR函数用法解析》:本文主要介绍MySQL中FIND_IN_SET函数与INSTR函数用法解析,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一... 目录一、功能定义与语法1、FIND_IN_SET函数2、INSTR函数二、本质区别对比三、实际场景案例分

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序