背景简述

本人是一个自学一年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. Spring学习(八)--Spring的AOP

    自工作以后身不由己,加班无数,996.995不可控制,高高立起的flag无法完成,无奈,随波逐流,尽力而已! 1.advice通知 advice主要描述Spring AOP 围绕奥方法调用而注入的切面 ...

  2. 使用gettid() 注意事项

    gettid()这个函数不可以在程序中直接使用,它是Linux本身的一个函数, 但是:仅包含#include <sys/types.h>,然后使用,编译时会报该函数未定义之类的错误! 解决 ...

  3. SQL错题集

    查找最晚入职员工的所有信息 select * from employees where hire_date = (select max(hire_date) from employees) 查找入职员 ...

  4. weblogic 安装+部署(一)

    昨天刚接触weblogic,在windows下搭建了一个weblogic,没什么技术,留个笔记. 1.首先要有jdk,添加环境变量这个没什么好说的. 2.下载weblogic:可以去官网下:http: ...

  5. C/C++的二分查找

    假设有一种温度传感器,已经测得它的电压和温度的对应关系,将电压值以ADC转换后的数字量的值表示,形成温度-AD值的对照表,如下. 大致成一条反比关系的曲线. ADC的底层驱动已经写好,对外有一个接口可 ...

  6. LiteOS-任务篇-源码分析-系统启动函数

    目录 前言 链接 参考 开启调度 LOS_Start 函数源码 osTickStart 函数源码 LOS_StartToRun 函数源码 前言 20201009 移植好内核后,开始实战内核. 源码分析 ...

  7. Thinkphp中D方法和M方法的区别

    两者共同点都是实例化模型的,而两者不同点呢?一起来看一下: $User = D('User');括号中的参数User,对应的模型类文件的 \Home\Model\UserModel.class.php ...

  8. NOI 2011 【阿狸的打字机】

    之前讲了[AC自动姬],今天我终于把这题给刚下来了...嗯,来给大家讲一讲. 题目描述: 打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P'两个字母.经阿狸研究发现,这个打字机是这样工 ...

  9. 关于【s】和[t]字符

    [s]:当一个具有执行权限的文件设置 [s](SetUID) 权限后,用户执行这个文件时将以文件所有者的身份执行.passwd 命令具有 SetUID 权限,所有者为 root(Linux 中的命令默 ...

  10. MeteoInfoLab脚本示例:获取气团轨迹每个节点的气象数据

    读取HYSPLIT输出的轨迹数据文件和相应时间的气象数据文件,生成轨迹图层,循环每条轨迹的节点,读出该节点的经度.纬度.气压.时间,通过对气象数据插值获得该节点的气象数据.脚本程序: #------- ...