Data types à la carte in JavaScript

710 阅读4分钟

Data types à la carte in JavaScript

à la carte 意思是可以分开点的菜,跟 omakase 意思刚好相反。

这是一个简单的 Data Types à la Carte (如果英文好推荐读一下) 的JavaScript。实际上只是论文中的Haskell翻译成 PureScript,再编译成 JavaScript,因为类型系统的部分会在编译时丢失,所以我在上面又加了一层,把类型系统做的事情搬到了JavaScript的运行时。

搬到JavaScript后,可以解决各种 flux 的分支问题,因为不管是哪种*ux,都可以归类为定义表达式和翻译表达式的过程。不论是我写的 react-most 还是 redux,都会面临定义Action和switch case Action的厄运,而这种定义和switch case就会破坏开放封闭原则。导致多加一个action需要打开之前写的代码进行修改,而不能只是简单的添加。

安装

yarn add alacarte.js

太长不读;

之前 (The Expression Problem)

  const Intent = Type({
    Inc: [Number],
    Dec: [Number],
+   Mult: [Number],
  })
  const increasable = connect(intent$ => {
    return {
      sink$: intent$.map(Intent.case({
        Inc: (v) => over(lensCount, x=>x+v),
        Dec: (v) => over(lensCount, x=>x-v),
+       Mult: (v) => over(lensCount, x=>x*v),
        _: () => identity
      })),
      actions: {
        inc: Intent.Inc,
        dec: Intent.Dec,
+       mult: Intent.Mult
      }
    }
  })

之后 (Data Types a la carte)

  const {Add} = Expr.create({ Add: ['fn'] })
  const evalAdd = interpreterFor(Add, function (v) {
    return x => x + v.fn(x)
  });

  const evalVal = interpreterFor(Val, function (v) {
    return ()=> v.value
  });

  const evalOver = interpreterFor(Over, function (v) {
    let newstate = {}
    let prop = v.prop()
    return state => (newstate[prop] = v.fn(state[prop]), newstate)
  });

- let interpreter = interpreterFrom([evalLit, evalAdd, evalOver])
- let injector = injectorFrom([Val, Add, Over])
- let [val, add, over] = injector.inject()

+ const {Mult} = Expr.create({ Mult: ['fn'] })
+ const evalMult = interpreterFor(Mult, function (v) {
+   return x => x * v.fn(x)
+ });

+ let injector = injectorFrom([Val, Add, Over, Mult])
+ let interpreter = interpreterFrom([evalLit, evalAdd, evalOver, evalMult])
+ let [val, add, over, mult] = injector.inject()

  const counterable = connect((intent$) => {
    return {
      sink$: intent$.filter(isInjectedBy(injector))
                    .map(interpretExpr(interpreter)),
      inc: v => over(val('count'), add(val(v))),
      dec: v => over(val('count'), add(val(-v))),
+     mult: v => over(val('count'), mult(val(v))),
    }
  })

例子

为什么

如果把Action看成表达式,Reducer看成解释器,就会出现 表达式问题 ,一旦要添加新的表达式,就躲不过要修改之前的定义。

Data Types à la CarteFrom Object Algebras to Finally Tagless Interpreters 对表达式问题的解释都比我要好,推荐英文好的看一看

怎么用

有了 Data Types à la Carte, 我们可以在任何地方定义表达式,在任何地方定义表达式的解释器,只需要在最后使用时,给定表达式用到的类型。

定义表达式

let {Add, Over} = Expr.create({
  Add: ['fn'],
  Over: ['prop', 'fn']
})

Add 是表达式类型名字,=[‘fn’]= 表示该数据类型参数,叫 fn

Over 也一样,第一个是 prop 第二个是 fn

解释器

然后,定义表达式类型的解释器

// Instances of Interpreters
const evalAdd = interpreterFor(Add, function (v) {
  return x => x + v.fn(x)
});

const evalVal = interpreterFor(Val, function (v) {
  return ()=> v.value
});

const evalOver = interpreterFor(Over, function (v) {
  let newstate = {}
  let prop = v.prop()
  return state => (newstate[prop] = v.fn(state[prop]), newstate)
});

Val 类型唯一特殊的类型,所以 alacarte.js 自带,你不需要定义,只需要 =import {Val} from ‘alacarte.js’= ,然后实现它的解释器就好。Val的参数名一定叫 value。

组合这些解释器,就会得到一个能解释由 Val :+: Add :+: Over 三种类型的表达式

let interpreter = interpreterFrom([evalVal, evalAdd, evalOver])

注射器 💉

要将单个表达式类型注入到 Val :+: Add :+: Over 类型,需要注射器的支持

let injector = injectorFrom([Val, Add, Over])

现在注射器 injector 可以将表达式注入到 Val :+: Add :+: Over 内

let [val, add, over] = injector.inject()

调用注射器的 inject 方法,会得到注射好的表达式构造函数,这些构造函数分别被注射成类型约束

val :: (Val :<: (Val :+: Add :+: Over)) => Num -> Expr Val :+: Add :+: Over
add :: (Add :<: (Val :+: Add :+: Over)) => Expr Val :+: Add :+: Over -> Expr Val :+: Add :+: Over
over :: (Over :<: (Val :+: Add :+: Over)) => Expr Val :+: Add :+: Over -> Expr Val :+: Add :+: Over -> Expr Val :+: Add :+: Over

约束 (Val :<: (Val :+: Add :+: Over)) 的意思是 Val 可以注入到 (Val :+: Add :+: Over),保证类型安全

添加表达式

先在,来看看新加一个表达式 Mult 如何不影响以前的表达式和解释器定义

let {Mult} = Expr.create({
  Mult: ['fn'],
})
const evalMult = interpreterFor(Mult, function (v) {
  return x => x * v.fn(x)
});

这样就定义完了 Mult 类型与其解释器,不需要修改之前的代码,而只需要在使用时,使用带 Mult 的注射器生成的表达式构造函数构造表达式就好。

let interpreter = interpreterFrom([evalVal, evalAdd, evalOver, evalMult])
let injector = injectorFrom([Val, Add, Over, Mult])
let [val, add, over, mult] = injector.inject()

然后就可以开始组合表达式了。

let expr = over(val('count'), mult(val(4)))

JS 在使用时会稍微啰嗦,因为类型系统做的事情需要手动做一些,想Haskell只需要

let x :: Expr (Add :+: Val :+: Mult) = add(mult(val(1), val(2)), val(3))

前面js干的 interpreterFrom 和 injectFrom 都可以由类型系统编译时推出来。

新加一个解释器

比如说我们需要一个新的解释器,这个解释器会把表达式解释成字符串

const printAdd = interpreterFor(Add, function (v) {
  return `(_ + ${v.fn})`
});

const printVal = interpreterFor(Val, function (v) {
  return v.value.toString()
});

const printOver = interpreterFor(Over, function (v) {
  return `over ${v.prop} do ${v.fn}`
});

const printMult = interpreterFor(Mult, function (v) {
  return `(_ * ${v.fn})`
});
interpretExpr(printer)(expr)

会打印出来 count + (count * 2)