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 Carte 和 From 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)