【Scala】从函数字面量发现函数式编程

函数字面量”这个概念,我个人倾向于这样解释:它是基于某类函数的“类型”声明,以内联(in-line)方式写成的对应该函数类型的一个“值”(或者称实例)!就像我们在Scala里定义一个普通变量“var num:Int=1”一样,数字“1”就是一个Int型的“字面量”,它代表着一个Int类型的“值”。

  • 过去传统的非函数式编程语言里,函数就是函数,是一种包含了函数名,参数列表,返回值和函数体的代码结构
  • 进入到函数式编程语言的范畴之后,一些“实质性”的东西发生了变化,函数的地位被抬升到前所未有的高度,它变成了一类(first-class)的,变得和其他的数据类型在使用方式上完全一致了

函数的“类型”和“值”:从何而来?

什么是函数的“类型”以及如何去描述它。老实说,这个问题并不难解决,因为函数的类型“特征”显然体现在它的参数和返回值的类型上,如果我们把这些类型从函数定义中提取出来,然后按照函数签名的形式排列好,我相信大多数人是可以很容易地理解并接受这种描述方法的,而在Scala里,也确实是这样做的。

  • Scala编写的传统形式的函数定义
def plusOne(num:Int):Int = {
    num+1
}
  • 这个函数将传入的Int参数加1之后返回,按照我们前面提到的类型提取原则,这个函数的“类型”可以这样描述:
