Scala函数式编程之一——编程范式

本节的内容的有以下几点:
一、编程范式以及为什么要使用函数式编程?
二、什么是函数式编程
三、函数式编程的特征

一、编程范式以及为什么要使用函数式编程?

1、编程范式

我想大家应该在平时工作过程中,也许会因为项目而去另外学习或适应一种自己之前完全不熟悉的编程语言,对有着一定编程经验并已经熟练掌握一门语言的人来说,快速上手一门语言并应用于项目中也许并不是一件很困难的事情。但是情况并非总是如此,跨语言对一个程序员来说影响也许不是最大的,但是编程范式的变更也许会让一个程序员好一会都缓不过神来。

编程范式与编程语言不同,它很深层更内在,它是编程思想的凝练,通过编程语言的体现出来,又通过实践内化为程序员的一种编程思维。它并不容易在短时间内融汇贯通,而需要通过大量地实践加深对这种编程方式的理解。

我们常见的主流编程思维有三种:
1、逻辑式编程
2、命令式编程
3、函数式编程

三种编程范式都体现了各自独特的对用程序解决问题的思考。
1、逻辑式编程不注重解决问题的步骤,而是注重逻辑。它设定答案须符合的规则来解决问题,而非设定步骤来解决问题:规则+事实=结果。利用它编写的程序不是由指令序列组成,而是由一系列公理或定义对象之间关系的规则组。
2、命令式编程关心解决问题的步骤。它需要我们制定好对应解决某问题的一系列步骤,且让程序严格按照步骤去执行。它编写的程序需要我们去考虑在编码范围内需要考虑的一切问题,包括性能,边界验证,资源回收等。
3、函数式编程关心的是数据的映射,它重视更高层面上数据集之间的变换关系,而不是编制程序执行的每一步。它在机器学习算法高度发展的今天,变成了一种算法实现的主要编程范式之一。它的思维方式是将数据集的变换和数据上计算逻辑组合起来产生结果。编码者不需要过多关心数据集中每个元素的具体变换步骤,只需要在数据集合上组织计算逻辑并触发计算。

二、什么是函数式编程

正如上面提到的,函数式编程是一种面向数据映射的编程范式,它的目标是使用纯净的函数来表达问题的解决方式。

所谓函数式编程的函数本质,并不是指我们编程语言中的函数(例如python的def之类的),而是数学中的函数映射,这种映射只是接受参数,并得到一个结果,它并不会对外界产生任何影响,这样的一个函数的好处非常多,它们更有利于模块化,因此更容易测试、复用、并行化、泛化以及推导。

我们可以把函数对外界产生的影响称之为副作用,下面一个例子来说明,带有副作用的函数是如何造成困扰的。

一个简单的副作用例子

我们为玩具店购买玩具来编写一段程序,程序目的是购买一个玩具,并在信用卡上扣费

class Shop{
  def buyToy(cc: CreditCart): Toy = {
    val toy = Toy()
    cc.charge(toy.price) // 副作用的源头
    toy
  }
}
class CreditCart{
  // deduct
  def charge(price: Double) = ???
}
case class Toy(val price: Double = 10)

cc.charge(toy.price)就是副作用的源头,因为信用卡的计费可能会涉及到外部世界的一系列交互,我们的函数只不过想要返回一个玩具,而其它额外的行为也随之发生了,这就是副作用。

这样的副作用导致很难进行测试,因为我们不希望我们的测试方法真的去走一遍信用卡和外部交互的流程。这种对可测试性的修改就意味着设计的修改:按理说CreditCard不应该知道如何去跟信用卡公司去进行实际扣费和持久化计费到内部系统中,我们可以让CreditCard忽略这件事,通过一个Payments接口,与外部交互的逻辑都托管给这个实现这个Payments的对象,然后分别实现一个为真正执行计费逻辑的Payments和一个用于测试的MockPayments。这样的做法使得模块更加模块化和可测试。

class Shop{
  def buyToy(cc: CreditCart, p: Payments): Toy = {
    val toy = Toy()
    p.charge(cc, toy.price)
    toy
  }
}
trait Payments{
  def charge(cc: CreditCart, price: Double)
}

我们这里再考虑一个问题:buyToy方法很难复用!例如一个客户想要购买20个玩具,最理想的是复用这个方法,调用20次进行扣费,不管是从实际意义上的手续费角度,还是从支付系统的调用的性能方面都有十分不理的影响。当然,我们还可以使用一个新的方法buyToys去实现,那么重复的代码逻辑会很多,而且会失去代码复用性和组合性。

