Scalaz(22)- 泛函编程思维: Coerce Monadic Thinking

马上进入新的一年2016了,来点轻松点的内容吧。前面写过一篇关于用Reader实现依赖注入管理的博文(Scalaz(16)- Monad:依赖注入-Dependency Injection By Reader Monad)。刚好年底这几天抽空重审了一遍,这时才真正认识到让一个老资格OOP程序猿去编写一段FP程序时会发生什么事情:他会用FP语法和数据类型按照OOP的思维编写程序。其结果就是一段尴尬的代码,让人看得不知怎么去形容,更不用提FP程序的精简高雅了。我在前面博文的示范程序正是落入了这个OOP思维陷阱。

我们先把源代码搬过来看看:

package Exercises
import scalaz._
import Scalaz._
object reader3 {
trait OnOffDevice {
    def on: String
    def off: String
}
trait SensorDevice {
    def isCoffeePresent: Boolean
}
trait PowerConfig {
  def getPowerVolts(country: String): Int
  def isUSStandard(volt: Int): Boolean
}

trait OnOffComponent {
    def onOffDevice: OnOffDevice
}
trait SensorComponent {
    def sensorDevice: SensorDevice
}
trait Device extends OnOffComponent with SensorComponent
trait DeviceComponent {
    def onOffDevice: OnOffDevice
    def sensorDevice: SensorDevice
}
trait PowerComponent {
    def powerConfig: PowerConfig
}
trait Appliance extends DeviceComponent with PowerComponent
object Appliance {
  val appliance = Reader[Appliance,Appliance](identity)
  val onOffDevice = appliance map {_.onOffDevice}
  val sensorDevice = appliance map {_.sensorDevice}
  val powerConfig = appliance map {_.powerConfig}
}
object OnOffDevice {
import Appliance.onOffDevice
    def on: Reader[Appliance,String] = onOffDevice map { _.on }
    def off: Reader[Appliance,String] = onOffDevice map { _.off }
}
object SensorDevice {
import Appliance.sensorDevice
  def isCoffeePresent: Reader[Appliance,Boolean] = sensorDevice map { _.isCoffeePresent }
}
object PowerConfig {
import Appliance.powerConfig
    def getPowerVolts(country: String) = powerConfig map {_.getPowerVolts(country)}
    def isUSStandard(volts: Int) = powerConfig map {_.isUSStandard(volts)}
}
object OnOffService {
    def on = for {
        ison <- OnOffDevice.on
    } yield ison
    def off = for {
        isoff <- OnOffDevice.off
    } yield isoff
}
object SensorService {
    def isCoffeePresent = for {
        hasCoffee <- SensorDevice.isCoffeePresent
    } yield hasCoffee
}
object PowerService {
    def isUSStandard(country: String) = for {
        is110v <- PowerConfig.getPowerVolts(country)
        isUSS <- PowerConfig.isUSStandard(is110v)
    } yield isUSS
}
class OnOffDeviceImpl extends OnOffDevice {
    def on = "SomeDevice.On"
    def off = "SomeDevice.Off"
}
class SensorDeviceImpl extends SensorDevice {
    def isCoffeePresent = true
}
class PowerConfigImpl extends PowerConfig {
    def getPowerVolts(country: String) = country match {
        case "USA" => 110
        case "UK" => 220
        case "HK" => 220
        case "CHN" => 110
        case _  => 0
    }
    def isUSStandard(volts: Int) = volts === 110
}
object MockOnOffDevice extends OnOffDeviceImpl
object MockSensorDevice extends SensorDeviceImpl
object MockPowerConfig extends PowerConfigImpl
trait OnOffFunctions extends OnOffComponent {
    def onOffDevice = MockOnOffDevice
}
trait SensorFunctions extends SensorComponent {
  def sensorDevice = MockSensorDevice
}
trait DeviceFunctions extends DeviceComponent  {
    def onOffDevice = MockOnOffDevice
  def sensorDevice = MockSensorDevice
}
trait PowerFunctions extends PowerComponent {
    def powerConfig = MockPowerConfig
}
object MockAppliance extends Appliance with DeviceFunctions with PowerFunctions
def trigger =
  if ((PowerService.isUSStandard("CHN")(MockAppliance))
      && (SensorService.isCoffeePresent(MockAppliance)))
       OnOffService.on(MockAppliance)
   else
     OnOffService.off(MockAppliance)              //> trigger: => scalaz.Id.Id[String]
trigger                                           //> res0: scalaz.Id.Id[String] = SomeDevice.On
}

这段代码前面用trait进行了功能需求描述,接着用Reader定义依赖,再接着通过Reader组合实现了依赖的层级式管理,直到形成最终的Reader组合:

object MockAppliance extends Appliance with DeviceFunctions with PowerFunctions

这些都没什么问题,也体现了函数式编程风格。问题就出在这个trigger函数定义里,我们来看看:

def trigger =
  if ((PowerService.isUSStandard("CHN")(MockAppliance))
      && (SensorService.isCoffeePresent(MockAppliance)))
       OnOffService.on(MockAppliance)
   else
     OnOffService.off(MockAppliance)              //> trigger: => scalaz.Id.Id[String]

首先感觉代码很乱;每句都有个MockAppliance很笨拙(clumsy),感觉不到任何优雅的风格,也看不出与常用的OOP编程有什么分别。

回忆下当时是怎么想的呢?trigger的要求是:如果电源是US标准并且壶里能检测到有咖啡,那么就可以启动加热器,否则关停。

