Coursera - Programming Language - 课程笔记 - Week 6

Week 6

Racket 基础 Racket Basis

  • 定义一个变量(define x 3)

  • 定义一个基于函数调用的变量(define y (+ x 3)),这里面+是一个函数

  • 定义一个函数(define cube1 (lambda (x) (* x (* x x))))或者(define cube2 (lambda (x) (* x x x)))

  • 使用语法糖定义函数(define (cube3 x) (* x x x))

  • if语句(if e1 e2 e3)e1为条件语句,e2为真分支,e3假分支

  • 多参数函数实际上就是柯里化的函数的语法糖,即下二者等价

    • (define functionName (lambda (x) (lambda (y) (...))))
    • (define (functionName x y) (...))
  • 同样可以定义部分应用

  • 但是有一个问题是,如果使用上面的前者,在调用参数时需写成((functionName x) y)

  • 在Racket中,任何括号都是有意义的

    • 在大多数情况下,(e)是对e的零参数调用
  • 函数调用(cube1 3)

  • racket中的列表操作

    • 空列表null,在racket中,'()表示空列表
    • 列表创建(元素和列表构建)cons
    • 列表的头元素car
    • 列表的尾部cdr
    • 检查列表是否为空null?
    • 构建一个长列表(list e1 ... en)
  • racket的语法内容

    • 原子项(Atom),#t, #f, 3, “hi”, null, 5.0, x, ...
    • 一些特殊范项(Special Form),原子项的子集define, lambda, if
    • 括号内的一组语法项的序列,(t1 t2 ... tn)
      • 如果第一个语法项是一个特殊范项,那么这个序列是特殊的
      • 否则就是一个函数调用
    • 任何使用方括号的地方都可以用圆括号代替,但一定要注意对应
  • 采用括号的形式是很好的,因为使用括号可以将任何程序文本转换成一个树表示,这一过程是平凡且准确的

    • 原子项是叶子
    • 序列则是以元素为子节点的节点

动态类型指定 Dynamic Typing

  • 优——可以不受到类型检查器的限制以构建非常灵活的数据结构
  • 劣——很难在真正测试前找到一些小错误,以及在获取内容类型方面比较麻烦

Cond表达式 Cond Expressions

  • 避免嵌套的if表达式,应使用cond表达式

  • 通用语法(cond [e1a e1b] [e2a e2b] ... [eNa eNb]),每一个表达式对中,前一个为条件,后一个为该条件为真时的操作

  • 为了实现一个好的格式,最后一个条件需要一定为真#t,这样可以保证cond表达式中一定会有一个分支的语句执行,避免所有语句为假时的奇怪情况

  • 对于if表达式和cond表达式,条件表达式可以评估为任何事物

    • 只要不是#f的内容都被视为真