去除副作用

函数式的解决方案就是去除副作用,我们可以不需要在买玩具的时候把扣费的逻辑执行了,可以把这个费用本身和玩具一起返回,我们再来改造一下代码:

class Shop{
  def buyToy(cc: CreditCart): (Toy, Charge) = {
    val toy = Toy()
    (toy, Charge(cc, toy.price))
  }
}
case class Charge(cc: CreditCart, amount: Double){
  def combine(other: Charge): Charge = {
    if(cc == other.cc)
      Charge(cc, amount + other.amount)
    else
      throw new RuntimeException("不允许不同信用卡扣费")
  }
}

在这段执行逻辑中,我们并没有在buyToy的方法中进行任何结算费用的操作,而只是返回物品本身和它的费用,我们希望的是把副作用剥离到更外层,而不是在函数调用的过程中进行,那么我们的结算多个Toy的动作也就更好完成了。

def buyToys(cc: CreditCart, n: Int): (List[Toy], Charge) = {
    val purchases : List[(Toy, Charge)] = List.fill(n)(buyToy(cc))
    
    // List[(A, B)] => (List[A], List[B])
    val (toys, charges) = purchases.unzip
    (toys, charges.reduceLeft(_.combine(_))) // 合并消费
  }

现在我们可以把购买玩具的逻辑和付账逻辑隔离开,并可以复用代码实现多个玩具的购买。
相比之前使用Payments接口而言,我们使用Charge作为一等值的来隔离副作用,将
购买->付账(副作用)->得到玩具
的逻辑转变为
(购买->得到账单(可合并)->得到玩具)*->账单一并结账(副作用)

我们可以自己实现一个Payments对象在最后结算Charge里的price,但是Toy类并不需要了解它。

买玩具小结

我们在这个例子中看到如何把计费的创建过程与实际的处理过程进行分离。总的来说,就是把这些副作用推到程序的外层,来转化任何带有副作用的函数。对于优秀的函数式编程者来说,程序的实现就是一层纯的内核和一层很薄的外围来处理副作用。

三、函数式编程的特征

纯函数

我们在前面提到过纯函数的这一概念,这里给出它的精确定义:如果一个函数在程序执行的过程中出了根据输入参数给出结果之外,对外界没有任何其它的影响,那么可以说这一类函数是没有副作用的,这类函数也称为纯函数。

例如Scala中1 + 2(+实际上是一个中置操作符,可以被改写为1.+(2)),那么函数+只接受2为参数,然后与1相加返回一个新的整型3,整个过程没有引入到除了参数和调用者外的任意一个外界变化。

引用透明和替代模型

纯函数为函数式编程带来的一个好处就是:纯函数更容易推理,这就使得我们程序执行的推导过程更为流畅和自然。我们需要走到更高的层次去看看这些好处是怎么来的。

(为了叙述下面的内容,我们先来说明一下编码层次:函数<表达式<程序。)

我们上升至表达式的领域来:对于1 + 2这个表达式,它在任何一个地方都可以被它的结果3直接取代而不会引起程序的任何变更,我们称之为引用透明(表达式层面上的)。当调用一个函数时传入的表达式是引用透明的,并且函数的调用也是引用透明的,那么这个函数就是一个纯函数。纯函数要求无论进行来任何操作都可以用它的返回值来代替它,这种限制使得程序的求值可以通过简单自然的推导得出,我们称之为替代模型(程序层面上的)。如果程序中每个表达式都是引用透明的,那么我们可以使用替代模型来进行等式推理,就例如我们的代数方程一般。

替代模型的之所以很容易进行推理,因为它对运算的影响是局部的,只需要理解局部的计算逻辑,不需要在每一个表达式执行过程中都纵观全局的变化,对于程序的执行可如对代数推理一般流畅而自然地进行。它使得程序进行模块化变得十分简单而清晰,而模块化的函数更容易被测试和进一步的组合,提供程序的整体质量。

小结

总的来说,函数式编程的相对于其它编程范式来说有着它独特的优势,尤其是对于我熟知的命令式范式来说,它展现了一种完全不同的编程思维。在本章笔者也有一些对问题的思考:
去除了副作用之后,所有问题的都有一套函数式的编程方案嘛?
笔者认为,Scala在意的是,如何进行函数式编程,并非所有问题的最佳方案都是使用函数式编程范式解决,函数式范式有自己的适用场景。
引用:
https://www.zhihu.com/question/28292740
《Scala函数式编程》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容