`
netbabe
  • 浏览: 24590 次
  • 来自: 北京
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

一个面向Scheme程序员的monad介绍

阅读更多

今天整理硬盘时翻出一篇旧的翻译稿,那是多年前“计算机英语”课程上要求交的一篇作业,
也是哥投身此行业后不多翻译过的东西(还有一篇记得是被面试时考官要求的),作为英语水平
差强人意之人,当初挑这篇翻译是有小算盘的,代码多文字少,能少翻就少翻,免得“翻多必失”。
     好久没空在博客园上发帖,只好把陈年旧货拿出来晒一晒,希望对想理解monad机制又不想学
习范畴论的朋友有所帮助,当然高手就直接跳过算了。
     说说我的心得吧,对Haskell引入程序世界的monad,先是不屑一顾,哥不需要,要副作用
直接赋值就行了,吃饱了撑着搞那么复杂的东西干嘛。后来看许多大牛推荐,于是就有了好奇心了,
到底是个嘛玩意,引无数英雄尽折腰的。起先企图从范畴论入门,结果苦不堪言,一直没能把这么
抽象的东西和实际编程联系起来。后来学了一点Haskell,又看到这篇文章,才建立起一点直觉。而
且惊奇的发现,它居然是比CPS更高的一个抽象,不仅感叹数学逻辑的力量真是强大。同样是编程序,
数学家们居然构造出这么深刻的东西。

     这里是原文链接,以下为译稿:
一个面向Scheme程序员的monad介绍
Dave Herman
 
I.简介
 
    这是一篇从Lisp/Scheme角度对monad技术进行介绍的文章,读者需对延续、延续传递模式、累
计器以及累计器传递模式有所了解。
    monads 为我们提供的主要洞察是,所有编程副作用诸如变量改写、输入输出,到非终止性,都与
计值顺序相关。而对于简单、可终止的纯lambda表达式,计算顺序是无关紧要的:无论你怎么对它进
行归约化简,最终结果是没有差别的。但是当程序有副作用时,就必须按正确的顺序先后计值。
(Monad不是唯一的处理副作用的范型——延续传递和A-normal模式也能处理它。但它们几个范型都
是有关联的。)
 
II.延续传递模式
 
     由于monad是在纯数学语义的背景下讨论副作用,我们也不妨试一试用Scheme纯函数子集来编写
有副作用的程序。
 
    (begin (turn-on-safety!)
           (pull-trigger!))
 
     在我们的纯函数Scheme中,我们必须把BEGIN表达式定义为对它两个参数的先后计值(我们将只
考虑两个参数的情况),但最终表达式的结果是后一个参数的结果值。很自然可以这样定义:
 
    (define (begin v1 v2) v2)
 
    一个纯函数Scheme可能会按任意次序对程序进行求值:
 
    (begin (turn-on-safety!)
           (pull-trigger!))
 -> (begin (turn-on-safety!)  ; effect: pull-trigger!
           #<void>)
 -> (begin #<void>            ; effect: turn-on-safety!
           #<void>)
 -> #<void>
 
      这就不对了,上面这种做法可能会让我们的某一位论文委员会成员勃然大怒。让我们对程序进行
延续传递变换(CPS),以确保其按正确的次序被求值(下面我使用[[--]]来表示对一个表达式进行
CPS变换):
 
    [[(begin (turn-on-safety!)
             (pull-trigger!))]]
  = (lambda (k)
      ([[turn-on-safety!]] (lambda (res1)
                             ([[pull-trigger!]] (lambda (res2)
                                                  (k res2))))))
 
   这样在一个以延续传递模式编写的程序中,我们可以把BEGIN定义为:
 
    (define (begin cps-exp1 cps-exp2)
      (lambda (k)
        (cps-exp1 (lambda (res1)
                    (cps-exp2 (lambda (res2)
                                (k res2)))))))
 
     注意第一个表达式cps-exp1的求值结果被参数res1所接收,但随后就被忽略了(后面没有任
何子表达式对res1再进行引用)。
 
III. 累计器传递模式
 
     我们已经会编写累计器传递模式的程序,就是所有函数过程都必须有一个额外的参数,这个参数
代表一个随着计算过程推进而不断被更新的“寄存器”。考虑一个很小的伪随机数产生器:
 
    (define seed (current-time))
 
    (define (rand)
      (let ([ans (modulo (* seed 16807) 2147483647)])
        (begin (set! seed ans)
               ans)))
 
     在每次对RAND函数的调用中,seed种子变量都被改写为新产生的随机数的值。如果我们要用
纯Scheme实现上面的程序,我们就需要把seed种子变量作为一个额外的参数,在所有可能会更动
它的函数过程中进行连续传递。这样导致所有函数过程现在都必须返回一个点对:一个是函数的实
际求值结果,另一个是在函数计算过程中被更新传递的seed种子变量。
 
    ;; rand : number -> (number x number)
    (define (rand seed)
      (let ([ans (modulo (* seed 16807) 2147483647)])
        (cons ans ans)))
 
    ;; rand-point : number -> (point x number)
    (define (rand-point seed)
      (let* ([r1 (rand seed)]
             [r2 (rand (cdr r1))]
             [r3 (rand (cdr r2))])
        (cons (make-point (car r1) (car r2) (car r3))
              (cdr r3))))
 
    ;; rand-segment : number -> (segment x number)
    (define (rand-segment seed)
      (let* ([r1 (rand-point seed)]
             [r2 (rand-point (cdr r1))])
        (cons (make-segment (car r1) (car r2))
              (cdr r2))))
    ...
 
   整个程序将用一个初始种子变量启动计算,象这样:
 
    (run-my-program (current-time))
 
     上面每个函数过程都有一对共同的特征,即它们都有一个“seed”参数,并且返回一个点对,包
含函数结果和新产生的种子。下面让我们通过curry化seed参数把程序进一步进行抽象:
 
    ;; rand : -> (number -> (number x number))
    (define (rand)
      (lambda (seed)
        (let ([ans (modulo (* seed 16807) 2147483647)])
          (cons ans ans))))
 
    ;; rand-point : -> (number -> (point x number))
    (define (rand-point)
      (lambda (seed)
        (let* ([r1 ((rand) seed)]
               [r2 ((rand) (cdr r1))]
               [r3 ((rand) (cdr r2))])
          (cons (make-point (car r1) (car r2))
                (cdr r2)))))
 
    ;; rand-segment : -> (number -> (segment x number))
    (define (rand-segment)
      (lambda (seed)
        (let* ([r1 ((rand-point) seed)]
               [r2 ((rand-point) (cdr r1))])
          (cons (make-segment (car r1) (car r2))
                (cdr r2)))))
 
    那些没有引用或改变seed种子值的函数过程无须做此变更。我们把有副作用的函数叫做“操作”,而
把那些没有副作用的函数叫做“纯函数”。举例我们可以这样写一个求两点距离的函数:
 
    (define (distance pt1 pt2)
      (sqrt (+ (sqr (- (point-x pt1) (point-x pt2)))
               (sqr (- (point-y pt1) (point-y pt2)))
               (sqr (- (point-z pt1) (point-z pt2))))))
 
     这个函数由于没有改动seed种子,所以是纯函数性的。它并没有用一个额外参数来表示当前的种
子变量,它也没有返回一个点对。这意味着distance函数可以被任何可能具有副作用的函数调用,但
distance函数却不能反过来调用它们(因为这有可能造成副作用作废,导致无法再将累计的seed种子
传递给下一个操作)。
 
    我们可以设置一对操作来对seed种子值做提取和赋值:
 
    ;; get-seed : -> (number -> (number x number))
    (define (get-seed)
      (lambda (seed)
        (cons seed seed)))
 
    ;; set-seed : number -> (number -> (void x number))
    (define (set-seed new)
      (lambda (old)
        (cons (void) new)))
 
    我们也可以用类型来抽象这种统一的计算模式:我们把返回值类型为alpha的操作的类型设定为
T(alpha),就象这样:
    T(alpha) = number -> (alpha x number)
 
    我们前面几个函数都可以被赋予类型:
 
    get-seed     : -> T(number)
    set-seed     : number -> T(void)
    rand         : -> T(number)
    rand-point   : -> T(point)
    rand-segment : -> T(segment)
 
    然后我们可以试一下用上面这种办法去定义BEGIN,与延续传递方式相比显著的不同就是现在是
用累计器传递来规定计值顺序:
 
    ;; begin : T(alpha) T(beta) -> T(beta)
    (define (begin comp1 comp2)
      (lambda (seed0)
        (let* ([res1 (comp1 seed0)]
               [val1 (car res1)]
               [seed1 (cdr res1)])
          (comp2 seed1))))
 
    这个定义版本和延续传递版本都把BEGIN当作“操作组合子”:它具有两个操作作为参数,并返回
一个新的操作。但这对我们实现rand操作用处不大:
 
    (define (rand)
      (begin (get-seed)
             (let ([ans (modulo (* ??? 16807) 2147483647)])
               (begin (set-seed ans)
                      (lambda (seed)
                        (cons ans ans))))))
 
    象上面这样做,RAND函数中的第二个操作如何从前面的GET-SEED操作获得当前种子值呢?这个
问题是因为BEGIN废弃了前一个操作的结果值。让我们编写一个新的组合子,使得第一个操作的结果
能被后一个操作使用:
 
    ;; pipe : T(alpha) (alpha -> T(beta)) -> T(beta)
    (define (pipe comp1 build-comp2)
      (lambda (seed0)
        (let* ([res1 (comp1 seed0)]
               [val1 (car res1)]
               [seed1 (cdr res1)])
          ((build-comp2 val1) seed1))))
 
   这个新组合子获取一个操作参数,以及一个接受该操作结果值并以此构造第二个操作的函数作为另
一个参数。最后它执行被构造出来的第二个操作。
 
    (define (rand)
      (pipe (get-seed)
            (lambda (seed)
              (let ([ans (modulo (* seed 16807) 2147483647)])
                (begin (set-seed ans)
                       (lambda (seed)
                         (cons ans ans)))))))
 
    我们还可以抽象出一个单独的把值“提升”到计算类型的新操作:
 
    ;; lift : alpha -> T(alpha)
    (define (lift v)
      (lambda (seed)
        (cons v seed)))
 
    现在我们可以整理出最终版本的RAND函数:
 
    (define (rand)
      (pipe (get-seed)
            (lambda (seed)
              (let ([ans (modulo (* seed 16807) 2147483647)])
                (begin (set-seed ans)
                       (lift ans))))))
 
    每个monad都由一个类型构造器T和两个操作组成,T被用来给这两个操作赋予类型:
 
    lift : alpha -> T(alpha)
    pipe : T(alpha) (alpha -> T(beta)) -> T(beta)
 
    [这两个操作可以用其他名字命名:pipe操作有时叫bind,或者>>=,或*,还有let。而lift操作
经常被称为unit或return。
 
    还有,monad的两个操作必须满足以下三个法则:
    (pipe (lift x) f)   = (f x)
    (pipe m lift)       = m
    (pipe (pipe m f) g) = (pipe m (lambda (x) (pipe (f x) g)))
 
   [ 我累了,我不想证明我们的例子符合monad法则。实际上,这可能做不到,因为我们的pipe
版本有两个参数(而非完全curry化的)——噢,在这个问题上非终止性的证明会有麻烦。]
 
     还可以给monad定义其他操作,只要它们不违反上述法则。
 
      注意在我们的monad中,操作实际上是一个函数,它需要一个能获取初始值的种子。还要注意操
作执行后将产生一个点对结果,包含一个终值以及累计下来的seed种子。由于我们实际上只对终值感
兴趣,我们可以构造一个“run”过程来执行“monad化”操作,以得到最终值:
 
    ;; T(alpha) -> alpha
    (define (run m)
      (car (m (current-time))))
 
     注意这是唯一的从monad中退出的方式,例如从T(alpha)到alpha。“monad化”操作是基于组
合子产生出一个操作的链条,最后用一个顶层函数把结果值”run”出来。延续传递风格的程序也需要
这样一个顶层函数,用一个初始延续启动计值过程。
 
IV.总结
 
    以上我所介绍的monad有两个主要观念:
 
    1.对计值顺序做出强制规定
    2.把累计器抽象出来
 
     PIPE操作是一个从两个小的操作构造一个复合操作的组合子;它象延续传递方式一样要求这两个
小操作必须按顺序执行。(实际上,这说明延续传递方式是monad的一种特例。)这样我们就可以给
纯函数的核心语言加上各种副作用,并确保这些副作用以正确的顺序发生。monad对程序设计语言的
语义研究是有益的,它让我们可以用纯函数语义(例如lambda演算)来给可变状态和一阶延续等有用
的语言特性进行建模,然后我们可以按统一的数学抽象的方式对副作用的顺序执行进行论证推理。
      在Haskell这个惰性语言中,monad被用于做各种顺序操作,特别是输入/输出,monad让所有输
入/输出操作按正确的顺序执行。Haskell的设计者还把他们决定不放在核心语言中的各种副作用(如可
变状态和一阶延续等)都用monad来仿真实现。
 
V.更多
 
      以上我努力从一个程序员的视角对monad进行介绍。我没有介绍monad的数学背景,也没有介绍
代数法则的应用。原因是当你在研究低层代码时,很难把程序同monad这个数学语义对象联系起来。所
以我尽量坚持以一种诉诸直觉的方式介绍monad,以后再讨论它的精确定义。可学的东西还有不少啊。
分享到:
评论

相关推荐

    The Scheme Programming Language

    ChezScheme是一个高效、优化的Scheme系统实现,其特点包括了高效的解释执行、编译至机器码的能力、对尾调用优化的支持以及对SRFI标准库的广泛支持。由于ChezScheme的速度和功能,它经常被推荐给初学者和经验丰富的...

    Go-GoScheme-只是用Go编写的另一个Scheme解释器

    GoScheme是一个用Go语言实现的Scheme编程语言解释器。Scheme是一种基于Lisp家族的函数式编程语言,以其简洁的语法和强大的元编程能力而受到程序员的欢迎。Go语言则以其高效的性能、简单清晰的语法以及良好的并发支持...

    Teach Yourself Scheme in Fixnum Days

    1. Scheme编程语言概述:本书的标题即为一个知识点,即Scheme是一种功能强大的编程语言,适合在短时间内学习。Scheme是一种基于λ演算的Lisp方言,它拥有简洁的语法和强大的功能。 2. 数据类型:书中介绍了Scheme的...

    Android-scheme-libscheme-lib是一个scheme使用的库

    Scheme-Lib是一个专门为Scheme编程语言设计的库,特别针对Android平台进行了优化和适配。Scheme是一种历史悠久、功能强大的Lisp方言,以其简洁的语法和强大的函数式编程特性著称。在Android平台上使用Scheme-Lib,...

    Scheme 程序语言介绍之一

    Scheme 程序语言介绍之一

    chez scheme windows exe执行查询

    Chez Scheme 是一个功能强大的 Scheme 编程语言实现,由 C 家族的编程语言编写而成,提供高效且兼容 R6RS(第六版 Scheme 报告)的标准。它以其简洁的语法、丰富的库支持以及高度可移植性而受到程序员的喜爱。在 ...

    scheme实现唤醒外部app

    在移动应用开发中,"scheme"是一种常见的机制,用于实现应用程序间的交互,即从一个应用启动另一个应用。本文将深入探讨scheme如何实现唤醒外部APP,以及它在Webview和浏览器环境中的应用。 首先,理解scheme的基本...

    learn scheme

    ### Scheme语言介绍与计算机科学基础 #### 一、标题与描述概述 - **标题**:“Learn Scheme” - **描述**:“Lisp is a perfect language....希望这份文档能为你开启探索Scheme之旅提供一个良好的起点。

    Fluent Scheme中文手册修订.docx

    它提供了一个强大的编程平台,支持多种数据类型、函数式编程和面向对象编程等特性。 2. Fluent-Scheme 接口 Fluent Scheme 提供了一个强大的接口机制,允许用户在 Fluent 中调用 Scheme 命令,并在 Scheme 中调用 ...

    scheme and the art of programming

    ### Scheme与编程艺术 ...通过以上内容的介绍,《Scheme与编程艺术》不仅提供了一个学习Scheme语言的完整指南,还深入探讨了编程的一些核心理念和技术,适合所有希望深入了解功能性编程的程序员阅读。

    Scheme语言基础教程

    - **交互式评估器**:Scheme拥有一个交互式的评估环境,可以即时测试代码的效果,非常适合学习和调试。 - **教育和研究领域的应用**:自1975年以来,Scheme就被广泛应用于教育和研究领域,尤其是在计算机科学的教学...

    Lisp语言教程(Scheme)

    - **Guile**:Guile是GNU项目的组成部分之一,是一个基于Scheme语言的扩展语言库。它不仅可以作为一个独立的语言环境,还可以被集成到其他应用程序中作为脚本语言使用。Guile支持跨平台使用,可以在Linux和多种Unix...

    the little scheme (示例代码,windows运行环境, pdf文件 和 [The Seasoned Schemer pdf])

    压缩包中的资源提供了书中实例代码、一个Windows下的Scheme运行环境以及其姊妹篇《The Seasoned Schemer》的PDF电子版。 1. **Scheme编程语言**:Scheme是Lisp家族的一员,是一种简洁、高度表达性的函数式编程语言...

    scheme STk

    总之,Scheme STk是一个强大的工具,它不仅提供了Scheme编程的环境,还为开发者提供了学习和应用Scheme的便利。无论你是初次接触Scheme的新手,还是希望深入探索函数式编程的专家,STk都能为你提供一个高效且直观的...

    Scheme跳转的demo

    当一个应用希望监听并响应特定scheme的意图(Intent)时,需要在manifest中声明一个,并设置类别(action)为"android.intent.action.VIEW",数据类型(data)为自定义的scheme。例如: ```xml ...

    scheme_自学教程.pdf

    综上所述,《Scheme自学教程》旨在为有一定编程基础的初学者提供一个快速入门的平台,通过实践导向的教学方法,帮助读者快速掌握Scheme编程的核心技能。教程强调实用性和快速反馈,避免了繁冗的理论探讨,确保学习者...

Global site tag (gtag.js) - Google Analytics