StateMonad TS 实现

StateMonad

State Monad 相比起其他类型的 Monad 例如 Maybe 或者 Either 要更难理解。并不是指其代码有多么复杂,而是它的作用并不是特别直观,以至于在初学的时候要花大量的时间来理解它的作用。因此本文从一个例子出发,来说明为什么需要 State Monad。Monad 虽然是被 haskell 发扬光大,但鉴于 haskell 的普及程度,本文还是决定采用 TS 来描述。另外,Monad 目前在大部分语言中都存在,不一定非要通过 haskell 来学习。

随机数生成

State Monad 顾名思义是和状态(State)相关的一个 Monad。这里先简单的介绍一个随机数生成方法线性同余法,其原理非常简单:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> X n + 1 = ( a ∗ X n + b ) % m X_{n+1} = (a*X_n + b) \% m </math>Xn+1=(a∗Xn+b)%m

其中 a, b, m 都是参数。从公式来看,这是一个递推公式,即本次随机数的值也依赖于上一次随机数的结果。这里采用一个常用的参数即 a=25214903917,b=11,c=65535 并在 ts 里面实现:

ts 复制代码
class Random{
  readonly a = 25214903917
  readonly b = 11
  readonly m = 65535
  state: number
  constructor(){
    this.state = (this.a * Date.now() + this.b) % this.m
  }

  nextInt(range: number): number{
    this.state = (this.a * this.state + this.b) % this.m
    return this.state % range
  }
}

let random = new Random()
console.info(random.nextInt(100)) // 87
console.info(random.nextInt(100)) // 14
console.info(random.nextInt(100)) // 21

这段从面向对象的编程模式来看并没有什么问题。我们定义了一个成员变量 state 来存储上一次产生的随机数从而达到实现递推公式的目的。这里的 state 就是 State Monad 中的 state,可以认为是一个函数的外部变量。但是从函数式编程的角度来看,nextInt 这个方法修改了一个外部变量使得它变成了一个非纯函数。我们当然希望函数越纯越好,于是可以这样实现:

ts 复制代码
function nextInt(range: number,  state: number): [number, number]{
  const nextState = (25214903917 * state + 11) % 65535
  return [nextState % range, nextState] 
}

const [rand1, state1] = nextInt(100, Date.now())
const [rand2, state2] = nextInt(100, state1)
const [rand3] = nextInt(100, state2)
console.info(rand1, rand2, rand3) //58, 42, 65

现在 nextInt 是一个纯函数了,但是看起来总觉得怪怪的。为了实现纯函数,我们不得不把 state 通过参数传入传出,这样使用起来也太麻烦了。有没有一种办法能够既有保持纯函数,又不手动传递状态呢。这时候就该 StateMonad 粉墨登场了。

使用 StateMonad 实现

随机数生成的问题或者其他需要 state 的场景我们总可以抽象为 S => (A, S) 即通过一个状态 S 得到本次的输出 A 以及下一次的状态 S,可以称之为状态转化函数。StateMonad 并不直接存储状态,而是存储状态的转换:

ts 复制代码
type StateFunction<S, A> = (state: S) => [A, S];
class StateMonad<S, A> {
    runState: StateFunction<S, A>

    constructor(runState: StateFunction<S, A>){
        this.runState = runState
    }
    
    flatMap<B>(transform: (args: A) => StateMonad<S, B>): StateMonad<S, B>{
       return new StateMonad(
         s => {
            const [v, newS] = this.runState(s)
            return transform(v).runState(newS)
         }
       )
    }
}

这里的 runState 就是刚刚提到的状态转换函数。但是,我们还需要一个方法将不同的 StateMonad 连接起来,即上面的 flatMap 方法。其签名为 A => StateMonad<S, B>,即根据上一个 StateMonad 的输出,产生一个新的 StateMonad,并且让状态在这两个 Monad 之前传递。我们来采用 StateMonad 来实现一下:

ts 复制代码
class Random extends StateMonad<number, number>{}

const nextInt = (range) => new Random(state => {
  const nextState = (25214903917 * state + 11) % 65535
  return [nextState % range, nextState]
})

