类型 uuid_从类型理解rxjs与async generator (一)

经历了react hooks的洗礼后,我们走进了rxjs的怀抱。本文尝试从类型系统角度去理解rxjs。

本文主要是本人对Erik meijer(RX的发明者) talk的理解(信息量很大,看了n遍),错误之处,欢迎指正

https://www.youtube.com/watch?v=sTSQlYX5DU0​www.youtube.com

另一个很有用的资料是 André Staltz的文章

JavaScript Getter-Setter Pyramid​staltz.com

我们首先仔细回顾下我们日常使用的javascript,基本上围绕着两个东西展开 function和value,下面详细讨论下function。

function几乎可以分为三类

  • getter:没有入参,有返回值
  • setter: 没有返回值,只有入参
  • setter&&getter: 既有入参也有返回值

为了简化讨论我们这里只讨论getter和setter,为了进一步简化讨论,这里不区分function和method

Getters

首先定义getter的类型如下

interface Getter<T> {
   ():T
}
interface XXXGetter<T> {
   xxx(): T
}

我们日常使用的函数很多都是getter,如Math.random(),uuid() ,getter相比直接的value访问一般有如下一些作用

  • 屏蔽获取值的方式,将使用方和获取值的具体方式解耦,甚至可以用来支持依赖注入
function getHost(){
   if(process.env.NODE_ENV === 'development'){
       return 'test.xxx.com'; // 返回线下测试地址
   }else{
       return 'prod.xxx.com'; // 返回线上地址
   }
}
  • 惰性求值,配合memo使用可以避免重复的计算,如react的useMemo和useCallback
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • 惰性求值,用来隔离副作用,典型的就是Reader Monad
  • 插入副作用,如每次访问某个对象时记录一条日志
function getUser() {
  Analytics.sendEvent('User object is now being accessed');
  return {name: 'Alice', age: 30};
}

Setters

setters恰好与getter相反,其只能接受参数,没有返回值,我们平时使用的很多函数都是setters,例如console.log, document.write,其类型签名如下

interface Setter<T> {
  (arg:T): void
}
interface XXXSetter<T> {
  xxx(arg: T): void
}

与getters相反,getter多用于抽象,而setters则更多的用于发送命令。我们当然也可以对setters进行封装

function fancyConsoleLog(str) {
  console.log('⭐ ' + str + ' ⭐');
}

Getter Getter

getter-getter仍然还是一个getter,只是其返回值也是一个getter,其类型签名如下

interface GetterGetter<T> {
   (): Getter<T>
}

getter-getter可以充分利用闭包的特性,进行状态封装,考虑下述函数

let i = 2;
function getNextPowerOfTwo() {
  const next = i;
  i = i * 2;
  return next;
}

如果我们想要我们的getter是有状态的,每次调用的结果都不一定唯一,我们如果不在内部实现里依赖其他的getter,那么只能将状态放在外部,但这对外部环境造成了污染。而且这种也不是并发安全的,当多个getter同时访问一个外部变量,结果很难预料。更好的方式则是再封装一层getter

function getGetNext() {
  let i = 2;
  return function getNext() {
    const next = i;
    i = i * 2;
    return next;
  }
}

let getNext = getGetNext();
console.log(getNext()); // 2
console.log(getNext()); // 4
console.log(getNext()); // 8
getNext = getGetNext(); //   restart!
console.log(getNext()); // 2
console.log(getNext()); // 4
console.log(getNext()); // 8
console.log(getNext()); // 16
console.log(getNext()); // 32

这样既防止了污染,也是并发安全的。

Setter Setter

setter-setter 也还是一个setter,只是其函数也是一个setter,如下就是一个setter setter

function addHelloEventListener(callback){
   callback('hello');
}
addHelloEventListener(result => {
   console.log('result:',result); 
})

我们发现这就是我们平时日常使用的callback,实际上行getter和setter在一定程度上可以相互转换

  • getter getter style
function getTen(){ // producer
  return 10;
}
function printResult(result){ // consumer
  logger.info(result);
}
printResult(getTen());
  • setter setter style
function triggerCb(cb){ // producer
   cb(10);
}
function printResult(result){ // consumer
  logger.info(result);
}
triggerCb(printResult)

我们可以将一个getter操作以setter方式的进行书写,这实际就是CPS变换