(Int)=>Int
  • 多个参数类型之间还是使用逗号分隔,向右的箭头作为参数列表和返回值之间的分割,箭头右侧就是返回值的类型。这个“类型”描述了这样“一类”函数(记住,是一类函数而不是一个函数,这很重要

  • 按照前面提取函数类型的做法从函数定义中剔除类型声明的部分,那么剩下的自然就是关于“值”的部分了

(num)=>{
    num+1
}

//这段代码里唯一做的一点调整就是把函数定义中的参数列表和函数体之间的“=”换成了“=>”,
// 是的,这是为了迎合Scala的语法要求,而这个形式也与前面的类型声明形式相呼应
  • 把函数的“类型”和“值”联合起来,看看一个函数是如何像一个普通的变量那样被定义出来的
val plusOne:Int=>Int={
    (num)=>num+1
}

// 多个参数
val pow2:(Int,Int)=>Int={
    (num,num1)=>num*num1
}

我们定义了一个变量(确切地说是一个不允许二次赋值的变量(single assignment variable)):plusOne,“Int=>Int”是这个变量的类型,很显然它指代的是一类函数,等号右侧是这个变量的值,也就是一个函数字面量。

变量plusOne完全等同于前面定义的函数plusOne,从某种角度上说,变量plusOne的这种函数存在形态才是函数式编程语言对函数最本质的认知。这样的存在形态印证了函数式编程将函数视作一类(first-class)的信条,因为它从定义到使用都和其他的类型与值没有任何区别了

像传值一样传递函数:一场“裂变”式的能量释放

如果说这样的分离仅仅让我们像使用其他数据类型一样使用函数,那我实在看不出如此大费周章的价值在哪里

  • 在某个需要声明函数的地方声明一个函数类型,在调用的时候传一个对应的函数字面量,这样“曲折”的做法究竟好在哪里?
  • 看上去,必须要有一个强有力的证据来揭示一些我们还没有意识到的东西,而我相信,最好的例证莫过于“高阶函数”了。
  • “高阶函数”并不是一个高深的概念,一个接受其他函数作参数或着返回一个函数的函数就是高阶函数
def hof(list:List[Int],f:(Int)=>Int):List[Int]={
    list match {
        case List() => Nil
        case head :: tail => f(head) :: hof(tail, f)
    }
}

hof是一个高阶函数,它接受一个List参数和一个函数参数,这个函数参数本身接受一个Int型参数并返回一个Int型的结果,hof的工作就是取出list中的每个元素交给f处理, 将处理后的结果放入一个新的List并返回。

实际上你大概已经看出来了,这个hof其实是集合类型里自带的map函数的一个简化版本,但是我们不会在这里讨论map函数,我们只是想借这个简化的例子帮我们发现我们正在寻找的东西。

回头看我们前面刚刚定义过的函数变量plusOne,它正是hof可以接受的那类函数,所以我们把它传给hof看一下:

scala> hof(List(1,2,3),plusOne)
//返回
res0: List[Int] = List(2, 3, 4)

一个新的List返回了,每一个元素都加了1。回到我们前面质疑的地方,如果我们不让hof接受一个函数参数,而是直接在函数实现里去调用plusOne这个函数,那么我们会得到一个非高阶函数的实现:nonHof

def nonHof(list:List[Int]):List[Int]={
    list match {
        case List() => Nil
        case head :: tail => plusOne(head) :: nonHof(tail)
    }
}

OK,这也没有什么问题,返回的结果都是一样的,

  • 那么差别在哪里?
  • hof之于nonHof到底有什么优势?
  • 我想我们的讨论已经无限接近这篇文章将要触及的最核心的部分了,那么,让我们再引入一个函数吧,关键的角色要登场了:
val double:Int=>Int={
    (num)=>num*2
}

新函数double,它把输入的Int参数乘以2并返回,看看它的参数类型“Int=>Int”,和plusOne是同一类型的,同时也是高阶函数hof接受的第二参数类型,这是否意味着double也可以传给hof呢?让我们试试看:

scala> hof(List(1,2,3),double)
res1: List[Int] = List(2, 4, 6)

我们没有对hof进行任何的改动,但它现在看起来却像“变了个人一样”,它有了新的“玩法”,它的处理逻辑发生了明显的变化,虽然整体的处理流程没有改变,也就是依次迭代每一个元素进行处理,但是在局部,它对每一个元素的处理方式已经彻底改变了。

你有没有看到函数hof蕴含的巨大潜力?它规定了对一个集合的框架性处理流程,却不指明对个体元素的处理方法,而是改由调用方基于自身的需要在调用时动态传入。

这意味着函数hof的每一次调用,不只是处理的数据可能不同,连处理的逻辑也会发生变化,甚至相同的数据也可以按不同的方法去处理。

差异分析

反观函数nonHof,在同样的场景下,它具有和hof一样对局部处理逻辑进行“切换”和“插拔”的能力吗?显然不行!

由于它在函数体内调用的是“一个”具体的函数实现,这就等同于把这个函数的实现代码以硬编码(hard code)的方式嵌入到了函数体内。

从更高的抽象层次看,在一个函数里直接调用另一个已定义的函数实际上是完全“固化”了产生和处理这个中间(局部)值的逻辑和算法,这个中间(局部)值不可能再通过另外一套不同逻辑和算法来产生了,在这种情况下,开发者要么修改现有的nonHof,要么针对double需求提供另外一个版本的nonHof实现。

或许具有深厚OO背景且目光敏锐的程序员会立即反应过来,如果在OO语言里,这是一个典型的可以使用“模板方法+策略”模式进行重构的场景,最终一样可以实现对局部算法的动态“切换”和“插拔”,

但是如果我们把hof和经过OO优化设计的方案相比较,你会发现后者显得太笨重了,而且针对每一种策略的实现也都是预定义的,这与在函数式编程语言里随手就可以写好的一个函数字面量比起来,灵活性和开发效率上都相差甚远。

函数“类型”和“值”的分离,使得函数可以作为一个值(函数字面量)传递给其他的高阶函数,高阶函数的行为会因为传入函数的不同而表现地高度灵活和多变,也可以说具有了某种“动态”特性;另一方面,由于变化的部分都剥离给了外部传入的函数,高阶函数自身就变得高度可复用。

所有这些都是函数式编程天然的优势,是在语言层面上直接支持的,你甚至不需要精心设计你的程序就可以轻易获取这些优势

结语

“类型”归类型,“值”归值,“类型”与“值”的分离是函数跻身一等公民的必然结果,而这样的切分使函数可以按“类型”声明,按“值”传递,彻底解放了函数的使用方式,在高阶函数里,函数的这个特性被发挥到了极致,从而彻底改变了编程的思维和模式,造就了今天的函数式编程。

我的理解

函数式编程将函数的类型(函数的参数类型和返回值类型),(参数值与返回值的总和[函数字面量])分离开,将函数的定义作为一类函数,而不是一个函数。这样的函数就可以像函数像普通的类型一样传递。一类函数,有千变万化的实现。但却有灵活的插拔能力。函数式编程是一个从值=>值的过程,是一条线!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值