const next = nextInt(100)
const [res] = next.flatMap(
  x1 => next.flatMap(
    x2 => next.flatMap(
      x3 => new StateMonad<number, [number, number, number]>(s => [[x1, x2, x3], s])
    )
  )
).runState(Date.now())
console.info(res) // 74, 14, 59

emmm 很好,我们刚从非纯函数中解放出来,又踏入了回调地狱中。为了获取结果我们不得不嵌套 3 层回调函数,非常的难受。当然在 scala 或者 haskell 中我们可以使用 do-notation 来完成类似的操作,这样就更优雅了:

scala 复制代码
for {
    x1 <- next
    x2 <- next
    x3 <- next
} yield (x1, x2, x3)

不过这是一个使用 ts 的例子,我们没有这样的语法糖。但是忽略掉难看的回调函数,我们不得不承认,这里面的每一个函数都是纯的不能再纯的纯函数。nextInt 是纯函数,next.flatMap 同样是纯函数。我们使用了纯函数实现了随机算法,并且没有手动传递状态,这就是 StateMonad 的威力。

Dive Into flatMap

flatMap 可以说是 StateMonad 中最核心的一个函数了。它的参数是一个函数,其签名为:

ts 复制代码
(args: A) => StateMonad<S, B>

其接受一个值,并将其转换为一个新的 StateMonad。为什么需要这样的函数,是因为很多时候我们需要把根据上一个 StateMonad 输出(注意不是状态),并结合当前 StateMoand 输出。例如我需要三个随机数,那么我需要首先获得第一个随机数,再获得第二随机数,这样我们就必须能显示的获得前一个 StateMonad 的输出,也就是 这里的 A。我们再来看 flatMap 的实现:

ts 复制代码
{
       return new StateMonad(
         s => {
            const [v, newS] = this.runState(s) // 1. 执行状态转换函数,获取最新的状态 
            return transform(v).runState(newS) // 2. 根据本次输出得到下一个 StateMonad,并且将状态传递给下一个 StateMonad
         }
       )
    }
}

实际上这里的核心代码就两行,第一行获取最新状态,第二行生成新的 StateMonad 并传递状态。简洁且优雅。 了解了原理之后,刚刚的例子我们还可以优化优化:

javascript 复制代码
// 新增一个 collect 来收集输出
const collect = <S, T>(x: T) => new StateMonad<S, T>(s => [x, s]) 
const next = nextInt(100)
const [res] = next.flatMap(
  x1 => next.flatMap(
    x2 => next.flatMap(
      x3 => collect([x1, x2, x3])
    )
  )
).runState(Date.now())

嗯,看起来舒服多了(并没有)。

总结

虽然本文使用随机数生成来简单介绍了 StateMonad 的使用场景以及原理,但是 StateMonad 的应用场景显然更加广泛。只要是符合 S => (A, S) 这样的场景都能使用。当然,除了 flatMap 之外, mapreturn 在本文中并没有介绍,但是 flatMap 就足够我们理解 StateMoand 了。感兴趣的同学也可以想一想如何在已经定义了 flatMap 的情况下获得 map

相关推荐
再思即可4 天前
sicp每日一题[2.13-2.16]
编程·lisp·函数式编程·sicp·scheme
ezreal_pan18 天前
基于约束大于规范的想法,封装缓存组件
redis·缓存·函数式编程
知识分享小能手2 个月前
从新手到高手:Scala函数式编程完全指南,Scala 文件 I/O(27)
大数据·开发语言·后端·flink·spark·scala·函数式编程
程序员Allen2 个月前
第一个Lambda表达式
java·函数式编程
互联网架构小马2 个月前
12种增强Python代码的函数式编程技术
开发语言·后端·python·函数式编程
知识分享小能手3 个月前
从新手到高手:Scala函数式编程完全指南,Scala 访问修饰符(6)
大数据·开发语言·后端·python·数据分析·scala·函数式编程
Jack_hrx3 个月前
Java中的Monad设计模式及其实现
java·开发语言·设计模式·函数式编程·monad
niuiic4 个月前
来聊聊函数式
typescript·函数式编程
无休居士4 个月前
【设计模式】函数式编程范式工厂模式(Factory Method Pattern)
设计模式·函数式编程·工厂模式
陈建1115 个月前
设计模式学习笔记 - 开源实战三(下):借助Google Guava学习三大编程范式中的函数式编程
函数式编程