函数式编程模式强调纯代码(pure code),主要实现方式是使用不可变数据结构,目的是函数组合(composability)最终实现函数组件的重复使用。但是,如果我们在一个函数p内部使用了可变量(mutable variables),如果函数的输入参数e是纯代码,那么表达式p(e)同样是纯代码的,因为函数的调用者是无法接触到函数内部申明的这些可变量的。不过,这样的做法会造成函数的臃肿代码,因为在函数内部是无法实现函数组合的,无法重复使用函数组件,实际上又违背了FP的宗旨。Scalaz提供了专门解决可变量使用问题的方法,能保证即使在并行运算的环境内各线程无法影响相互间的可变量,即ST Monad。

Scalaz的可变量分两种:一个内存地址STRef或一个可变数组STArray。我们先看看它们在源代码中的定义:effect/ST.scala

/**Mutable variable in state thread S containing a value of type A. [[http://research.microsoft.com/en-us/um/people/simonpj/papers/lazy-functional-state-threads.ps.Z]] */
sealed trait STRef[S, A] {
protected var value: A /**Reads the value pointed at by this reference. */
def read: ST[S, A] = returnST(value) /**Modifies the value at this reference with the given function. */
def mod[B](f: A => A): ST[S, STRef[S, A]] = st((s: Tower[S]) => {
value = f(value);
(s, this)
}) /**Associates this reference with the given value. */
def write(a: => A): ST[S, STRef[S, A]] = st((s: Tower[S]) => {
value = a;
(s, this)
}) /**Synonym for write*/
def |=(a: => A): ST[S, STRef[S, A]] =
write(a) /**Swap the value at this reference with the value at another. */
def swap(that: STRef[S, A]): ST[S, Unit] = for {
v1 <- this.read
v2 <- that.read
_ <- this write v2
_ <- that write v1
} yield ()
}
...
/**Mutable array in state thread S containing values of type A. */
sealed trait STArray[S, A] {
def size: Int
def z: A
implicit def manifest: Manifest[A] private lazy val value: Array[A] = Array.fill(size)(z) import ST._ /**Reads the value at the given index. */
def read(i: Int): ST[S, A] = returnST(value(i)) /**Writes the given value to the array, at the given offset. */
def write(i: Int, a: A): ST[S, STArray[S, A]] = st(s => {
value(i) = a;
(s, this)
}) /**Turns a mutable array into an immutable one which is safe to return. */
def freeze: ST[S, ImmutableArray[A]] = st(s => (s, ImmutableArray.fromArray(value))) /**Fill this array from the given association list. */
def fill[B](f: (A, B) => A, xs: Traversable[(Int, B)]): ST[S, Unit] = xs match {
case Nil => returnST(())
case ((i, v) :: ivs) => for {
_ <- update(f, i, v)
_ <- fill(f, ivs)
} yield ()
} /**Combine the given value with the value at the given index, using the given function. */
def update[B](f: (A, B) => A, i: Int, v: B) = for {
x <- read(i)
_ <- write(i, f(x, v))
} yield ()
}

我们看到STRef和STArray都定义了write,mod,update这样有副作用的操作函数,它们都返回了ST[S,STRef[S,A]]类型的结果。ST是个Monad,我们可以从源代码中证实:

/**
* Purely functional mutable state threads.
* Based on JL and SPJ's paper "Lazy Functional State Threads"
*/
sealed trait ST[S, A] {
private[effect] def apply(s: Tower[S]): (Tower[S], A) import ST._ def flatMap[B](g: A => ST[S, B]): ST[S, B] =
st(s => apply(s) match {
case (ns, a) => g(a)(ns)
}) def map[B](g: A => B): ST[S, B] =
st(s => apply(s) match {
case (ns, a) => (ns, g(a))
})
}

ST与State Monad极其相似,备有map和flatMap,所以是个Monad,能支持for-comprehension。我们可以通过ST的for-comprehension实现STRef,STArray操作函数的组合,因为这些操作函数的返回结果都是ST类型的。但write,mod这些操作函数有个共同的奇怪现象:它们都没有调用过S类型的值,直接按传入就输出去了。这正是ST Monad如何命名的:ST又可以被称为State Tag,也就是说每一项操作都有独立的状态类型S,如果S类型有所不同的话是无法调用操作函数的。而for-comprehension是一种串型流程,能保证线程之间不会交叉运行,相互影响各自的可变量。ST Monad与State Monad最大的不同是它没有run方法,也就是我们无法用ST的内部方法来获取ST[S,A]的A值。我们先看看STRef和STArray的一些用例:

 import scalaz._
