我爱“不可变编程”,但我更爱Binding.scala的Var

291 阅读3分钟
原文链接: zhuanlan.zhihu.com
最近在Coursera上学习Scala的课程,在一次课程的作业中,有一个函数的实现在Instruction中特别指出使用while循环实现,然而之前的作业中是从未使用while循环的,同样的功能通常使用递归形式或者foreach、map等函数来实现,避开使用变量。
这时突然提出要用while循环来做,那就避免不了使用var变量了,然而这完全与函数式编程的理念相悖。于是有小伙伴提出了问题,最后的几张图是教员的回复,大概意思是说Scala中for“循环”会被展开成foreach的调用,中间会有成吨的性能损耗,而且foreach底层实现也是用while循环,干脆直接用while循环算了。。。
另外也有小伙伴表示,Scala对与简单for循环没有优化性能极差,是一个众所周知的问题,并甩出了一个ISSUE的链接。。。https://issues.scala-lang.org/browse/SI-1338

我推荐的风格是所以情况都不用while和局部变量var,可以用foldLeft和尾递归代替。

foldLeft性能差一点,适合性能不敏感的场合。尾递归性能和while差不多。

可读性上来讲,foldLeft和尾递归不比while差,但可能初学者需要一点时间适应一下。

从保证程序的正确性上讲,无论尾递归还是while都有可能写错,写成死循环,foldLeft却能保证循环的次数,不可能写成死循环。再者,var还有额外的风险,如果写错了的话,可能会不小心改错了值。

所以我推荐所有场合都不用while和局部变量var,一律用foldLeft和尾递归即可。


我只强调“局部变量var”一律用foldLeft和尾递归代替,是因为类里面的var和可变数据结构更棘手些,不可一概而论。

棘手的原因有三点:

  1. 类里的var造成的潜在危害很大,可能在并发或者重入的情况下,导致很难解决的bug。
  2. 不容易完全消除类里的var,如果一定要消除,会导致代码变长、性能变差。
  3. 如果你本来就是为可变模型建模,用var最顺手。

所以,类里的var是架构设计问题,而不只是编码问题。

举个例子,我在Binding.scala中做了一个艰难的设计决策。我让Binding.scala的内部数据在初始构建数据绑定表达式时不可变,但在运行时可变。

我在数据绑定表达式的生命周期管理一节写到:

简而言之,Binding.scala 将函数分为两类:
  • 用户定义的 @dom 方法,用于产生不带副作用的纯函数式的数据绑定表达式。
  • 调用 dom.render 以及 Binding.watch 的函数,自动地管理所有副作用。


可能某些人会诟病Binding.scala的数据模型不够“purity”,因为运行起来会变。

但实际上用起来,这种混合做法却很顺畅。因为用户定义的数据绑定表达式只描述数据之间的关系,而不修改任何变量,所有的变量修改都由Binding.scala框架负责。那么,只要我写的框架没有bug,用户就很难自己写出bug。