Typescript函数式编程: monad和generator

Typescript函数式编程: monad和generator

翻译自 Advanced functional programming in TypeScript: monads and generators

欢迎来到这个系列的第二篇。在第一篇里,你已经用Typescript创建了第一个monad。在这篇,你会看到如何使用generator使得monad代码更有可读性。
你可已在这个里找到所有代码。请查阅和这部分有关的代码分支。

生成器函数

ES6标准的一部分引入了生成器函数。生成器函数是一种特殊的函数,可以在执行中暂停。这听起来有些反直觉,如果你觉得javascript是单线程执行的,并遵守运行到结束方式(run-to-completion)。然而,有了生成器,代码依然是同步执行的。暂停执行意味着把控制返还给调用者函数。调用者函数然后在任一一点恢复执行。我们看一个简单例子:

function *numbers(): IterableIterator<number> { 
    console.log('Inside numbers; start');
    yield 1;
    console.log('Inside numbers; after the first yield');
    yield 2;
    console.log('Inside numbers; end');
}

这里有两个新的语法。一个是function关键字后面的*。这个说明numbers不是一般的函数,而是一个生成器函数。另一个新的关键字是yield(产生)。这个和return有点像,但可以在方法体中多次执行。产生(yield)一个值,生成器方法就会给调用者返回这个值。然而,不像return一样,调用者决定是否恢复执行并把控制交还给这个生成器函数。恢复后,执行将从上一个yield语句开始。我们先调用numbers来取到一个生成器实例:

const numbersGenerator = numbers();

在这一点上,numbers函数没有一行是执行的。要执行, 我们需要调用next。生成器函数将会执行到它遇到第一个yield之前,然后控制会交还给调用者。

console.log('Outside of numbers'); 
console.log(numbersGenerator.next());
console.log('Outside of numbers; after the first next');
console.log(numbersGenerator.next());
console.log('Outside of numbers; after the second next');

运行这段代码,会得到下面输出:

Outside of numbers
Inside numbers; start
{value: 1, done: false}
Outside of numbers; after first next
Inside numbers; after the first yield
{value: 2, done: false}
Outside of numbers; after the second next

你可以看到,执行在numbers和调用者函数之间来来回回。每次调用next会返回一个包含产生的值和是否结束的标志位doneIteratorResult。标志位done表示在numbers里面是否还有代码执行。

image

生成器是一个强大的机制。一个用到的地方是构架无限懒加载序列(lazy, infinite sequences)。另一个是协程(co-routes) - 一种不同两部分代码可以通信的并发模型。

使用生成器实现的Maybe

回忆一下,我们之前用这种方式实现了getSupervisorName:

function getSupervisorName(maybeEnteredId: Maybe<string>): Maybe<string> {
    return maybeEnteredId
        .flatMap(employeeIdString => Maybe.fromValue(parseInt(employeeIdString)))
        .flatMap(employeeId => repository.findById(employeeId))
        .flatMap(employee => employee.supervisorId)
        .flatMap(supervisorId => repository.findById(supervisorId))
        .map(supervisor => supervisor.name);
}

显然,这个代码比一开始深层嵌套的代码可读性更好了。但是,这和通常的过程式代码很不一样。我们是否可以改成像过程是一样的代码呢?我们知道,生成器可以暂停一个函数的执行,然后再回复。这意味着我们可以在yield语句之间插入一些需要执行的代码。我们可以写一个方法,接受一个生成器方法,然后插入一些处理空值的代码。我们来实现一个用yield语句来处理空值的getSupervisorName函数:

function* () { 
    const enteredIdStr = yield maybeEnteredId
    const enteredId = parseInt(enteredIdStr)
    const employee = yield repository.findById(enteredId)
    const supervisorId = yield employee.supervisorId
    const supervisor = yield repository.findById(supervisorId)
    return Maybe.some(supervisor.name)
}

