签到

05月06日
尚未签到

共有回帖数 0

    做月子

    等级:
    1 今昔东途
    ——我如何组织故事,故事如何改变我

    1.1 例:Perl5 - 按名克里化的失败

    曾经有名Perl僧人给出了这么个悲剧——他试图按名字对Perl函数施行克里化(currying. 余谈,姬海棠羽立曾经提出过「克里化米饭」——明确地说是「对米饭施行克里化」——的概念,虽然我认为被克里化的主要是除了米饭之外的其他材料)的等作用行为,即允许函数的部分应用。勇气可嘉,不过结果实在不尽人意。

    实际生成克里化的代码很简短:

    sub curry { my $f = shift; my $args = @_; sub { $f-(@$args, @_) } }

    函数curry生成一个被克里化的包装,但使用起来并不显然。对待变换的函数f, 考虑施行一次curry的情况:

    curry($f, @args) = sub { $f-(@args) }

    但是这显然不是我们所需要的样子。直觉上curry这个包装应当是迭代的,每次抽出下一个调用留待施行,并提供对应的参数。如果读者对CPS(Continuation-Passing Style)有印象的话,大概可以更方便地理解这里构造迭代的方式——实际上我们需要传递续延(continuation)。

    原例试图构造这样的调用:

    $g = curry(&curry, $f)

    考虑这里的结果。生成的包装是

    $g = sub { curry($f, @_) }

    我们试图对$g多次应用参数:

    $g-($a) = sub { curry($f, @_) }-($a) = sub { $f-($a, @_) }

    危险、危险、危险。至此我们已经失去了curry期望的不变式,或者说进入了演算的收束阶段,应当对施行于f的应用进行求值了。如果读者仍然没有看出下面的发展,可以再进行两次应用:

    $g-($a)-($b) = $f-($a, $b)
    $g-($a)-($c) = $f-($a, $b)-($c)

    最后一式已经没有救赎。如同我们在看到curry的实现之后所期望的那样,施行三次应用就可以毁掉克里化包装:因为这里并没有持续传递续延,或者说没有真正迭代下去。或许读者会反射性地意识到这里可以试着用call-by-value Y combinator( namely, f.(x y.f (x x)) (x y. f (x x)) )施行匿名递归,不过我们没法声明递归的终止条件。重新审视克里化的情况——每个函数被表为一元函数或是一元函数的复合,但函数本身是被明确给出的。或者说,即使不限定类型,也限定了自变量的数目。

    对于Perl 5来说,很难做到这样的事情——函数不具有类型,也不严格限定其抽象,换言之没有严格的在应用时将自由变量替换成绑定变量的要求。在没有任何契约或是缺乏完整抽象的情况下施行部分应用,无异于用参数填满不定参数列表并决定施行最终的调用。这已经不是无类型λ演算的问题了(λ演算假定函数是一元的,并且明确给出抽象),而是语言天生的pitfall. 当然,在函数具有确定参数列表但语言不缺省提供部分应用支持的情况下,也不见得能写出合适的克里化或是等作用替代品——尤其是在希望避开反射的情况下。虽说拒绝把类型抽象带入运行时并不是应当提倡的做法,但在运行时大张旗鼓地对类型加以判断仍然是在引入奇怪的分支,致使控制流进一步奇葩化——我们试图用多态性或/和模式匹配避免引入的东西死灰复燃,直觉上看来这并不好;另一方面,过量控制流的引入似乎也容易让语言在需求上失去对rest of the program的表述能力——或者说,以肉眼看来,强调步骤和分支的东西相对更不在乎「演算」的意义:这不见得是坏事,因为这样的表述不见得是迫切需要的,但不代表不该列入考虑。
    1.2 例:C实现 - 手操闭包的失败

    另一个例子来自C. 这里我们试图给出一个仿闭包实现,虽然没法做到完整的一等函数,但至少要求实现自动捕获。单纯从C语义上的解法我并没有完整考虑过,或者说应该是不可行的——宏没法探测它之外的实体或是试图了解其中的实体。给出的实现使用了gcc的nested function扩展、标识符命名扩展、类型反射扩展以及内嵌汇编。

    #define _closure ({
    #define _lambda(_rt_, ...)
    _rt_ $(__VA_ARGS__)
    #define _closure_end
    void *_rsp_, *_rbp_, *_cntx_;
    asm volatile ("movq %%rsp, %0" : "=r" (_rsp_));
    asm volatile ("movq %%rbp, %0" : "=r" (_rbp_));
    char *_tramp_ = (char *) malloc(sizeof(char) * (19 + (unsigned long) (_rbp_-_rsp_)));
    memcpy(_tramp_, $, 19);
    _cntx_ = *((void **) (&_tramp_[8]));
    memcpy(_tramp_ + 19, _cntx_, (unsigned long) (_rbp_ - _cntx_));
    *((void **) (&_tramp_[8])) = (void *) (_tramp_ + 19);
    (typeof(&$)) ((void *) _tramp_); })

    #define _closure_free(_x_) free(_x_)

    实际的用法比如:

    #include stdio.h
    #include closure_gcc.h

    int (*make_adder(int base, int base2))(int, int) {
    int a = 1;
    char arr[] = "hello";
    int (*adder)(int, int) =
    _closure
    _lambda(int, int arg1, int arg2) {
    puts(arr);
    return arg1 + arg2 + a + base;
    }
    _closure_end;
    return adder;
    }

    int main(void) {
    int a = 1, b = 2;
    int (*adder)(int, int) = make_adder(3, 2);
    puts("ayayaya?");
    printf("%dn", adder(a, b));
    _closure_free(adder);
    return 0;
    }

    单纯一层的捕获没有问题,栈帧也还存活。不过实际上在下面的例子上就会崩溃:

    #include stdio.h
    #include closure_gcc.h

    typedef int (*functype1_t)(int);
    typedef functype1_t (*functype2_t)(int);

    functype2_t f(int x) {
    functype2_t g =
    _closure
    _lambda(functype1_t, int y) {
    functype1_t h =
    _closure
    _lambda(int, int z) {
    return x + y + z;
    }
    _closure_end;
    return h;
    }
    _closure_end;
    return g;
    }

    void testfunc(functype1_t func) {
    printf("%dn", func(5));
    }

    int main(void) {
    puts("some test");
    printf("%dn", ((f(1))(2))(3));

    functype1_t g = (f(1))(2);
    testfunc(g);

    return 0;
    }

    gcc/x86-64在不优化的情况下可能存活,因为生成的代码会把参数复制到栈上。在调用够早的情况下,这些超出作用域和生存期的东西仍然没有被覆写,使得实现的函数仍然能拿到旧有的值。-O2的话会直接崩溃,这其实也悲惨地在预期之内。gcc为nested function生成一段跳板代码并假定栈可执行,从而将这些代码置于栈上,同时为nested function生成常规的代码段落。跳板代码处理的问题主要是外层函数的局部量——或者说内嵌函数的上下文。对这里的64位代码而言,用于索引局部量的基址被填入r10, 此后跳转到实际的函数代码;宏_closure_end则为跳板代码和上下文提供堆上的空间,并改写mov r10, *的第二操作数为堆上的跳板+上下文拷贝。单纯使用r10的话,自然只允许一层的嵌套。——如果允许直接给出捕获以加强条件,仍然没有多少回旋的余地(考虑使用两层作用域和一个中间名字对捕获变量进行中转的情况,同时这里需要使用typeof反射出捕获变量的类型,构造出新的跳板代码和上下文。新跳板代码中应当涉及上下文的重写——也即假定已知caller的上下文位于某处,则将callee的上下文和caller进行合并;到这里读者或许会意识到捕获这个条件可能可以放松,但无论放松与否都涉及到另一个难以回避同时难以克服的问题:函数代码中相对上下文的偏移在编译时将会给定,不作额外hack则无法和新上下文中的偏移同步变化,同时无法预估访存的方式和时机——也即难以明确区分两种上下文的访问,从而不容易通过调整基址来寻址旧上下文中的变量)。

    实际上这里我们落入了一个更凄惨的境地。由于无法干预求值,实现者被迫去和实现做妥协式的斗争——这已经使得付出的努力不具有多少意义。Gophers所谓的抽象拆解之所以能成功,是因为他们给出了语义和编译器的实现——而实际上他们并未拆解任何语义抽象,反倒是依旧顺着语义的路走下去,只不过没有一本正经地希望靠中间表示和额外的分析对相应的后端代码生成进行指导;换言之,他们的拆解是实现之直觉——而这个直觉倒是UNIX的传统:由于语义本身并没有吸引多少注意,直觉之实现更容易起到吸引眼球的作用。然而从完全或不完全的第三方看来,他们仍然受制于既定抽象,这同样是抽象没被拆解的证据;语义仍然僵硬,用户仍然能感觉到求值的制约和phase的高墙。而到去年(2012)为止,Go给我的感觉仍然像是Java 1.4的替代品:简单未必带来更多的自由和可用性,或者说自由并非建立在单纯试图被限制起数量的语言特性上——实际上自由会来自抽象的程度而非历史性的需求,一组接近理想的核心抽象较之一系列实现简单的特性重作品更容易从语义上解放用户——同时我们不妨注意到限制数量的尝试不见得是有效的,毕竟粗暴的语义带来的矛盾和对额外规定的需求导致语义逐渐难看地成长的例子之一就是C. 在这个意义上,C并没有承诺多少自由性——虽然用户具有自由实现上层建筑的权利,但如果这些权利都被剥夺了,他们还能剩下什么呢?思考的自由并非建设的自由,语义的抽象并非中间结果(实现)的抽象——不过恰当的抽象也未必是更「高」的抽象……尤其假如邪教徒乃至同袍们试图鼓吹产生自本能之归纳的轻内聚式抽象即为具有表现力的「高层抽象」这一观点的话。不管选择产生抽象还是主推实现特性,总之我们仍然应该抛开成见重读现状。
    1.3 薄铁如丝

    下面的话题或许不免和后面重复,但是相信读者从自身受到的计算机科学/技术方面的训练大概也能了解到,不那么根源的知识往往纠结在一起,被表现力和需求驱动着回旋前行。这里我要谈论的是语言描写软件系统的方式——更多地是描写而不是描述,是强调细部、细节和可能在控制不当时产生邪恶代码的场合,然而也是真正独立出来时候会富有表现力乃至人情味的部分。考虑一个事件驱动的软件系统——读者可以选择自己熟悉的实现,它可能是libevent, nginx核心,nano, kirno(U酱十五岁!)或者其他样子的东西;考虑其中分用事件的方式,考虑事件驱动框架在其中所扮演的角色。独立的事件驱动框架往往要求注册事件和事件对应的动作,而这些书写上离散的组分构成了软件系统的行为——同时,事件驱动框架和其他基础部件构成了软件系统的机制。这是解耦没错,但是解耦的结果如何呼应我们的直觉呢?回想我们写下hello world这样的c程序的时候——我们书写入口函数,写下输出的调用,并要求入口函数返回——这是一件完整的事情。肌肉和皮肤并不因为独立于骨骼而在体表上显得离散——当然如果你身高五十米还能喷出蒸汽那就是另一回事——相反,因为有骨骼的存在,它们才更有效地构成躯体,显得健壮或是可爱或是可爱到不行。

    我们实现机制。我们实现框架。我们实现工作流。然而离散的事件和对应的动作鸟瞰起来更是直觉看来的耦合——它们附着在事件框架上或是其他的机制上,露出框架的形状,但并没能描述它们所刻画的软件系统究竟做了什么——对于一个大系统来说这恐怕难以避免,因为机制的层次并不见得容易确定和区分,但对于不会很大的应用程序呢?会有人为野地里可爱的花朵举行一个仪式,全心全灵为它赐福吗?——倒不如这时候归回到形如泛灵论的地位上,重新以用例来考虑软件,并试图在单独的层次上描绘用例。这可能只是一个简单的自动机抽象和加载——这样的描述甚至不见得能被拣选进入一等对象的天堂——但是它直观地描述了应用程序是什么和做什么。从另一个角度看来,这使得应用程序被容许转瞬即逝,强调框架的可信度并扩大了可编程的范畴。我们是开发者。我们不满足于胡乱点击鼠标或者输入命令,我们同样不满足于给定的配置文件;我们之所以没有对手头的应用软件加以修改是因为它们把业务和协议埋得有点深而我们太累了——然而我们不见得会放弃对软件本身编程的想法,因为编程是我们与软件交互的方式之一……是富有技巧性和表达力的方式。反观前面的两个例子,我们需要做什么呢?确实我们需要一等对象,不过在没有的时候也不要忘记我们在做什么——毕竟阅读和编写代码的经验已经开始破坏直觉了。请谨慎抉择(如果有可能的话)。
    2 行礼如仪
    ——……魔法语言中的「a」……读作/ai/

    2.1 单子和推导式

    标题虽然泛泛地说了推导式,但我打算主要关注单子推导(后面我们会作出一个列表推导)。考虑类型构造子M, 若M类型的值有两个运算

    return :: a - (M a)
    bind :: M a - (a - M b) - M b

    我们称(M, return, bind)是单子(Monad)。不过我们出于方便考虑,可以导出第三个运算

    then :: M a - M b - M b

    也即在忽略bind中第一项应用和第二项应用之间的关联,比如下面的定义

    then x y = bind x λz.y

    项λz.y具有类型a - M b, 假定x具有类型M a而y具有类型M b——但y是自由变量。

    同时考虑单子应有的性质:

    bind (return v) f = f v
    bind f return = f
    bind f (λx.bind (g x) h) = bind (bind f g) h

    前两条性质是平凡的。单纯的返回——绑定即为平凡的复合,这里的v只需具有类型a. 同时bind和return互逆。第三性质看起来有点晦涩,不过我们可以把它写成中缀式

    f `bind` (λx. (g x) `bind` h) = (f `bind` g) `bind` h

    这就是结合律。连续的bind操作产生同样的顺序,实际上这保证了在函数式范式当中可以存在严格的一般序(normal order)求值,也给出了单子推导式(Monad Comprehension)的定义。经由分配律的连续复合,我们可以写出

    p1 `bind` λx1.p2 `bind` λx2.p3 ... λx(n-1).pn `bind` λxn. return (f x1 x2 ... xn)

    这样一个顺接的形式,不妨将其记作

    [ f x1 x2 ... xn | x1 - p1, x2 - p2, ..., xn - pn ]

    这就是一个只含生成式(generator)的单子推导式。[* 实际上,推导式中也可以含有卫式(guard, 即不产生状态变化的单纯的谓词)]。假如pi具有λz.P(假定P中不含绑定变量z)的形式,那么这里的前后关系会是更宽泛的then. 在这个前提下,如果我们构建列表单子,就很容易使用单子推导式给出求值序受限的列表推导式,比如下面的笛卡尔积

    product l1 l2 = [ (x, y) | x - l1, y - l2 ]

    实际上是

    l1 `bind` λx. l2 `bind` λy. return (x, y)

    而l1, l2具有单子类型List

    return x = make_list x
    bind l f = map_then_concat f l

    其中原语map_then_concat将f应用至l的每一元素上并展平结果生成单一的列表,假定f具有类型a - List b而l具有类型List a.

    这时候我们对某些隔离IO action的手法加以重新评判——考虑下面的例子。设有类型构造子IO和如下操作

    getLine :: IO string
    putStrLn :: string - IO ()

    其中()表示一个动作(action)。读者应该能意识到这里的M a和a - M b形式,以及顺接结果的M b形式。读者甚至会意识到这是Haskell, 而Haskell实现了使用单子隔离IO的尝试——那么这里到底发生了什么?抛开繁复的想法,单纯考虑静态强类型的类型系统要求。如果我们定义一个值v,它往往具有(意会)「正常」的类型t; 在典型的过程式语言中,我们允许定义函数

    getval :: t1 - t

    从而书写v = getval u是可行的,不过u往往是隐式的。但是注意到我们为了完整书写非平凡的定义域,引入了一个类型t1, 这可能是所谓的real world或者runtime/io context. 直觉要求我们把getval的类型改写成下面的样子:

    getval :: cntx - (type, cntx)

    其中(x, y)表示一个有序对。有没有发现这里的context是需要传递的?换言之,在一序列的IO动作A1, A2, ..., An当中,对合适的i, A(i+1)的输入依赖A(i)的结果。那么从朴素的第一考虑出发,构建IO单子并施行单子推导(或是更本愿地,施行bind)就可以统一这样的形式。同时注意到cntx - (type, cntx)这形式,实际上可以是一个关于type的IO类型构造子(泛泛地说,实际上它是关于type和cntx的类型构造子,但假设了IO背景的话cntx是暂时可以视作常量的)——我们可以顺利地把它作成单子。另外一方面,回到getLine和putStrLn的例子:注意到IO string不是string(这显然是两个类型,请不熟悉这类型系统的读者注意),故而偷偷塞进一个算符(Haskell叫它「-」)使得IO string下降为string变得可能(实际上我不觉得说“下降”很合适,因为IO string的责任并不是单纯的string. 如果读者觉得IO的黑盒还是有点迷糊,可以重新参考比较好懂的单子化文法分析组合子(Monadic Parser Combinator),可能更容易理解单子推导中间结果之类型的意义变化——这里就明显不是「提升」和「下降」的关系,而是表示「分析状态」)。整体来说,用于IO的单子推导首先保证了求值序,同时给出了状态传递的简记法,并使用状态(同时也是类型)隔离了两个世界(缺乏完整状态变化过程的连续IO是无意义的,因而这样的IO被限制在推导中;从IO得到结果的过程在脱离上下文的情况下是无意义的,因而这些过程被「感染」而限制在推导中)。

    当然这或许真的是个——如同yinwang指出的一样——理论hack. 我们的出发点是对IO函数加以制约,强迫埋没在幕后的context显现出来并加以演算,但我们避免了什么?实际上的可能是无副作用的代码被嵌在单子推导的过程当中,其结果仍然是难以评估或者复现的。举例而言,典型的情况就是并发和消息机制:同一段无副作用代码会在两个主动成分上表现出难以复现的结果,这和我们书写一般的并发程序差异并不大,最多是从字面上避开了共享内存;但这样的结论并不令人满意,毕竟erlang这种无节操的伪函数式语言已经达到了相当程度的隔离,这里并非单纯的函数式所直白涉及之领域。不过目前至少我们可以通过单子推导将IO整合进静态强类型的函数式语言当中,我并不认为这毫无价值——看上去还是满有趣的。与其说是理论hack, 倒不如调侃地说是理论happens?
    2.2 辉夜华服

    创作和使用语言对于不少人来说看上去是艰涩无趣的事情,然而这不能掩饰语言本身的华彩——或者说,如果onelang真的存在,反倒就没那么多趣味了。挣扎、反驳、批评和妥协使得语言的设计者和使用者为自己制造希望,同时又在希望的不确定性或是挫败感中找到前行的乐趣所在。抛开这些,单单构建适当的语言表示也颇有简易的乐趣——不过就像我们在前面的故事中所见到的一样,单纯想把语言的分析器和语言的文本救赎为一等对象需要额外的努力和折损,这并不是多数常用语言能够轻而易举做到的。另一方面,我希望在这里尝试给出另一些形式上的讨论。考虑程序设计语言的语法:如果这语法能够直观地反映语义,那么实际上操纵语义对象的手段就变得明晰。考虑一个黑盒的函数和一个被当作应用(application term)解释的列表就会知道这一点:也即,演算较之指令及其附属设施更容易凭直感理解和操作;然而回到使用中的语言上,或许我们都会有钟爱的对象表示——举例来说,从形式上来看我喜爱各样的推导式,而单纯写出推导式的话并不见得利于程序对程序的操作,因为推导式比较复杂,而单纯把推导变化成原语并加以应用就破坏了身为一等对象(但容易被展开成糖)的推导式之形式。另一个例子是用于局部使用之符号定义的where子句,这里不再赘述。

    有时我们面临的问题不再是「如何操纵程序」或是「如何提供同一」,而是「何时引入操纵程序的机制」。在我所见到的语法中,并没有能够权衡这两侧的。在缺乏同一的情况下引入额外的对象表示——正如同我们在某些函数式语言中看到的,除去强制缩进的话仍然颇可喜的形式一样——容易破坏操纵程序的方式,而单纯的演算之演算容易陷入纯粹表示的困境——有能力给出适当的演算,但这些东西呈现出来的形式未必好看。这里我们会如何选择?或许会提出把适合演算的语言用作中间语言的想法,但这样的做法的负担未免太重了——中间语言仍然保持了待分析的语义,如果没有实用的后端则是什么变化都没有,相反的情况则稍微好点(可以作为前端demo的示例,但未必能干净地进行体系无关优化——当然这可能是我见识太少)。或者说,元编程是个诱惑,但这诱惑真正被大量投入使用的情况较之使用者希望改变文法以构造一等DSL的情况哪个更接近手头的需求?进一步澄清这个问题——文法应当试图暴露出语义的形式,还是应当提供相当的可塑性,或者这两件事情具有相同的核心,其实应该引入相同的机制让用户自行解决?举例而言,Haskell及其部分方言中就选择了容易实现的方式,提供了把二元函数应用写成中缀二元算符的情况。这是容易模仿和使用的部分。更一般地说,模式匹配这一手段是在做什么?考虑到类型的情况,是否可以通过引入模式匹配完成特定演算形式的构造?这一构造是否会引发歧义?——往下的讨论,这样的模式是什么?可能是什么?保持形式上的一致性是一方面,但这样的一致性是否会滋生对象表示中异类的部分?

    我偶尔会有这样的疑问。然后我就会按mod4-1切回主工作区,看看bash. 那时候我感觉到果然应该作公主一样的打扮,套上舒适的运动服和裤子而不是适合身份的衣服。
    3.2 例:listT in C

    这是个失败作的副产物,总体上来说也是失败的。请记得下面的代码不是listT——因为它不是个容器,而且也缺乏类型检查。

    我曾经想用C试作类型迹(type traits),但是最后我走神地写下这样的测试:

    #include stdio.h
    #include list_tmpl.h

    struct point2d {
    int x, y;
    list_ctl_t lctl;
    };

    int main(void) {
    struct point2d p = { .x = 1, .y = 2 };

    list_ctl_t h;
    init_list_head(&h);
    list_add_tail(&(p.lctl), &h);

    iter_type(struct point2d) itr;
    list_foreach(itr, &h) {
    itr2obj(point, itr);
    printf("%d %dn", point-x, point-y);
    }

    return 0;
    }

    其中

    struct list_ctl_struct {
    struct list_ctl_struct *prev, *next;
    };
    typedef struct list_ctl_struct list_ctl_t;

    读者应该对这个来自LK的形式不陌生,所以我不多加注解。

    我希望提供一个单纯的迭代器而不是需要显式指出类型和成员的container_of. 大方向上别无选择地,我给出了这样的迭代器实作:

    #define iter_type(_typename_)
    struct {
    list_ctl_t *itr;
    _typename_ typeref;
    }

    当然这里写成具名的结构更好,不过演示的话怎么都可以了。节操不多的读者或许会意识到我打算编译期反射出类型,因为这里我没有模板,更不用提特化或是偏特化。在此之前,先确保迭代式的foreach遍历形式上是正确的(在我给出的演示实现中,需要覆盖掉已有的的定义):

    #define list_foreach(_iter_, _head_)
    for ((_iter_).itr = (_head_)-next; (_iter_).itr != (_head_); (_iter_).itr = (_iter_).itr-next)

    这里我没能提供erase式的遍历,因为形式上会很难看,也没法和容器关联。添加额外的作用域可以解决这问题,但形式上同样难看。最终我选择保持for循环的原本形式。

    实际的鬼畜发生在这里:

    #define _itr2obj(_iter_, ...)
    container_of((_iter_).itr, typeof((_iter_).typeref), _if(__VA_NARG__(__VA_ARGS__))(__VA_ARGS__, lctl))

    #define itr2obj(_object_, _iter_, ...)
    typeof(_itr2obj(_iter_, __VA_ARGS__)) _object_ = _itr2obj(_iter_, __VA_ARGS__)

    第二个宏很直白,给出了按对象名字的定义。辅助宏是对container_of的简单封装,使用typeof扩展在编译时从迭代器中反射出对象类型并按链表节点在对象结构中的成员名字(由不定参给出,或是缺省的lctl)取得对象指针。宏的缺省参数处理由下面的分支宏产生:

    #define _primitive_cat(_arg_, ...) _arg_ ## __VA_ARGS__
    #define _if(_cond_) _primitive_cat(_if_, _cond_)
    #define _if_1(_term_, ...) __VA_ARGS__
    #define _if_2(_term_, ...) _term_

    这里的_primitive_cat和_if只是把文本_if_和参数_cond_连接起来,期望产生_if_1或是_if_2. 1和2是_cond_展开的结果,注意到实现中的_cond_是__VA_NARG__的结果——这个宏不复杂但占了很多行,所以我建议大家参考它原先的出处(groups.google.com/forum/?fromgroups=#!topic/comp.std.c/d-6Mj5Lko_s)。于是在没有不定参的时候,结果被展开成_if_1,否则展开成if_2:对无类型λ演算有了解的读者应该能注意到这里的两个_if分支与真假/car-cdr的联系,我重新把它们写出来:

    P, Q = λz.z P Q
    fst P, Q = P, Q true
    snd P, Q = P, Q false
    (P, Q, ..., R) = λz.z P, Q, ..., R, nil

    虽然这样的相似程度实在是太一般了所以意义不见得大或是有,毕竟car-cdr也算是列表比较自然的一面。

    楼主 2015-10-25 13:31 回复

共有回帖数 0
  • 回 帖
  • 表情 图片 视频
  • 发表

登录直线网账号

Copyright © 2010~2015 直线网 版权所有,All Rights Reserved.沪ICP备10039589号 意见反馈 | 关于直线 | 版权声明 | 会员须知