• 首页
  • Github
使用递归函数类型的一种 CPS 状态机写法

go 可以定义直接递归的复合类型,也就是在类型的定义中,使用正在定义的类型名。

而函数是类型的一种,所以可以定义一个函数类型,它的返回值的类型是这个函数本身。

例如:

type Proc func() Proc

上面这个类型,可以用来表示状态机中的一个状态,结合 CPS 手法,就可以实现一种协程。

计数状态机

一个例子,从1数到10:

package main

import "fmt"

type Proc func() Proc

func Count10(n int, cont Proc) Proc {
	return func() Proc {
		fmt.Printf("%d\n", n)
		if n == 10 {
			return cont
		}
		return Count10(n+1, cont)
	}
}

func main() {
	proc := Count10(1, nil)
	for proc != nil {
		proc = proc()
	}
}

上面的 main 函数中,for 语句如果换成

	proc = proc()
	proc = proc()
	proc = proc()
	proc = proc()

则只会打印1到4,这个状态机的步进是可控的。

多个 Proc 也可以实现并发乱序执行:

func main() {
	rand.Seed(int64(time.Now().UnixNano()))
	var procs []Proc
	for i := 0; i < 128; i++ {
		procs = append(procs, Count10(1, nil))
	}
	for len(procs) > 0 {
		n := rand.Intn(len(procs))
		if cont := procs[n](); cont != nil {
			procs[n] = cont
		} else {
			procs = append(procs[:n], procs[n+1:]...)
		}
	}
}

如果需要更多状态,可以将状态放入 struct,然后定义 Proc 为该 struct 的方法,这样就能在 Proc 内读写状态。 一个例子是:https://github.com/reusee/sb/blob/cdbe32270fd478df2d5e6b1a603c22497e70e8d6/marshal.go#L11

生成器

对 Proc 的类型稍作修改,可以实现一种生成器

type Proc func() (int, Proc)

上面这个类型,可用于实现 int 序列的生成器。例如 fib 序列:

package main

import "fmt"

type Proc func() (int, Proc)

func FibGen() func() int {
	proc := func() (int, Proc) {
		return 1, func() (int, Proc) {
			return 1, fib(1, 1, nil)
		}
	}
	return func() (ret int) {
		ret, proc = proc()
		return
	}
}

func fib(a int, b int, cont Proc) Proc {
	return func() (int, Proc) {
		return a + b, fib(b, a+b, cont)
	}
}

func main() {
	gen := FibGen()
	for i := 0; i < 10; i++ {
		fmt.Printf("%d\n", gen())
	}
}

连续调用 gen,可以生成 fib 序列。

一个完整的生成器的例子是:https://github.com/reusee/sb/blob/bbd151e5dcae0ab38515793de3f1133920dd7e74/marshal.go#L11

后记

能实现这种写法,需要几个语言机制的配合。一是递归的类型定义,一是函数字面量,一是方法可以作为值传递。 不是每门语言都有这些特性,尤其是递归类型定义,一些新出的语言也没有。 所以并不是语言有很多 fancy 的东西,就是好。基础不牢,地动山摇。 语言特性做得正交,容易组合使用,不会出现某个特性很难和另一个特性结合使用这种情况,显然是更高明的语言设计哲学。

2019-11-02
comments powered by Disqus