我们假定maybeEnteredId在函数的比包里定义了,类型是Maybe<string>。现在,const enteredIdStr = yield maybeEnteredId`的语义是:

  • 如果maybeEnteredId有值得花,那么把这个值赋给enteredIdStr
  • 否则,整个方法返回None

换句话说,yieldflatMap起了一样的作用,但是语法不一样了。这对一个过程式编程的人来说更熟悉。

实现Run

这还没有完成。我们还需要一个函数来消费这个生成器函数。换句话说,我们需要赋予这些yield语句以意义。我们定义一个run的函数。这个函数接受一个生成器方法并返回一个包含计算结果的Maybe实例。我们先实现一下run:

static run<R>(gen: IterableIterator<Maybe<R>>): Maybe<R> { 
    let lastValue; 
    while(true) { 
        const result : IterableIterator<Maybe<R>> = gen.next(lastValue)
        if (result.done || result.value.value === null) { 
            return result.value
        }
        lastValue = result.value.value
    }
}
  1. run方法接受一个gen生成器函数,并描述了我们的计算。
  2. 我们进到一个无限循环中并调用gen.next(lastValue)。 这个调用会把控制流转到gen里面并执行到第一个yield(先忽略lastValue
  3. 一旦上一步结束, 控制流会返回run。传给yield的值被包装在IteratorResult并作为gen.next的返回值。
  4. result有个标志位done。意味着在gen里面的代码有没执行完
  5. result.value持有yield的返回值。 是一个Maybe实例。英雌我们要查看是否是空值。如果是的话,我们就在整个计算中返回None
  6. 否则,我们解开Maybe持有的值并赋给lastValue
  7. 以上循环不断重复。lastValue有值的时候,会传给gen.next作为调用yield的返回。

这里发生了很多。一种好的理解方式是把这两部分代码想象成互相通信。

  1. 调用yield, genrun发送了一个Maybe<T>实例。
  2. run通过调用gen.next(lastValue) 回复了一个T实例

这里主要的意义是我们影藏了处理空值的逻辑,在调用者的角度,代码是这样的:

function getSupervisorName(maybeEnteredId: Maybe<string>: Maybe<string> { 
    return Maybe.run(function* ()  {
        const enteredIdStr = yield maybeEnteredId;
        const enteredId = parseInt(enteredIdStr);
        const employee = yield repository.findById(enteredId);
        const supervisorId = yield employee.supervisorId;
        const supervisor = yield repository.findById(supervisorId);
        return Maybe.some(supervisor.name);
    }());
}

我们现在获得了一个干净优雅的getSupervisorName实现,所有的重复代码都在run里面。然而和flatMap方式不一样,这对一个过程是编程人员是更自然的。

回到monad

你也许会说这很漂亮。但是和monad有什么关系呢?我们没有利用到Maybemonad的好处。让我们修复这个问题。你也许注意到了runflatMap之间的相似性。两者都是处理空值并使用类似的逻辑: 如果一个Maybe实例是空的,那么返回None。否则,用解出来的持有的值继续计算。所以我们可以用flatMap实现run !

static run<R>(gen: IterableIterator<Maybe<R>>): Maybe<R> { 
    function step(value?){
        const result = gen.next(value)
        if (result.done) { 
            return result.value
        }
        return result.value.flatMap(step);
    }
    return step()
}

这个递归的实现更加有函数式变成的思想:

  1. 我们定义了一个step函数,其接受一个可选的value参数,并传给gen.next。我们知道,这将会使得在gen里面继续执行,直到最近一个yield
  2. 检查result.done, 如果是false(还有代码待执行), 我们只要在result.value上调用flatMap并递归地把step作为continuation函数传给flatMapflatMap已经处理了空值的情况。
  3. 只要我们没碰到None, step递归调用会继续执行gen, 直到下一个yield。这个过程不断重复。

Client代码还是一样的。

总结

在这片文章中,我们看到了如何通过生成器的使用提高了monad的使用体验。monad代码看上去更干净了,此外,团队合作中对习惯过程式变成的程序员也更容易了。这种使用生成器的方式也是async/await机制的基础。在后面的文章中,你会学到Promise也是monad并且async/await只不过是 function*/yield 的一种特殊变体。但这里先不去提前讨论:)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容