《Clojure编程》笔记 第5章 宏
阅读原文时间:2023年07月10日阅读:2

目录

背景简述

本人是一个自学一年Java的小菜鸡,理论上跟大多数新手的水平差不多,但我入职的新公司是要求转Clojure语言的。坊间传闻:通常情况下,最好是有一定Java的开发工作经验,再转CLojure可能容易一些。我入职后的实际经历也确实让我感受到了Clojure的自学难度略大于自学Java,遇到的困难主要与中文资料较少有关,具体为:

1 中文的面向新手的较为系统的教程材料较少,目前个人感觉最好用的还是《CLojure编程 Emerick著》这本书,网上应该很好找,如果大家没有电子版的话可以留言,我看到后就立刻分享给大家

2 中文的网上相关问题和讨论较少, 以前学Java的时候基本遇到的问题用百度就能解决,现在大概率要直接用bing或谷歌,或者直接在stackoverflow(虽然是英文的,但貌似是最好用的IT问答网站)上查

我的这个系列笔记主要是基于 0工作经验的后端开发转学Clojure 的场景下完成的,里面有一些个人观点和个人理解的注释,写的时候是为了便于自己理解相关的概念,现在分享出来一方面是希望能帮助像我一样的新手更好地理解,另一方面也是希望有高手能够发现错误并帮忙斧正,谢谢

一些格式的简单约定:

粗体:比较重要的内容

斜体:我个人理解/观点或是补充内容,大家选择性食用

P15:表示书上第15页

第5章 宏

clojure被称为“可被编程的语言”,很大一部分原因是因为宏,宏是一种可以以其他语言里面很难,甚至不可能的方式来对语言进行扩展的机制

假设一个场景:一门语言中没有“循环”(比如Java里面没有for),那么用这门语言写代码就会用很多重复的内容,而正是Java将“循环”进行抽象,形成了for,所以Java的循环才能用,而宏就是在语言层面构建一个“抽象”(比如循环),因此宏是消灭模板文件,将语言打磨的符合需要的终极武器

宏可以让我们控制Clojure编译器,在其作用域内,可以被用来对语言的语法进行微调或者彻底改变语言的语法,宏可以让开发人员制造各种武器,而这些武器和语言内置的武器没有任何区别

同像性(代码即数据,代码可以用语言自身的数据结构来描述)是宏的基础

宏的实现细节上其实也是函数,只是因为有了一些特别的元数据表明这是一个宏

宏和函数的区别主要发生在编译期:

函数调用会被直接转换成字节码,在运行时传给函数的参数会被求值为对应的值,传给函数

宏会被编译期调用,调用的参数是吧传入的数据结构不做求值直接传给宏,然后宏再返回一个数据结构,而这个数据结构本身必须是要能求值的,求值出来的数据结构会代替宏原来的位置

5.1.1 宏不是什么

代码生成机制概念:代码生成通常是以一个高级别的表示(比如一个正式的语法或者一个对象模型的描述作为输入),产生一段实现这个对象模型的一段代码

代码生成机制和宏的区别:

代码生成机制

需要编译器进行特殊步骤来编译

宏的编译和普通代码编译过程一样

对象生成系统依赖的是一些专门的对象模型

宏使用的就是普通的数据结构

代码通常不具有可组合性

可以调用另外的宏

5.1.2 有什么是宏能做而函数不能做的

比如Java的改进型for循环一直无法完成,就是因为Java缺乏表达力,当然可以写成一个方法调用

添加改进型for需要在Java的编译器层面进行一些修改,但是只能在运行期被调用,而且它们也访问不了编译器,因此单靠函数没有办法将一段没有求值的代码(println调用)插入一个循环结构,简单说就是CLojure程序员可以给语言添加新的语言结构

CLojure的内置操作符只有16个,就是特殊形式,剩下的常用功能比如defn等都是通过宏完成的

5.1.3 宏vsRuby的eval

暂时没看

宏以Clojure数据结构的形式接受Clojure代码作为参数

postwalk函数可以递归地遍历一个嵌套的列表,并且对于列表里面的某个元素做一些处理

因为宏是在编译期执行的,所以如果宏里面用了一个没有定义的var,宏是不会报错的,因此需要一些工具来帮助调试宏

5.3.1 宏扩展

macroexpand-1:以一个数据结构(通常就是被引号引住的宏形式)作为参数,比如:

(macroexpand-1 '(reverse-it
                  (qesod [gra (egnar 5)]
                         (nltnirp (cni gra)))))

=> (doseq [arg (range 5)] (println (inc arg)))

macroexpand:扩展一个宏直到最顶级的形式不再是一个宏

(macroexpand '(reverse-it
                  (qesod [gra (egnar 5)]
                         (nltnirp (cni gra)))))