已经完成了电源标准和咖啡壶内容检测即加热器开关的组件(combinators)。都是细化了的独立功能函数,这点符合了函数式编程的基本要求。

当时的思路是这样的:

1、获取当前电源制式,判断是否US标准 

2、获取咖啡壶检测数据,判断是否盛载咖啡

3、if 1 and 2 then OnoffService.on else OnOffService.off

但是为了获取1和2的Boolean结果就必须注入依赖:MockAppliance,所以在trigger函数定义里进行了依赖注入。现在看来这就是典型的OOP思想方式。

首先我们再次回想一下函数式编程的一些最基本要求:

1、纯代码(pure code):实现函数组合-这点在前面的功能函数组件编程中已经做到

2、无副作用(no-side-effect):尽量把副作用推到程序最外层,拖延到最后-trigger使用了依赖MockAppliance,产生了副作用

3、我经常提醒自己Monadic Programming就是F[A]:A是我们要运算的值,我们需要在一个壳子内(context)对A进行运算。

看看这个版本的trigger:因为直接获取了isUSStandard和isCoffeePresent的Boolean运算值所以需要立即注入依赖。首先的后果是trigger现在是有副作用的了。再者trigger和MockAppliance紧紧绑到了一起(tight coupling)- 如果我们再有个Reader组合,比如什么DeployAppliance的,那我们必须再搞另一个版本的trigger了。即使我们通过输入参数传入这个Reader组合依赖也会破坏了函数的可组合性(composibility),影响函数组件的重复利用。看来还是按照上面的要求把这个trigger重新编写:

  object MockAppliance extends Appliance with DeviceFunctions with PowerFunctions
  def trigger(cntry: String) = for {
    isUS <- PowerService.isUSStandard(cntry)
    hasCoffee <- SensorService.isCoffeePresent
    onoff <- if (isUS && hasCoffee) OnOffService.on else OnOffService.off
  } yield onoff       //> trigger: (cntry: String)scalaz.Kleisli[scalaz.Id.Id,Exercises.Exercises.rea
                                                  //| derDI.Appliance,String]
  trigger("CHN")(MockAppliance)                   //> res0: scalaz.Id.Id[String] = SomeDevice.On
  trigger("HK")(MockAppliance)                    //> res1: scalaz.Id.Id[String] = SomeDevice.Off

现在这个版本的trigger是一段纯代码,并且是在for-comprehension内运算的,与依赖实现了松散耦合。假如这时再有另一个版本的依赖组合DeployAppliance,我们只需要改变trigger的注入依赖:

  trigger("CHN")(DeployAppliance)                   //> res0: scalaz.Id.Id[String] = CoffeeMachine.On
  trigger("HK")(DeployAppliance)                    //> res1: scalaz.Id.Id[String] = CoffeeMachine.Off

怎么样?这样看起来是不是简明高雅许多了?

噢,祝大家新年快乐!

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏函数式编程语言及工具

Scalaz(25)- Monad: Monad Transformer-叠加Monad效果

中间插播了几篇scalaz数据类型,现在又要回到Monad专题。因为FP的特征就是Monad式编程(Monadic programming),所以必须充分理解...

1826
来自专栏nimomeng的自我进阶

Swift4 新特性一览

当然,由于Session的时间限制,肯定没有把所有Swift4的特性说全。仅就列出来的这几个,我个人比较喜欢的有 XCode支持Swift的refactor,S...

662
来自专栏racaljk

关于llvm kaleidoscope: 记一次Debug血泪之路

简而言之,慎(bu)用(yong)全局变量!                                

681
来自专栏机器学习算法与Python学习

python初学者的建议

Python是一种非常富有表现力的语言。它为我们提供了一个庞大的标准库和许多内置模块,帮助我们快速完成工作。然而,许多人可能会迷失在它提供的功能中,不能充分利用...

41012
来自专栏诸葛青云的专栏

想当黑客?浅谈C语言编程:不会这个知识就别想了!

看到标题点进来的朋友,应该对黑客这个名词很敏感吧?我想应该是这样的,但是你们知道作为一名黑客需要学习哪些知识吗?小编不是什么大佬,但小编可以明确的告诉你,学习C...

110
来自专栏码洞

编程的智慧

编程是一种创造性的工作,是一门艺术。精通任何一门艺术,都需要很多的练习和领悟,所以这里提出的“智慧”,并不是号称一天瘦十斤的减肥药,它并不能代替你自己的勤奋。然...

371
来自专栏Script Boy (CN-SIMO)

软件工程作业02

题目:      二柱子又对自己提出了新的要求: ? 设计思想: 项目名称、源文件等见软件工程作业01 对于乘除法,其实我们作业01中已经有了除法,别忘了...

1860
来自专栏非典型技术宅

Swift实践:使用CoreData存储多种数据类的通讯录1. CoreData支持存储数据类型2. 使用CoreData存储多种数据类的通讯录3. Codable

1323
来自专栏web前端教室

看过必懂!JavaScript的继承

最近刚把Js高级程序设计看了一遍,仔细的读了其中的第6章的第6.3节-继承。今天写出来跟大家分享一下,顺便也加深一下自己的理解。

641
来自专栏青玉伏案

Objective-C中的类目,延展,协议

  Objective-C中的类目(Category),延展(Extension),协议(Protocol)这些名词看起来挺牛的,瞬间感觉OC好高大上。在其他O...

1838

扫码关注云+社区