《Clojure编程》笔记 第5章 宏
背景简述
本人是一个自学一年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 引述和语法引述
' : 引述,一个单引号,返回参数的不求值形式
` : 语法引述,使用的是一个反引号(就是键盘左上键)
引述和反引述的两个不同点:
- 语法引述将无命名空间限定的符号求值为当前命名空间的符号
- 语法引述允许反引述,某些元素可以选择性的被反引述,从而使得它们在语法引述的形式内被求值
;;; 第一个区别的代码
;;; 符号的默认空间化对于正确的代码很关键,可以防止疏忽而重定义一个已经定义过的值
(def foo 123)
=> #'helloworldclojure.core/foo
[foo (quote foo) 'foo `foo]
=> [123 foo foo helloworldclojure.core/foo]
5.4.2 反引述与编接反引述
~ : 反引述,小波浪线,就是把引述内部的某元素求值
~' : 强制使用没有命名空间限定的符号作为绑定的名字,P246
‘@ : 编接反引述,把另一个列表的内容解开加入到第一个列表里面去
5.5 什么时候使用宏
主要有两点
第一点:宏在某些上下文(编译期)中很方便、很强大,但在运行期会使代码很难写,可以考虑把主要逻辑从宏里面抽到函数里面,从而使宏只是简单地做一些组织工作,真正的逻辑都通过调用函数来做
第二点:只在需要自己的语言组件时才使用宏,换言之就是函数无法满足需要的时候再使用宏,使用场景有:
- 需要特殊的求值语义
- 需要自定义的语法——特别是领域特定的表示法
- 需要在编译期提前计算一些中间值
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编程》笔记 第5章 宏的更多相关文章
- C#高级编程笔记之第二章:核心C#
变量的初始化和作用域 C#的预定义数据类型 流控制 枚举 名称空间 预处理命令 C#编程的推荐规则和约定 变量的初始化和作用域 初始化 C#有两个方法可以一确保变量在使用前进行了初始化: 变量是字段, ...
- 标C编程笔记day04 预处理、宏定义、条件编译、makefile、结构体使用
预处理:也就是包括须要的头文件,用#include<标准头文件>或#include "自己定义的头文件" 宏定义,如:#define PI 3.1415926 查看用宏 ...
- C#高级编程笔记之第一章:.NET体系结构
1.1 C#与.NET的关系 C#不能孤立地使用,必须与.NET Framework一起使用一起考虑. (1)C#的体系结构和方法论反映了.NET基础方法论. (2)多数情况下,C#的特定语言功能取决 ...
- Python核心编程笔记 第三章
3.1 语句和语法 3.1.1 注释( # ) 3.1.2 继续( \ ) 一般使用换行分隔,也就是说一行一个语句.一行过长的语句可以使用反斜杠( \ ) 分 ...
- 《Clojure编程》笔记 第4章 多线程和并发
目录 背景简述 第4章 多线程和并发 4.0 我的问题 4.1 术语 4.1.1 一个必须要先确定的思考基础 4.2 计算在时间和空间内的转换 4.2.1 delay 4.2.2 future 4.2 ...
- 《Clojure编程》笔记 第13章 测试
目录 背景简述 第13章 测试 13.1 术语 13.2 clojure.test 13.2.1 定义测试的两种方式 13.2.1.1 用deftest宏把测试定义成单独的函数 13.2.1.2 用w ...
- 《Clojure编程》笔记 第3章 集合类与数据结构
目录 背景简述 第3章 集合类与数据结构 3.1 抽象优于实现 3.1.1 Collection 3.1.2 Sequence 3.1.3 Associative 3.1.4 Indexed 3.1. ...
- 《Clojure编程》笔记 第1章 进入Clojure仙境
目录 背景简述 第1章 进入Clojure仙境 1.1 基础概念 1.2 常用的一些符号 背景简述 本人是一个自学一年Java的小菜鸡,理论上跟大多数新手的水平差不多,但我入职的新公司是要求转Cloj ...
- 《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. ...
随机推荐
- python_购物车
流程图 实现方式 #!/usr/bin/python3 __author__ = 'beimenchuixue' __blog__ = 'http://www.cnblogs.com/2bjiuji ...
- Python练习题 012:字符统计
[Python练习题 012] 输入一行字符,分别统计出其中英文字母.空格.数字和其它字符的个数. ----------------------------------------------- 这题 ...
- JDK1.8新特性之(一)--Lambda表达式
近期由于新冠疫情的原因,不能出去游玩,只能在家呆着.于是闲来无事,开始阅读JDK1.8的源代码.在开始之前也查询了以下JDK1.8的新特性,有针对性的开始了这段旅程. 只看不操作,也是不能心领神会的. ...
- Linux就该这么学28期——开篇
2020.10.03 正式开始系统学习Linux之旅,希望能在老刘的带领下,掌握操作要领. 现将所学记录在此. 学习环境如下: VmwareWorkStation 15 --虚拟机软件 versio ...
- Python库之SQLAlchemy
一.SQLAlchemy简介 1.1.SQLAlchemy是什么? sqlalchemy是一个python语言实现的的针对关系型数据库的orm库.可用于连接大多数常见的数据库,比如Postges.My ...
- linux块设备驱动---程序设计(转)
块设备驱动注册与注销 块设备驱动中的第1个工作通常是注册它们自己到内核,完成这个任务的函数是 register_blkdev(),其原型为:int register_blkdev(unsigned i ...
- .Net Core中使用Grpc
一.Grpc概述 gRPC 基于如下思想:定义一个服务, 指定其可以被远程调用的方法及其参数和返回类型.gRPC 默认使用protocol buffers作为接口定义语言,来描述服务接口和有效载荷消息 ...
- day26 Pyhton 复习re模块和序列化模块
# re # 正则表达式 # 元字符 # 量词 # 贪婪匹配与惰性匹配 # 元字符量词 # 元字符量词? 在量词规范内,遇到一个x就停下来 # .*?x (.如果是第一个元素,那么它一定会从第一个元素 ...
- 分布式系统中的CAP、ACID、BASE概念
目录 CAP ACID BASE CAP 分布式系统中,这三个特性只能满足其中两个. 一致性(Consistency):分布式中一致性又分强一致性和弱一致性,强一致性主浊任何时刻任何节点看到的数据都是 ...
- centos8平台php7.4.2安装phpredis实现对redis的访问
一,下载phpredis 1,官方下载地址: https://github.com/phpredis/phpredis/releases 2,wget下载 [root@yjweb source]# w ...