=> (loop*
 [seq_1607 (clojure.core/seq (range 5)) chunk_1608 nil count_1609 0 i_1610 0]
 (if
  (clojure.core/< i_1610 count_1609)
  (clojure.core/let
   [arg (.nth chunk_1608 i_1610)]
   (do (println (inc arg)))
   (recur seq_1607 chunk_1608 count_1609 (clojure.core/unchecked-inc i_1610)))
  (clojure.core/when-let
   [seq_1607 (clojure.core/seq seq_1607)]
   (if
    (clojure.core/chunked-seq? seq_1607)
    (clojure.core/let
     [c__5983__auto__ (clojure.core/chunk-first seq_1607)]
     (recur
      (clojure.core/chunk-rest seq_1607)
      c__5983__auto__
      (clojure.core/int (clojure.core/count c__5983__auto__))
      (clojure.core/int 0)))
    (clojure.core/let
     [arg (clojure.core/first seq_1607)]
     (do (println (inc arg)))
     (recur (clojure.core/next seq_1607) nil 0 0))))))

macroexpand-all:将宏彻底扩展

(walk/macroexpand-all '(reverse-it
                  (qesod [gra (egnar 5)]
                         (nltnirp (cni gra)))))

=> (loop*
 [seq_1618 (clojure.core/seq (range 5)) chunk_1619 nil count_1620 0 i_1621 0]
 (if
  (clojure.core/< i_1621 count_1620)
  (let*
   [arg (. chunk_1619 nth i_1621)]
   (do (println (inc arg)))
   (recur seq_1618 chunk_1619 count_1620 (clojure.core/unchecked-inc i_1621)))
  (let*
   [temp__5720__auto__ (clojure.core/seq seq_1618)]
   (if
    temp__5720__auto__
    (do
     (let*
      [seq_1618 temp__5720__auto__]
      (if
       (clojure.core/chunked-seq? seq_1618)
       (let*
        [c__5983__auto__ (clojure.core/chunk-first seq_1618)]
        (recur
         (clojure.core/chunk-rest seq_1618)
         c__5983__auto__
         (clojure.core/int (clojure.core/count c__5983__auto__))
         (clojure.core/int 0)))
       (let* [arg (clojure.core/first seq_1618)] (do (println (inc arg))) (recur (clojure.core/next seq_1618) nil 0 0)))))))))

list:将传入参数生成一个列表,但是注意要让函数方法名加 ' 阻止求值

5.4.1 引述和语法引述

' : 引述,一个单引号,返回参数的不求值形式

` : 语法引述,使用的是一个反引号(就是键盘左上键)

引述和反引述的两个不同点:

  1. 语法引述将无命名空间限定的符号求值为当前命名空间的符号

  2. 语法引述允许反引述,某些元素可以选择性的被反引述,从而使得它们在语法引述的形式内被求值

    ;;; 第一个区别的代码
    ;;; 符号的默认空间化对于正确的代码很关键,可以防止疏忽而重定义一个已经定义过的值
    (def foo 123)
    => #'helloworldclojure.core/foo
    [foo (quote foo) 'foo `foo]
    => [123 foo foo helloworldclojure.core/foo]

5.4.2 反引述与编接反引述

~ : 反引述,小波浪线,就是把引述内部的某元素求值

~' : 强制使用没有命名空间限定的符号作为绑定的名字,P246

‘@ : 编接反引述,把另一个列表的内容解开加入到第一个列表里面去

主要有两点

第一点:宏在某些上下文(编译期)中很方便、很强大,但在运行期会使代码很难写,可以考虑把主要逻辑从宏里面抽到函数里面,从而使宏只是简单地做一些组织工作,真正的逻辑都通过调用函数来做

第二点:只在需要自己的语言组件时才使用宏,换言之就是函数无法满足需要的时候再使用宏,使用场景有:

  1. 需要特殊的求值语义
  2. 需要自定义的语法——特别是领域特定的表示法
  3. 需要在编译期提前计算一些中间值

因为外部是有可能对也有宏里面的同名参数函数的,那样就会报错

5.6.1 Gensym来拯救

gensym:在宏里面建立本地绑定的时候,动态产生一个永远不会跟外部代码或者用户传入宏的代码冲突的名字,每次调用都是产生唯一的符号

# : 自动gensym,以#结尾的符号会被自动扩展,对于前缀相同的符号,也会被扩展成同一个符号,P247

5.6.2 让宏的用户来选择名字

将传入的符号作为宏的参数

5.6.3 重复求值

重复求值发生在传给宏的参数在宏的扩展形式里面多次出现的情况下

'~ : 先引述,在反引述,P250

如果宏需要制定本地绑定,那么把绑定指定在一个vector里面

定义var的时候不要耍小聪明

不要在宏里面实现复杂行为

defmacro宏本身是Clojure不稳定的宏,defmacro引入了两个隐藏的本地绑定

5.8.1 &env

&env是一个map,map的key是当前上下文下所有本地绑定的名字(而对应的值是未绑定的)

另一个用途就是在编译期安全地对表达式进行优化

5.8.2 &form

没看

5.8.3 测试上下文相关的宏

没看

串行宏:->和->>

->,把前面一个form插入到后面一个form的第二个元素位置,对于清理多级函数调用以及多级Java方法调用的代码非常有用

.. : 只支持Java方法调用的串行,还支持Java静态方法

->> : 把前面一个form插入到后面一个form的最后一个元素位置上,这个宏经常被用来对一个序列或者其他数据结构进行转换

宏是Clojure的终极表达力的体现,但是宏不应该是写代码的首选,宏是我们的最终武器

上一篇:《Clojure编程》笔记 第4章 多线程和并发

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章