看看上面代码,明显settter setter style的可读性较差,那么这种写法还有用吗,实际上还是有点用的

我们观察一下上述代码,发现实际可以将功能拆分开来

  • producer: 生成者负责生产值,即这里的10
  • consumer: 消费者负责消费值:即这里的http://logger.info

对于上面的getter getter style,实际是消费者决定生产者何时生产值即pull模型,而对于setter setter style,是生产者决定消费者何时消费值,即push模型,这对于我们平时的业务建模影响很大。

Iterator: 给getter getter加一点约束吧

我们再回顾下上面的getter-getter

function getGetNext() {
  let i = 2;
  return function getNext() {
    const next = i;
    i = i * 2;
    return next;
  }
}

let getNext = getGetNext();
console.log(getNext()); // 2
console.log(getNext()); // 4
console.log(getNext()); // 8
getNext = getGetNext(); //   restart!
console.log(getNext()); // 2
console.log(getNext()); // 4
console.log(getNext()); // 8
console.log(getNext()); // 16
console.log(getNext()); // 32

已经具有了支持多次重入和有状态两种性质了,实际上一旦我们再给我们的getter加上一些约束,将会发挥更大的作用

如果我们把我们的getNext当做一个序列,我们发现我们上面的getter是不太好区分这个序列是否结束的。我们可以考虑约定下getNext的返回类型,加一个标志位来表示该序列是否结束,定义返回类型如下

interface IteratorResult<T> {
  value: T,
  done: boolean;
}

重写上述函数

function getGetNext(): IteratorResult<number> {
  let i = 40;
  return function getNext() {
    if (i <= 48) {
      const next = i;
      i += 2;
      return {done: false, value: next};
    } else {
      return {done: true};
    }
  }
}

此时通过标记为,我们就可以很方便的实现迭代了

let getNext = getGetNext();
for (let result = getNext(); !result.done; result = getNext()) {
  console.log(result.value);
}

我们发现这个约定不错,只要任何函数都是这样实现的,就可以很方便的对该函数进行遍历了,我们甚至可以进一步的定义一个类型,这里仍然可以将其视为一个返回IteratorResult的getter

interface Iterator<T> {
    next(): IteratorResult<T>;
}

这样只要我们的对象实现了该接口,我们就可以通过next接口对该对象进行遍历了,如下我们就实现了一个简单的可迭代类型

class GetNext implements Iterator<number> {
  i = 42;
  public next(){
    if (this.i <= 48) {
      const next = this.i;
      this.i += 2;
      return {done: false, value: next};
    } else {
      return {done: true, value:undefined};
    }
  }
}

简直完美,但是每次使用Iterator都写那么一坨for代码实在有点麻烦,要是有人帮我写这段代码多好啊,制定语言规范的那帮人,那么聪明早就帮你想好了,只要你实现了Symbol.iterator协议,就可以很轻松的使用for of对你的对象进行遍历了(这下你知道为啥能对数组进行for of遍历了吧)

Iterable: for of 的背后功臣

此时定义实现了Symbole.iterator接口的类型如下

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

修改上述代码,支持Iterable接口

class GetNext2 implements Iterable<number> {
  [Symbol.iterator](){
    return {
      i: 42,
      next() {
        if (this.i <= 48) {
          const next = this.i;
          this.i += 2;
          return { done: false, value: next };
        } else {
          return { done: true, value: undefined };
        }
      }
    };
  }
}

for(const x of new GetNext2()){
  console.log('result2:',x);
}

这下我们遍历就简化了很多,但是仍然存在不足,此时我们没法使用传统的next方法遍历GetNext2对象了,明显for of遍历虽然简洁,但是next方法遍历实际更加灵活,所以进一步的我们考虑是否能同时支持Iterable和Iterator接口,答案是可以的。

IterableIterator: Generator不过是IterableItreator的实现罢了

我们定义IterableIterator接口

interface IterableIterator<T> extends Iterator<T> {
  [Symbole.iterator](): IterableIterator<T>
}

我们这次尝试实现 IterableIterator

class GetNext3 implements IterableIterator<number> {
  private i = 42;
  public next(): IteratorResult<number> {
    if (this.i < 48) {
      return {
        done: false,
        value: this.i++
      };
    } else {
      return {
        done: true,
        value: undefined
      };
    }
  }
  [Symbol.iterator](): IterableIterator<number> {
    return this;
  }
}

