马上进入新的一年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" =>
case "UK" =>
case "HK" =>
case "CHN" =>
case _ =>
}
def isUSStandard(volts: Int) = volts ===
}
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

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

噢,祝大家新年快乐!

Scalaz(22)- 泛函编程思维: Coerce Monadic Thinking的更多相关文章

  1. 泛函编程(27)-泛函编程模式-Monad Transformer

    经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...

  2. 泛函编程(24)-泛函数据类型-Monad, monadic programming

    在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...

  3. 泛函编程(38)-泛函Stream IO:IO Process in action

    在前面的几节讨论里我们终于得出了一个概括又通用的IO Process类型Process[F[_],O].这个类型同时可以代表数据源(Source)和数据终端(Sink).在这节讨论里我们将针对Proc ...

  4. 泛函编程(34)-泛函变量:处理状态转变-ST Monad

    泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Re ...

  5. 泛函编程(32)-泛函IO:IO Monad

    由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...

  6. 泛函编程(29)-泛函实用结构:Trampoline-不再怕StackOverflow

    泛函编程方式其中一个特点就是普遍地使用递归算法,而且有些地方还无法避免使用递归算法.比如说flatMap就是一种推进式的递归算法,没了它就无法使用for-comprehension,那么泛函编程也就无 ...

  7. 泛函编程(25)-泛函数据类型-Monad-Applicative

    上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...

  8. 怎样学习Scala泛函编程

    确切来说应该是我打算怎么去学习Scala泛函编程.在网上找不到系统化完整的Scala泛函编程学习资料,只好把能找到的一些书籍.博客.演讲稿.论坛问答.技术说明等组织一下,希望能达到学习目的.关于Sca ...

  9. scala泛函编程是怎样被选中的

    现在计算机技术发展现象是:无论硬件技术如何发展都满足不了软件需求:无论处理器变得能跑多快,都无法满足软件对计算能力的需要.按照摩尔定律(Moore's Law)处理器(CPU)每平方面积上包含的半导体 ...

随机推荐

  1. 函数柯理化以及利用柯理化实现bind方法

    1.函数柯理化 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术. 柯理化函数思想:一个js预先处理的思想:利用函数执行可以形 ...

  2. Jquery判断页面图片是否加载失败,加载失败则显示默认图片

    例子: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3. ...

  3. Script Component 引用package variable

    ScriptComponet 的变量分为两种类型,ReadOnly和ReadWrite,使用C#引用这两种类型的变量,有一点不同. 1,创建两个变量 2,将变量传递给script component ...

  4. Enterprise Solution 应用程序开发框架培训

    一.系统架构 C# .NET 4.0 + Win Form + SQL Server 2005 二.五大核心模块 (菜单设计器Menu Designer,查询设计器Query Designer,报表设 ...

  5. 了解canvas

    目录 [1]HTML属性[2]CSS样式 [3]API 坐标 填充和描边 阴影 绘制矩形 绘制路径 绘制文本 绘制图像 使用图像 变换 合成 [4]DEMO 前面的话 canvas元素是HTML5最受 ...

  6. Linux驱动开发——__stringify

    在Linux内核中有一个宏__stringify,在include/linux/stringify.h定义如下: #ifndef __LINUX_STRINGIFY_H #define __LINUX ...

  7. RPC原理详解

    RPC 功能目标 RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性. 为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分 ...

  8. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

  9. 【Swift学习】Swift编程之旅---继承(十七)

    在 Swift 中,继承是区分「类」与其它类型的一个基本特征.swift不支持多重继承.类可以调用和访问超类的方法,属性和subscripts下标,并且可以重写(override)这些方法,属性和附属 ...

  10. 使用Uploadify(UploadiFive)多文件上传控件遇到的坑

    最近项目中需要实现多文件上传功能,于是结合需求最终选择了Uploadify这一款控件来实现.相比其他控件,Uploadify具有简洁的界面,功能API基本可以解决大多数需求,又是基于jquery的,配 ...