import Scalaz._
import effect._
import ST._
object st {
def e1[S] = for {
r <- newVar[S]()
x <- r.mod {_ + }
} yield x //> e1: [S]=> scalaz.effect.ST[S,scalaz.effect.STRef[S,Int]] def e2[S] = for {
r <- newVar[S]()
x <- r.mod {_ + }
y <- x.read
} yield y //> e2: [S]=> scalaz.effect.ST[S,Int] def e3[S] = for {
arr <- newArr[S,Int](,)
_ <- arr.write(,)
_ <- arr.write(,)
_ <- arr.update ((a: Int,b: Int) => a + b, , )
r <- arr.freeze
} yield r //> e3: [S]=> scalaz.effect.ST[S,scalaz.ImmutableArray[Int]]

newVar[S](3)以状态S新建一个Int存放地址并存入值3。mod操作返回ST[S,STRef[S,Int]],返回的是个地址(reference),而read返回的是ST[S,Int],则是个值。首先注意e1[S],e2[S],e3[S]它们都附带了独立状态标签S。

现在我们需要从ST Monad里把运算结果取出来。从上面的分析我们可能面对两种方式:ST[S,A], ST[S,STRef[S,A]]。从ST[S,A]里取出的是一个A类型的值,而从ST[S,STRef[S,A]]里取出的是个内存地址。可以预见,如果我们通过某些方式能获取一个内存地址的话,就有可能在函数体外对地址内的值进行修改,也就造成了副作用的产生。Scalaz的解决方法是通过高阶类参数多态(2nd-rank parameteric polymorphism),利用编译器(compiler)对ST[S,STRef[S,A]]这样的读取操作进行拒绝编译。下面我们看看示范结果:
 runST(new Forall[({type l[x] = ST[x, Int]})#l]{def apply[S] = e2[S]})
//> res0: Int = 5
//runST(new Forall[({type l[x] = ST[x, Int]})#l]{def apply[S] = e1[S]})
//type mismatch; found : scalaz.effect.ST[S,scalaz.effect.STRef[S,Int]] required: scalaz.effect.ST[S,Int]
e1返回ST[S,STRef[S,A]],表达式new Forall[({type l[x] = ST[x, Int]})#l]{def apply[S] = e1[S]}无法通过编译。在这里Forall是个高阶类参数多态类,定义如下:
/** A universally quantified value */
trait Forall[P[_]] {
def apply[A]: P[A]
}
我们再重新组织一些上面的代码,使大家可以看的更清楚一点:
 type ForallST[A] = Forall[({type l[x] = ST[x,A]})#l]
runST(new ForallST[Int]{ def apply[S] = e2[S] }) //> res0: Int = 5
//runST(new ForallST[Int]{def apply[S] = e1[S]})
//type mismatch; found : scalaz.effect.ST[S,scalaz.effect.STRef[S,Int]] required: scalaz.effect.ST[S,Int]
从错误信息可以得出:编译器期待的类型是ST[S,Int], ST[S,STRef[S,Int]]是产生类型错误。利用高阶类参数多态类f,只有new Forall { def apply[A] >>> ST[S,A] }这样的款式才能通过编译。
与State Monad比较,ST Monad并不包含为获取运算值而设的run函数。ST Monad在类型外定义了读取运算值的函数runST。
runST方法的定义如下:
  /**Run a state thread */
def runST[A](f: Forall[({type λ[S] = ST[S, A]})#λ]): A =
f.apply.apply(ivoryTower)._2
State Monad获取运算值的方式是这样的:someState.run(svalue),svalue是个S类型值,是状态初始值。我们已经了解到所有的变量操作函数都没有使用S类型值,所以上面的f.apply.apply(ivoryTower)中这个ivoryTower是个没有意义的随意类型值,我们不需要注入任何S值去获取运算结果值。
 
 
 
 

Scalaz(28)- ST Monad :FP方式适用变量的更多相关文章

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

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

  2. Ansible系列(六):各种变量定义方式和变量引用

    本文目录:1.1 ansible facts1.2 变量引用json数据的方式 1.2.1 引用json字典数据的方式 1.2.2 引用json数组数据的方式 1.2.3 引用facts数据1.3 设 ...

  3. Ansible系列(五):各种变量定义方式和变量引用

    Ansible系列文章:http://www.cnblogs.com/f-ck-need-u/p/7576137.html 1.1 ansible facts facts组件是用来收集被管理节点信息的 ...

  4. 【转】 IOS,objective_C中用@interface和 @property 方式声明变量的区别

    原文: http://blog.csdn.net/ganlijianstyle/article/details/7924446 1.在  @interface :NSObject{} 的括号中,当然N ...

  5. 初识 Javascript.01 -- Javascript基础|输出方式、变量、变量命名规范、数据类型、

    Javascript基础 1 聊聊Javascript 1.1 Javascript的历史来源 94年网景公司   研发出世界上第一款浏览器. 95年 sun公司   java语言诞生 网景公司和su ...

  6. 基础知识:编程语言介绍、Python介绍、Python解释器安装、运行Python解释器的两种方式、变量、数据类型基本使用

    2018年3月19日 今日学习内容: 1.编程语言的介绍 2.Python介绍 3.安装Python解释器(多版本共存) 4.运行Python解释器程序两种方式.(交互式与命令行式)(♥♥♥♥♥) 5 ...

  7. python基础(内存分析,不引入第三方变量的方式交换变量的值)

    a,b指向同一块内存地址 下面方法是重新给b赋值;a,b指向不同的内存地址 字符串或int类型内存分析 不引入第三方变量的方式,交换a,b的值

  8. PythonDay02——编程语言、python介绍以及安装解释器、运行程序的两种方式、变量

    一.编程语言 1.1 机器语言:直接用计算机能理解的二进制指令编写程序,直接控制硬件 1.2 汇编语言:用英文标签取代二进制指令去编写程序,本质也是直接控制硬件 1.3 高级语言:用人能理解的表达方式 ...

  9. day2 编程语言介绍、Python运行程序的两种方式、变量

    一 编程语言介绍 1. 机器语言 用计算机能理解的二进制指令直接编写程序,直接控制硬件 2. 汇编语言 用英文标签取代二进制指令编写程序,本质也是直接控制硬件 3. 高级语言 用人能理解的表达方式去编 ...

随机推荐

  1. Atitit java的异常exception 结构Throwable类

    Atitit java的异常exception 结构Throwable类 1.1. Throwable类 2.StackTrace栈轨迹1 1.2. 3.cause因由1 1.3. 4.Suppres ...

  2. Gitlab备份、升级、恢复

    一.备份 1.使用Omnibus安装包安装 --gitlab-rake gitlab:backup:create 2.使用源码安装 --./use_gitlab----如果备份失败,PATH路径错误, ...

  3. jquery 插件开发

    一.$.extend()  这种方式用来定义一些辅助方法是比较方便的 $.extend({ sayHello:function(name){ console.log('Hello:'+name); } ...

  4. 每天一个linux命令(31): /etc/group文件详解

    Linux /etc/group文件与/etc/passwd和/etc/shadow文件都是有关于系统管理员对用户和用户组管理时相关的文件.linux /etc/group文件是有关于系统管理员对用户 ...

  5. POJ1014 解题报告(DFS)

    题目在此:http://poj.org/problem?id=1014 要看清题意呢,题中要求输入的是价值分别为1,2,3,4,5,6的大理石的个数,而不是6块价值为输入数字的大理石!选这个题主要想练 ...

  6. JUnit4使用

    1.导入Junit4jar包: Eclipse中在项目上右键点击Bulid Path,然后再点击Add libraries,选择JUnit 2.初次使用 首先先创建一个java项目如下: Demo.j ...

  7. Java多线程系列--“基础篇”07之 线程休眠

    概要 本章,会对Thread中sleep()方法进行介绍.涉及到的内容包括:1. sleep()介绍2. sleep()示例3. sleep() 与 wait()的比较 转载请注明出处:http:// ...

  8. [转载]"百度方法+"案例—从持续集成到持续交付

    前言 百度开放云(https://bce.baidu.com)是百度基于十五年基础架构核心技术积累推出的云服务,目前推出了14个云计算产品和9个大数据产品,并提供数字营销云.在线教育.物联网等10种解 ...

  9. Android Activity返回键控制的两种方式

    Android Activity返回键监听的两种方式 1.覆写Activity的OnBackPressed方法 官方解释: Called when the activity has detected ...

  10. 索引深入浅出(5/10):非聚集索引的B树结构在堆表

    在“索引深入浅出:非聚集索引的B树结构在聚集表”里,我们讨论了在聚集表上的非聚集索引,这篇文章我们讨论下在堆表上的非聚集索引. 非聚集索引可以在聚集表或堆表上创建.当我们在聚集表上创建非聚集索引时,聚 ...