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

相关文章

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

nudepy,一个有趣的 Python 库!

更多资料获取 📚 个人网站:ipengtao.com 大家好,今天为大家分享一个有趣的 Python 库 - nudepy。 Github地址:https://github.com/hhatto/nude.py 在图像处理和计算机视觉应用中,检测图像中的不适当内容(例如裸露图像)是一个重要的任务。nudepy 是一个基于 Python 的库,专门用于检测图像中的不适当内容。该

C++操作符重载实例(独立函数)

C++操作符重载实例,我们把坐标值CVector的加法进行重载,计算c3=c1+c2时,也就是计算x3=x1+x2,y3=y1+y2,今天我们以独立函数的方式重载操作符+(加号),以下是C++代码: c1802.cpp源代码: D:\YcjWork\CppTour>vim c1802.cpp #include <iostream>using namespace std;/*** 以独立函数

函数式编程思想

我们经常会用到各种各样的编程思想,例如面向过程、面向对象。不过笔者在该博客简单介绍一下函数式编程思想. 如果对函数式编程思想进行概括,就是f(x) = na(x) , y=uf(x)…至于其他的编程思想,可能是y=a(x)+b(x)+c(x)…,也有可能是y=f(x)=f(x)/a + f(x)/b+f(x)/c… 面向过程的指令式编程 面向过程,简单理解就是y=a(x)+b(x)+c(x)

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目: 题解: static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num & MASK1) == 0) {return

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

C语言:柔性数组

数组定义 柔性数组 err int arr[0] = {0}; // ERROR 柔性数组 // 常见struct Test{int len;char arr[1024];} // 柔性数组struct Test{int len;char arr[0];}struct Test *t;t = malloc(sizeof(Test) + 11);strcpy(t->arr,