局部绑定 Local Bindings

  • 和ML类似,使用let指定函数中的局部绑定
  • 基本格式:(let (some local bindings) body)
  • 使用中括号分隔开不同的变量:(let ([x1 e1][x2 e2]...[xn en]) body)
  • Racket有四种定义局部绑定的方式
    • let
    • let*
    • letrec
    • define
  • 上述四种定义方式具有不同的语法语义,使用最方便的一种方式(用的最广泛的是let
  • let可以绑定任意数量个局部变量(注意括号的分布)
    • 需要注意,let表达式中的表达式要在表达式之前的环境中进行评估
  • let*,其语法规则与上述表达式规则基本无异,唯一区别在于这个是符合ML风格的绑定,即使用当前表达式之前的所有内容进行评估
  • letrec使用同样的语法规则,rec是对“recursion”的缩写,其中的表达式用于所有的绑定进行评估,逼近包含之前的所有内容,还包括之后的所有内容(有些类似and用于ML中的相互递归的情形)
    • 同样可以在局部绑定中用于相互调用的内容
    • 但是表达式仍然按照顺序进行评估,如果出现了违例将会出错
    • 通常这种“乱序的引用”出现在函数体的定义中,因为在定义时,使用其后内容的函数体将会在let的内容评估后在let的body中被使用并评估,这个时候,函数体后定义的内容已经在函数的闭包中,因此不会出错
    • 一个规避性的方法:只在函数绑定中使用晚期绑定(later bindings)
  • define同样可以用于函数定义,其语义规则和letrec相同(在大多数racket用户中受欢迎)

顶级绑定 Top-level Bindings

  • 当由大量绑定在文件中时,其工作方式就像letrec一样
    • 类似于ML,可以引用其之前的绑定
    • 又不像ML,可以引用其之后的绑定,但只能发生在函数体中(因为其调用即评估的时候是在需要的绑定之后,这是符合顺序的)
    • 不像ML,不能在一个模块中定义相同的变量两次

使用set!进行异变 Mutation with set!

  • Racket是具有赋值语句的,但是只在真的合适的地方用
    • set! x e
    • 上述语法中的x是已经存在于环境中的变量,e会评估产生一个结果,并更新绑定x
    • 后续对于x的查找将会获得新的值,在这之前的查找则是原值(和赋值语句的组用类似)
  • Racket中有一个序列操作符begin,语法为(begin e1 e2 ... en),所有的表达式按顺序执行,其结果为最后一个表达式的值(相当于中间的序列是没有用的)。如果中间的某些表达式有额外影响(使用set!的异变或者输出),那么这些影响将会被按顺序执行
  • 对于顶级绑定,异变存在一些有趣的事实
    • 函数的闭包由函数定义是决定,但是函数体只在函数调用时被评估,因此函数中使用绑定将会受到set!的影响
    • 然而表达式产生了一个值之后,其值将不再与其评估时参与的值相关,因此表达式中的绑定不受到set!影响
  • 为了避免一些不应当被改变的内容被不慎改变,应当将其在函数内部进行一次本地拷贝
  • 一个通用规范:模块外不允许对模块内的内容进行异变
  • 一个基本假设:不对顶级绑定进行异变

有关cons的事实 Truth About Cons

  • 可以使用cons构造一个对,任何使用cons构造的结果都是对
  • 实际上在很多的动态类型语言中,列表是很多个嵌套的对,其最后一个元素是一个空列表null
  • 对应地,car实际上就是访问一个对的第一项,而cdr则是访问第二项
  • caddr xs定义为car(cdr(cdr(xs)))
  • 如果末尾是(cons sth oth),那么最终结果将不是一个列表,但仍是一个对
  • 相对地,前者被称为恰当列表(proper list),后者称为不恰当列表(improper list)
  • 对相对更加有用,而且在一个动态类型语言中,尝试区分一个对的构建和一个列表的拼接并没有什么意义(并不知道后一个元素是一个项目还是一堆项目)
  • 不下当列表的项目之间用点隔开
  • list?对恰当列表(包括空列表)返回#tpair?对任何使用cons构造的结果返回#t

用于可异变对的mcons mcons for Mutable Pairs

  • set!并不会改变cons中的内容
  • 为了解决可能需要改变对中的某个元素,提供了mcons进行构建
  • 其首尾元素使用mcarmcdr进行访问
  • 这样,我们可以对这个构造结果使用set-mcar!set-mcdr!对特定的对中的元素进行修改
  • 使用mpair?判断数据结构有效性
  • 当我们需要对连接操作的结果的某一项进行异变时,使用mcons一般情况下我们则使用cons

延迟评估和形式转换 Delay Evaluation and Thunks

  • 不同的语义内容制定了其子表达式在何时被评估
    • 函数参数是饥饿式(调用函数前,值就被评估)
    • 条件分支不是饥饿式的
  • 延迟评估一个表达式:将其放到一个函数里
  • 用一个零参数函数实现延迟评估,称之为形式转换(Thunk)
  • 对表达式e,对其形式转换(lambda() e),就能够通过调用这个函数(e)实现延迟评估(不直接评估,只在调用这个函数时评估内容),其调用结果为其内容结果

避免不必要的计算 Avoiding Unnecessary Computations

  • 形式转换让我们能够跳过一些并不需要的昂贵计算
  • 假设一些昂贵计算没有额外影响,那么我们要:
    • 直到需要时才计算之
    • 计算一次就记住这个值,未来使用之则立即完成(懒惰评估)
  • Racket已经有了promises用于实现懒惰评估,可以使用形式转换以及可异变对实现之
  • 形式转换体有一个问题:虽然我们解决了“调用时评估”的问题,但是多次调用时,意味着多次计算
  • 一个思路:提前算好这个值一次(传入时评估),存起来,但是仍然产生了问题:如果一直不使用这个值,那么最开始的计算毫无意义
  • 延迟函数,用一个对保存计算状态以及需要的计算的形式转换体:(define (my-delay th) (mcons #f th))
  • 强制函数,用于在必要时执行计算,或在已计算完成后直接取值:(define (my-force p) (if (mcar p) (mcdr p) (begin (set-mcar! p #t) (set-mcdr! p ((mcdr p))) (mcdr p))))
  • 另外的一个思路:使用延迟函数和强制函数进行进一步封装(连传入函数时都不进行评估),只在需要时进行调用,并存值(这就是promises

流 Streams

  • 一个流是一个无限长的值的序列
    • 不可被手动创建
    • 一个思路:使用形式转换体以创建序列的绝大部分
  • 一种很好的分工
    • 流的产生者知道每一个值的产生过程,但不知道要产生多少个
    • 流的接收者不知道具体产生过程,但是决定了具体接收多少个值
  • 使用对和形式转换体表示流
  • 一个流是一个形式转换体,调用之会返回一个对,其头为这个流的第一个元素,尾为表示这个流的第2个到无穷的流的形式转换体,即‘(next-answer . next-thunk)
  • 对流的定义,实际上是一种递归函数的巧妙应用

记忆化 Memoization

  • 如果一个函数没有任何额外影响,且不读入任何可异变内容,那么对同一组参数指定两次没有任何意义
    • 将之前的计算结果保存在缓存中
    • 当下面的条件满足时,这一思路将是对的
      • 维护内存比计算要廉价
      • 缓存的结果确实被重复用到了
  • 和上面的promises类似,这里的记忆化需要接受参数,也就意味着有多个可能的结果
  • 对于递归函数,使用记忆化方法可以带来指数级的速度提升

宏 Macros

  • 一个宏定义描述了如何将一些新语法转换为源语言中的其他语法
  • 实际上,宏可以是一种实现语法糖的方式
  • 宏系统是一个语言用于定义宏的内容
  • 宏扩展是每一次宏使用时重写语法的过程,其发生在程序被运行之前(甚至是编译之前)
  • 定义一个宏m,那么使用之(m ...)将会根据定义被扩展
  • 宏系统工作在属性字(Token)级别而非字符级别
  • 在Racket中使用宏要比其他语言更少担心于括号的使用
    • 宏的使用:(macro-name ...)
    • 扩展之后:(some else ...)
  • 对于局部绑定与宏的名称重复,不会出现冲突,Racket会准确地找到需要扩展的内容
  • 宏定义的语法:(define-syntex macro-name (syntex-rules (defined keywords) [(macro-definition one) (expansion one)] [(macro definition two) (expansion two)] ...))
  • 延迟宏:宏可以将一个表达式放到形式转换体里面,而不必显式地将其写入形式转换体,这是我们无法用函数实现的(但要注意不是所有的延迟评估都可以用宏实现,因为直接替换内容可能导致错误的多次执行)
  • Racket的宏系统可以很“干净”(Hygiene)地解决很多由于单纯替换导致的问题
    • 悄悄地将一些宏中的局部变量重命名成一些奇怪的名字以避免冲突
    • 在宏定义处查找相关的变量(类似一个词法作用域)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值