• 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

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

    噢,祝大家新年快乐!

     

     

     

  • 相关阅读:
    web api 初体验之 GET和POST传参
    清除系统日志及数据库(sql server)日志最佳实践
    大家好啊!
    [oc学习笔记]多态
    [oc学习笔记]便利构造器无法被继承
    [oc学习笔记]字符串
    antd异步加载的树
    react新建页面步骤(新手必看)
    ECharts 经常会修改到的一些样式配置
    关于数组的一些常用方法
  • 原文地址:https://www.cnblogs.com/tiger-xc/p/5087692.html
Copyright © 2020-2023  润新知