背景简述

本人是一个自学一年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. python3的基础数据类型

    看了很多文档,想自己整理一下关于python的数据类型.说干就干,下面接上. 首先,了解 常量与变量. 常量是什么?常量是指在整个程序操作过程中其值保持不变的数据: 变量是什么?变量即在程序运行过程中 ...

  2. React学习小记--setState的同步与异步

    react中,state不能直接修改,而是需要使用setState()来对state进行修改,那什么时候是同步而什么时候是异步呢? 基础代码: setCounter = (v) => { thi ...

  3. Book of Shaders 03 - 学习随机与噪声生成算法

    0x00 随机 我们不能预测天空中乌云的样子,因为它的纹理总是具有不可预测性.这种不可预测性叫做随机 (random). 在计算机图形学中,我们通常使用随机来模拟自然界中的噪声.如何获得一个随机值呢, ...

  4. 07 Sublime Text3常用快捷键

    通用常用类(General) ↑↓←→:上下左右移动光标,注意不是不是 KJHL ! Alt:调出菜单 Ctrl + Shift + P:调出命令板(Command Palette) Ctrl + ` ...

  5. Python3基础——函数

    ython 函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段. 函数能提高应用的模块性,和代码的重复利用率.你已经知道Python提供了许多内建函数,比如print().但你也可 ...

  6. C++中cstring.h和string.h的区别

    转载:https://blog.csdn.net/qian_chun_qiang/article/details/80648691 1.string与cstring有什么区别 <string&g ...

  7. P3660 [USACO17FEB]Why Did the Cow Cross the Road III G

    Link 题意: 给定长度为 \(2N\) 的序列,\(1~N\) 各处现过 \(2\) 次,i第一次出现位置记为\(ai\),第二次记为\(bi\),求满足\(ai<aj<bi<b ...

  8. CentOS7.7 系统下 virbr0 虚拟网卡的维护与管理

    在 CentOS 7 系统的安装过程中,如果有选择相关虚拟化的的服务安装系统后,启动网卡时会发现有一个以网桥连接的私网地址的 virbr0 网卡,这个是因为在虚拟化中有使用到 libvirtd 服务生 ...

  9. C# 生成chart图表的三种方式

    .net中,微软给我们提供了画图类(system.drawing.imaging),在该类中画图的基本功能都有.比如:直线.折线.矩形.多边形.椭圆形.扇形.曲线等等,因此一般的图形都可以直接通过代码 ...

  10. linux的安装3.7python

    centos安装python3 首先安装依赖包 yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-dev ...