当前位置: 首页 > 科技观察

Ramda令人困惑的函数签名规则

时间:2023-03-20 20:31:10 科技观察

我们在查阅Ramda的文档时,经常会看到一些“奇怪”的类型签名和用法,例如:(Applicativef,Traversablet)=>(a→fa)→t(fa)→f(ta)或者,某些函数的一些“奇怪”用法://当只传递两个函数时,R.ap也可以用作S组合子//R.ap(R.concat,R.toUpper)('Ramda')//=>'RamdaRAMDA'将Ramda“更深层次”的设计逻辑背后的这些“奇怪”点投射出来。本文将对此进行解释,并讲解其背后的通用函数式编程理论知识。Ramda众所周知的方面Ramda通常被认为是Lodash的另一个“更FP”的替代库。与Lodash相比,Ramda的优势(一)在于完整的currying和datalastdesign编程(pipe)带来的便捷流水线。举个简单的代码对比例子:Ramda:constmyFn=R.pipe(R.fn1,R.fn2('arg1','arg2'),R.fn3('arg3'),R.fn4)Lodash:constmyFn=(x,y)=>{constvar1=_.fn1(x,y)constvar2=_.fn2(var1,'arg1','arg2')constvar3=_.fn3(var2,'arg3')return_.fn4(var3)}Ramdatypesignature在Ramda的API文档中,类型签名的语法有些“奇怪”:add:Number→Number→Number我们结合Ramda的柯里化规则稍微推测一下,就可以将这个转换将函数转换为TypeScript定义:exportfunctionadd(a:number,b:number):number;exportfunctionadd(a:number):(b:number)=>number;OK,那为什么Ramda的文档没有直接用TypeScript表达函数的类型呢?——因为更简洁!Ramda文档中的类型签名使用了Haskell的语法。作为一种纯函数式编程语言,Haskell可以非常简洁地表达柯里化的语义。相比之下,TypeScript的表达方式就比较臃肿了。当然,使用Haskell的类型签名的意义不仅限于此,我们再看看其他“奇怪”的函数类型:ap:[a→b]→[a]→[b]Applyf=>f(a→b)→fa→fb(r→a→b)→(r→a)→(r→b)结合文档中的demo:R.ap([R.multiply(2),R.add(3)],[1,2,3]);//=>[2,4,6,4,5,6]R.ap([R.concat('美味'),R.toUpper],['披萨','沙拉']);//=>["tastypizza","tastysalad","PIZZA","SALAD"]//R.ap也可以用作S组合器//当只传递两个函数时R.ap(R.concat,R.toUpper)('Ramda')//=>'RamdaRAMDA'[a→b]→[a]→[b]很好理解,就是笛卡尔积;(r→a→b)→(r→a)→(r→b)我们也可以这样理解,就是两个函数的串接;Applyf=>f(a→b)→fa→fb有点难懂,语法有点生疏,先翻译成TypeScript语法吧::),嗯,这个类型不能简单翻译成TypeScript,因为:TypeScript不支持“类型构造函数”作为类型参数!例如:typeT=F;错误消息如下:类型“F”不是通用的。在类型签名中,F是一个类型构造函数,与Array的“返回类型类型”相同,但是在TypeScript中没有办法声明“一个类型参数是一个类型构造函数”。就像例子中的typeT=F;一样,我们不能告诉TypeScript这里的F是一个类型构造器,所以当number被传入F时,就会报错。OK,我们假设TypeScript支持声明“一个类型参数是一个类型构造器”,让我们看看如何翻译Applyf=>f(a→b)→fa→fb:typeAP=(f:F<((a:A)=>B)>)=>(fa:F)=>F;这里的F可以理解为一种“上下文”。类型签名可以简单理解为:取出一个包裹在上下文中的“函数”,再取出另一个包裹在上下文中的“值”,调用函数后,将函数的返回值包裹到上下文中返回.这里的“上下文”是一个总称。例如,我们可以将其特化为Promise:类型AP=(f:Promise<((a:A)=>B)>)=>(fa:Promise)=>Promise;constap:AP=(f)=>fa=>f.then(ff=>fa.then(ff));ap或者说Applyasafunction是编程中常见的抽象,具有非常重要的学习意义,但其抽象分析超出了本文的范围。这里我们只关注“是什么”,暂时不考虑“为什么”。那么,(r→a→b)→(r→a)→(r→b)和Applyf=>f(a→b)→fa→fb是什么关系呢?他们是同父异母,(r→a→b)→(r→a)→(r→b)是Applyf=>f(a→b)→fa→fb的特化,就像我们为承诺。功能也可以是“上下文”吗?答案是肯定的,我们可以把一个一元函数a->b理解为“包裹在上下文中的ab”,但是为了得到这个b,我们需要传入aa。先看Haskell对ap的定义:instanceApplicative((->)r)where(<*>)fgx=fx(gx)被TypeScript的实现代替了。我们稍微修改上面的Promise例子得到:typeF=(a:any)=>A;typeAP=(f:F<((a:A)=>B)>)=>(fa:F)=>F;constap:AP=f=>fa=>{return(r)=>f(r)(fa(r));类似地,我们将Applyspecialization的实现作为Array:typeAP=(f:Array<((a:A)=>B)>)=>(fa:Array)=>数组;constap:AP=f=>fa=>{returnf.flatMap(ff=>fa.map(ff));};总之,我们可以得出结论,ap的类型签名是[a→b]→[a]→[b]并且(r→a→b)→(r→a)→(r→b)是Apply的特化f=>f(a→b)→fa→fb。