背景简述

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

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

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

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

一些格式的简单约定:

粗体:比较重要的内容

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

P15:表示书上第15页

第5章 宏

5.0 术语

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

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

5.1 宏到底是什么

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

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

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

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

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

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

5.1.1 宏不是什么

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

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

代码生成机制
需要编译器进行特殊步骤来编译 宏的编译和普通代码编译过程一样
对象生成系统依赖的是一些专门的对象模型 宏使用的就是普通的数据结构
代码通常不具有可组合性 可以调用另外的宏

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

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

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

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

5.1.3 宏vsRuby的eval

暂时没看

5.2 编写你的第一个宏

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

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

5.3 调试宏

因为宏是在编译期执行的,所以如果宏里面用了一个没有定义的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)))))))))

5.4 语法

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

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

5.5 什么时候使用宏

主要有两点

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

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

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

5.6 宏卫生

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

5.6.1 Gensym来拯救

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

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

5.6.2 让宏的用户来选择名字

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

5.6.3 重复求值

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

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

5.7 宏的常见用法和模式

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

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

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

5.8 隐藏参数:&env和&form

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

5.8.1 &env

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

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

5.8.2 &form

没看

5.8.3 测试上下文相关的宏

没看

5.9 深入->和->>

串行宏:->和->>

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

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

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

5.10 总结

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

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

《Clojure编程》笔记 第5章 宏的更多相关文章

  1. C#高级编程笔记之第二章:核心C#

    变量的初始化和作用域 C#的预定义数据类型 流控制 枚举 名称空间 预处理命令 C#编程的推荐规则和约定 变量的初始化和作用域 初始化 C#有两个方法可以一确保变量在使用前进行了初始化: 变量是字段, ...

  2. 标C编程笔记day04 预处理、宏定义、条件编译、makefile、结构体使用

    预处理:也就是包括须要的头文件,用#include<标准头文件>或#include "自己定义的头文件" 宏定义,如:#define PI 3.1415926 查看用宏 ...

  3. C#高级编程笔记之第一章:.NET体系结构

    1.1 C#与.NET的关系 C#不能孤立地使用,必须与.NET Framework一起使用一起考虑. (1)C#的体系结构和方法论反映了.NET基础方法论. (2)多数情况下,C#的特定语言功能取决 ...

  4. Python核心编程笔记 第三章

    3.1     语句和语法    3.1.1   注释( # )   3.1.2   继续( \ )         一般使用换行分隔,也就是说一行一个语句.一行过长的语句可以使用反斜杠( \ ) 分 ...

  5. 《Clojure编程》笔记 第4章 多线程和并发

    目录 背景简述 第4章 多线程和并发 4.0 我的问题 4.1 术语 4.1.1 一个必须要先确定的思考基础 4.2 计算在时间和空间内的转换 4.2.1 delay 4.2.2 future 4.2 ...

  6. 《Clojure编程》笔记 第13章 测试

    目录 背景简述 第13章 测试 13.1 术语 13.2 clojure.test 13.2.1 定义测试的两种方式 13.2.1.1 用deftest宏把测试定义成单独的函数 13.2.1.2 用w ...

  7. 《Clojure编程》笔记 第3章 集合类与数据结构

    目录 背景简述 第3章 集合类与数据结构 3.1 抽象优于实现 3.1.1 Collection 3.1.2 Sequence 3.1.3 Associative 3.1.4 Indexed 3.1. ...

  8. 《Clojure编程》笔记 第1章 进入Clojure仙境

    目录 背景简述 第1章 进入Clojure仙境 1.1 基础概念 1.2 常用的一些符号 背景简述 本人是一个自学一年Java的小菜鸡,理论上跟大多数新手的水平差不多,但我入职的新公司是要求转Cloj ...

  9. 《Clojure编程》笔记 第16章 Clojure与web

    目录 背景简述 第16章 Clojure与web 16.1 术语 16.2 Clojure栈 16.3 基石:Ring 16.3.1 请求与应答 16.3.2 适配函数 16.3.3 处理函数 16. ...

随机推荐

  1. C# lock 死锁问题排查方法

    多线程程序发生死锁,某些重要线程卡住,不正常工作.排查起来非常麻烦.以下内容记录排查方法 1.确定死锁的位置,一般死锁会lock到某一行具体的代码,比如我就死锁在类似如下代码中 public void ...

  2. Centos-当前登录用户信息- w who

    w who 显示当前登录系统的用户,但w显示的更为详细 who 相关参数 # 默认输出 用户名.登录终端.登录时间 -a 列出所有信息 -b    系统最近启动日期 -m   当前终端信息,相当于 w ...

  3. spring-boot-route(二)读取配置文件的几种方式

    Spring Boot提供了两种格式的配置文件,分别是properties 和 yml.Spring Boot最大的特点就是自动化配置,如果我们想修改自动化配置的默认值,就可以通过配置文件来指定自己服 ...

  4. SQL实战——02. 查找入职员工时间排名倒数第三的员工所有信息

    查找入职员工时间排名倒数第三的员工所有信息CREATE TABLE `employees` (`emp_no` int(11) NOT NULL,`birth_date` date NOT NULL, ...

  5. Allegro PCB 转 PADS Layout 之后的修修补补

    操作系统:Windows 10 x64 工具:PADS Layout VX.2.3 参考:Allegro转PADS以及后续修改 我们可以看到转换后的PCB文件,乱糟糟的,所以还需要我们手动修改一下. ...

  6. pycharm 解决PEP8问题,配置autopep8到菜单栏

    autopep8是一个可以将Python代码自动排版为PEP8风格第三方包,使用它可以轻松地排版出格式优美整齐的代码.网络上有很多介绍如何在pycharm中配置autopep8的方案,但很多方案中还是 ...

  7. PropertySheet外壳扩展AppWizard

    下载source files - 39 Kb 下载Wizard - 17 Kb 本文旨在简化属性表外壳扩展的实现.它紧接我的第一篇文章 处理上下文菜单壳扩展和灵感 由Michael Dunn最优秀的系 ...

  8. CentOS7 没有安装 ifconfig 命令

    ifconfig 命令是设置或显示网络接口的程序,可以显示出我们机器的网卡信息. 除此之外, ip a 命令,也可以设置或显示网卡的信息 在 CentOS 7 下,默认 ifconfig 命令是没有安 ...

  9. spring redis 配置

  10. 多测师讲解python_模块间的调用_高级讲师肖sir

    案例1: 在aaa.py  文件A类中定义一个函数sadp: 在bbb.py文件中导入aaa模块,导入类 ,调用函数 案例2: aaa模块中定义一个A类, 在定义一个sadp的函数, 在bbb模块中导 ...