for (const x of new GetNext3()) {
  console.log("GetNext3:", x);
}

for (
  let obj = new GetNext3(), result = obj.next();
  !result.done;
  result = obj.next()
) {
  console.log("result3.1:", result.value);
}

读到这里你会说,谁会写这种代码啊,看着好难懂,规范的作者早就帮你想好了,为IterableIterator提供了简化的实现方式: Generator

function *GetNext4(i=42): IterableIterator<number>{
  while(i<48){
    yield i++;
  }
}
for(const x of GetNext4()){
  console.log('GetNext4:',x);
}
for(let gen=GetNext4(),x=gen.next();!x.done;x = gen.next()){
  console.log('GetNext4.1', x);
}

让我回顾一下之前的定义的一些类型

  • IteratorResult: 为value类型, 约定了返回值类型
  • Iterator: 为Getter类型,提供了统一的for 循环遍历方式
  • Iterable: 为 Getter -> Getter类型: 提供了for 循环遍历的简化方式,支持for of 操作
  • IterableIterator : 为Getter -> Getter类型: 同时支持Iterator和Iterable两种遍历方式,
  • Generator: 提供了简化实现Iterablleterator的方式,函数返回类型为IterableIterator类型

实际上给getter getter添加约束的方式并不只有这一种, 不同的约束方式实际会构造出不同抽象原语,下篇会继续讨论其他的约束方式

Obsevable: 给setter setter 加一点约束吧

我们发现我们给getter getter加了一点约束之后,就展现了巨大的作用,那么如果我们考虑给setter setter 加一点约束,会有什么作用吗。

回顾一下之前的例子

function triggerCb(cb){ // producer
   cb(10);
}
function printResult(result){ // consumer
  logger.info(result);
}
triggerCb(printResult)

我们发现假如我们的triggerCb的实现不加以限制,其行为就会很难预料

  • 调用次数无法控制:如果cb不是幂等的effect,如果多次调用会影响业务,如转账操作,也有可能不调用导致后续流程无法继续进行
function triggerCb(cb){
   cb(10);
   cb(10);
   cb(10);  
}
  • 同步异步行为难以预料
let a = 10;
function mutateA(){
  a++;
}
function triggerCbSync(cb){
  cb();
}
function triggerCbAsync(cb){
  cb();
}
triggerCbSync(cb) // 同步调用当前修改对当前栈立即可见
console.log('a:',a);
triggerCbAsync(cb) // 异步调用当前修改对当前栈不可见
console.log('a:',a); 

当写业务时,我们当然不期望使用他人的提供的sdk时,还需要去阅读别人的代码,当时假如别人给我提供这个triggerCb的sdk时,我假如无法保证上述行为的确定性,又怎么敢安心去使用呢。

所以最佳的方式是,给setter-setter也加点约束吧。

我们现在将上述的函数换个名字

// before 
function triggerCb(cb){ // producer
   cb(10);
}
function printResult(result){ // consumer
  logger.info(result);
}
triggerCb(printResult) // 触发producer生产,同时producer调用consumer


// after 
function observable(observer){ // producer
   observer();
}
function observer(result){ // consumer
  logger.info(result);
}
observable(observer) // pro

// 换成方法看看

const obj = new Observable(observer){ // producer
   observer.next(10);
}
const observer = {  // consumer
  next(result){ 
    console.log('result:',result);
  }
}
obj.subscribe(observer) // producer 调用 consumer

至此我们发现原来Observable就是setter setter而已,只是多加了些功能的限制

  • 增加了 complete和error这两个setter
  • next的可以进行多次调用(不保证单词调用)
  • complete|error和next存在约束关系,如complete|error之后,不会再次调用next
  • 增加了unsubscribe
  • produce的生产是lazy的,只有consumer 订阅producer的时候才会触发producer生产数据

实际上Observable是对setter setter的一种限制,当我们对setter setter加上不同的限制,会得到不同的抽象。如Promise 对setter setter的限制却是另外一种景象

  • next 只允许执行一次,即next本身也意味着complete
  • then回调执行是异步的
  • produce的生产是eager,生成promise对象时就开始进行生产数据,完成数据生产则异步触发consumer

实际上setter setter的限制远不止这些,基于不同的限制我们可能进一步派生出更多的抽象原语,这就放到下篇说吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值