Typescript函数式编程: monad和generator

2019-12-05  本文已影响0人  PA滔滔不觉UL

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`的语义是:

换句话说,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 的一种特殊变体。但这里先不去提前讨论:)

上一篇下一篇

猜你喜欢

热点阅读