Jichao Ouyang edited this page Apr 19, 2017 · 4 revisions

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)

Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.