目录
前言
本人是一个自学一年Java的小菜鸡,理论上跟大多数新手的水平差不多,但我入职的新公司是要求转Clojure语言的。坊间传闻:通常情况下,最好是有一定Java的开发工作经验,再转CLojure可能容易一些。我入职后的实际经历也确实让我感受到了Clojure的自学难度略大于自学Java,遇到的困难主要与中文资料较少有关,具体为:
1 中文的面向新手的较为系统的教程材料较少,目前个人感觉最好用的还是《CLojure编程 Emerick著》这本书,网上应该很好找,如果大家没有电子版的话可以留言,我看到后就立刻分享给大家
2 中文的网上相关问题和讨论较少, 以前学Java的时候基本遇到的问题用百度就能解决,现在大概率要直接用bing或谷歌,或者直接在*(虽然是英文的,但貌似是最好用的IT问答网站)上查
我的这个系列笔记主要是基于 0工作经验的后端开发转学Clojure 的场景下完成的,里面有一些个人观点和个人理解的注释,写的时候是为了便于自己理解相关的概念,现在分享出来一方面是希望能帮助像我一样的新手更好地理解,另一方面也是希望有高手能够发现错误并帮忙斧正,谢谢
一些格式的简单约定:
粗体:比较重要的内容
斜体:我个人理解或观点,不一定对
P15:表示书上第15页
第1章 进入Clojure仙境
1.1 基础概念
引用类型: Clojure有四种引用类型,分别是var ref agent atom,通过这些引用类型强制程序员将对象的状态和标识区分开
- 状态:根据时间会发生变化的一种属性,某一状态一旦产生就不会再变化
- 标识:就是一个指代,指的是一个在整个时间长河中的逻辑实体,换言之每个标识在不同时间都有一个状态
高阶函数: 变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数
函数式编程: 函数式编程的定义比较宽泛,但是可以从另一个角度理解函数式编程,即
- 操作的值是不可变的,数据结构是不可变的
- 声明式处理数据,而非命令式的控制和遍历
- 通过对函数进行递增式的组合,通过高阶函数和不可变数据结构,在更高的抽象级别来解决问题
这里说一下我个人对这三点的理解,欢迎拍砖纠偏
- 操作的值是不可变的,数据结构是不可变的。
关于值是不可变的,比如在Java中,Bob=new Person(),然后我们调用这个Bob去做其他事情,但是Bob的属性,比如年龄、身高有可能通过setter改变的,而在Clojure中,只要Bob定义好了,就是不能再变的了
关于数据结构不可变,我个人理解(这一块其实是有点模糊的)比如Clojure里面的数据结构就是括号里面包一堆东西,这个和同像性、代码即数据等有关 - 声明式处理数据,而非命令式的控制和遍历
这一部分建议看博文声明式编程和命令式编程的比较,关于命令式和声明式编程,讲的比较清楚 - 通过对函数进行递增式的组合,通过高阶函数和不可变数据结构,在更高的抽象级别来解决问题
我理解就是首先将一些功能合理的拆分封装在不同的函数中,然后根据情况巧妙地将它们组合使用,和Java里面经常使用的方法封装调用应该是一种思想,只不过这里希望能够被更好的执行
动态编程语言: 支持在运行时更新现有代码或加载新的代码
REPL: 交互式解释器,read读入、 eval求值、 print打印、 loop循环,可以输入命令并接收系统的响应。简单理解可以认为是一个随时可以跑代码的解释器。
补充一些理论知识: Clojure的REPL中输入代码会被编译成JVM字节码,跟直接执行一个Clojure源代码文件是一样的,因此“编译”是在运行时进行的,不需要单独的编译步骤,也就不存在解释执行。(编译就是先变成二进制文件,再运行,解释就是一行一行的转换然后运行;Java是一种半编译半解释语言,代码先转换成字节码,简单理解成编译,再被JVM执行,简单理解成解释,还涉及前端编译器、后端运行时编译器balabala在这里就不扩展了)
列表: 很重要! 用括号 ( ) 表示的内容,表示函数调用,第一个值表示操作符,剩下的值表示参数
函数位置: 列表中的第一个位置,该位置的符号只能求值成var或本地绑定的值、Clojure的特殊形式
符号:会被求值为当前作用域的值,有可能是函数、本地绑定、Java类、宏、特殊形式
本地绑定: 有点类似与方法中的形式参数
S表达式: Lisp里面的列表,符号表达式的简称,正确的且能够被成功求值的S表达式称为形式
形式:列表、符号表达式、S表达式可以理解为一个东西,就是能够被正确求值的 ( )
命名类型(标识符): 关键字和符号是两种不同的命名类型,当做标识符可能更容易理解一点,就是编程时用的名字,可以给变量、常量、函数、语法块命名
字面量: 比如数字、字符串,简单说就是一个“是什么就是什么的东西”
集合: 简单说就是在Clojure中,发挥着Java中能存一堆东西的那些数据结构(比如ArrayLIst、HashMap、TreeSet)作用的东西,具体有list vector set map四种实现
标量字面量:Clojure中只要不是集合类型值的语法形式就是标量字面量,就是字面量
- 字符串:双引号括起来的值
- 布尔值:true和false
- nil:就是null,逻辑上为false
- 字符:反斜杠加字符表示字符字面量,比如\c
- 关键字:一种标识符,经常被当做访问器来获取对应的值,比如通过map的键求值,键就是一个关键字;关键字本身作为函数时,作用就是在映射表查找对应的值,还有一个作用是轻量字符串,比如(name :ww),返回的就是“ww”,(= "name" (name :name)) 返回的就是true,关键字始终以冒号 : 开头,换句话说Clojure运行时看到冒号就会将这个字面量按照关键字的规则来运行,:/表示命名空间是限定的, : : 表示当前命名空间的关键字, : : / 表示某个特定命名空间的关键字;关键字是一种命名类型,因为有其自己的内在名字,以及一个可选的命名空间。 P15,要注意,Java中的关键字是指提前定义确认好的一些单词,只能有特定用法,比如static private等,和Clojure的关键字不是一个东西在P36的例子中可以看出,即便是向量(就是用[ ]包裹的内容)中,使用了关键字,是会简单被认为就是一个关键字,但是只要有键值对的形式,就可以用HashMap将其转化
- 符号:一种标识符,代表Clojure运行时里面的那个值,这个值可以是var所持有的值(函数或者其他值)、Java类、本地引用等,比如求平均值的average就是一个符号,代表名字是average的var所指向的函数,注意哈,名字是average的var所指向的函数,而不要简单的认为average是函数,是一种指向(或者说是代表)关系,而不是 is 的关系
- 数字:就是数字,以N结尾表示任意精度的整数,以M结尾表示任意精度的浮点数
- 注释:分号和#_ 两种,#_ 的好处在于,注释的边界是一个形式,而不是多少行
- 逗号、空格是一个东西,二者在Clojure中可相互替代,但在Clojure的字符串里面不是一个东西哈
- 集合字面量:四种常用,list:'( );vector:[ ];map { }:;set:#{ };P20;
list的括号前面有单引号,那么这个list是个存储值的集合,如果没有单引号,就会变成一个形式,关于形式的概念见前文 - 其他语法糖:P20
11.1 '(单引号):这个形式不被求值
11.2 #( ):定义匿名函数
11.3 #':得到var本身,var本身会被求值成var所代表的的值
11.4 @:得到引用所指向的值,这个值通常是
11.5 ` ~ ~@:宏的特殊语法
11.6 和Java互引用:最简单就是.代表了new,但是建议用. /的语法糖
同像性: “代码即数据”的理论前提,意思是程序的结构和句法(句子结构,句子成分如何组成)相似,因此能够通过阅读代码来推测内在含义,一般指语言的文本表示(源代码)与其抽象语法树有相同的结构,这一特性允许使用相同的(我理解就是Clojure的前缀表示法)表示语法,将语言中的所有代码当成数据,来存取和转换;在p200的解释是一门语言的代码可以用语言自身的数据结构来描述
代码即数据: 同像性的具体实现
宏: 对程序进行处理的程序;编写或操纵其他程序(或自身)作为数据,或者在运行时完成部分本应在编译时完成的工作的一种程序
元编程: 就是宏
同像性和递归的关系: 理解方式不同,递归其实是Java代码的一种理解方式,认为程序是程序,返回值是返回值,但是同像性认为只有一个东西,就是一坨内容,可以被理解为代码(因为有逻辑的表达),也可以理解为数据(因为最终运行的结果就是一个值)
Clojure的数据结构:将一个列表中的内容,转换为一个运算和一系列值,对列表的求值就是函数的定义
特殊形式(special form): 简单理解成基本函数,是Clojure组成计算的基本构建单元,其余部分都是基于这些特殊形式构建的,比如加和减在Java中就属于基本构建单元,add函数就是基于加和等号构建的;
-
quote:阻止表达式求值,最密切的就是指向var的符号,单引号 ' 会被解析成quote,比如列表一般会被求值成函数调用,但是用了qute后,这就是个列表,包含了几个元素的列表而已,不再是一个“数据”
-
do:代码块,依次求值传进来的所有表达式,并把最后一个表达式的结果作为返回值,fn、let、loop、try、defn及变种都隐式使用了do,所以可以在这些形式中使用多个表达式,并且最后一个表达式是返回值
-
def:在当前命名空间例定义(或重定义)一个var
-
let:本地绑定,(设置本地值、let绑定是一个意思,可以理解为在列表内部完成形参赋值,本地指的就是函数内部),绑定的向量称为定义绑定向量,绑定向量的形式[本地变量 传入变量]
4.1 有时候会在绑定向量中对一个表达式求值,这个时候用下划线 _ 表示绑定的名称,代表我们不关心其返回值,大多用于关心副作用的情况,比如要打印一句话;
4.1 let的绑定和其他函数的形参有两个不同:所有本地绑定是不可变的、编译期可以对通用集合类型进行解构,从而简化从绑定数组抽取数据的操作 -
let:解构,简单说就是自动从集合中取出数据并绑定到本地值上,顺序性数据结构和map是最关键、常用的数据结构,P29有读取向量内容的常用方法(first、second、last、nth),Clojure的解构特性就是let提供的,该特性提供了一种简洁的语法来声明式地从一个集合里选取某些元素,并把这些元素绑定到一个本地let上
5.1 顺序解构:可对任何顺序集合进行结构,包括Clojure原生list、vector、seq,任何实现java.util.List接口的集合,Java数组,字符串,解构可以理解为自动读取,但实际上结构是创建的反过程,解构被设计成能清晰反映被结构集合的结构,解构还支持嵌套
5.1.1 顺序结构的两个特性:保持“剩下的”元素(使用 & 符号)、保持被解构的值(使用 :as ,可以使用完成绑定之后还能使用最原始的集合进行操作)
5.2 map解构:对map解构有效,包括Clojure原生hash-map、array-map以及记录类型,任何实现了java.util.Map的对象,get方法支持的任何对象(Clojure原生vector、字符串、数组),如果解构的是vector、字符串或数组,则解构key则是数字类型的数组下标,具体见P33,这个在操作vector时会很方便,因为只用获取特定的某个值
5.2.1 map解构的特性:
5.2.1.1 保持被结构的集合(使用 :as )
5.2.1.2 默认值(使用 :or 提供默认map,如果要解构的key在集合中没有,才会使用默认集合,这样就不用在map解构之前将源map和默认map合并,关于or和:or 的对比,可以理解:or 多了一个判空的功能)
5.2.1.3 绑定符号到map中同名关键字所对应的元素,利用:keys :strs :syms来指定map中key的类型,并完成绑定,更多的会使用:key,因为我们习惯使用关键字作为map中的key
5.2.1.4 对顺序集合的“剩余”部分使用map解构 -
fn:定义函数,正常的形式就是(fn [x] (+ x 10)),但是也可以指定函数名字,以便在函数中调用自己(fn 可选函数名 [x] (+ x 10)),比如定义多参数列表的函数就需要用到这个方法,具体见P37,如果定义了多个列表,则按顺序依次执行,比如(defn ww [username] (println "12") (println "ee")),先打印12,再打印ee
6.1 letfn解决函数定义互相引用的问题,允许同时定义多个具名函数
6.2 defn是封装def和fn的宏,使我们可以简洁的定义一个具名函数(就是正常带名字的函数)
6.3 解构函数参数
6.3.1 可变函数参数:函数可以把剩余参数收集到一个列表中,这跟顺序解构的机制相同,而“剩余参数”列表也可以进行解构,具体见P39
6.3.2 关键字参数:给定必选的固定参数与可选参数列表相结合的一种使用方式,形式上是参数向量[] 后面又跟了一个{}map,然后这个map会被 &{} 包裹的内容解构,而且可选参数的传入顺序是无所谓的,可选参数传入时也是按照键值对的形式传入的
6.3.3 前置条件和后置条件:fn提供对函数参数和函数返回值进行检查的前置和后置条件,这对确保参数正确性和单元测试非常有用
6.3.4 函数字面量:在定义匿名函数,特别是非常简单的函数时,函数字面量提供了非常简洁的语法来做此事;特点有:函数字面量没有隐式地使用do、使用非命名的占位符号来指定函数的参数个数、函数字面量不能嵌套使用,P64有关于匿名函数和函数字面量使用的一点小建议 -
if:条件判断,一些其他函数和宏是建立在if基础上的,比图when、cond、if-let和when-let
8)true?和false?:没有复杂的逻辑,单纯判断所给参数是不是true或false
-
loop和recur:循环,具体见《Clojure程序设计》的笔记,简单说就是for循环的不同使用方法
-
var:引用var,命名一个var的符号求值成var所对应的值
-
. 和new:所有和Java的互操作,都是通过.和new实现的,不过更建议用P45的语法糖
-
try和throw:使得我们可以在Clojure代码中使用Java的异常处理和抛出机制
-
set!:改变对象状态
-
locking、monitor-enter和monitor-exit:monitor-enter和monitor-exit是底层原语,不推荐使用,应该用Clojure的宏locking
1.2 常用的一些符号
以下符号不仅限于出现在第一章,而是将我在前7章的阅读过程中遇到的,个人认为需要记住的符号都记录在此,便于查阅
- ->:这是个方法,支持链式执行,将前一个表达式的结果作为下一个函数的第一个参数来调用
- slurp:吃,读文件
- spit:吐,写文件,slurp和spit的使用见教程
- deref:解引用,一般使用语法糖@,通常只有在高阶函数或者解引用时指定一个超时、超时特性的时候采用deref
- delay:延时执行、缓存返回值,之后无副作用
- dorun:彻底实例化惰性序列,P167
- defonce:对变量只会赋值一次,再次赋值会失败并返回nil
- defn-:定义私有函数
- defprotocol:Clojure的接口称为协议,Interface在Clojure中都是特指Java接口
- io!宏:当被用在事务中的时候,就会抛出异常,因此可以把有副作用的函数用io!宏包起来,防止被误用在事务里面
- dotimes:赋值的时候给一个绑定,然后会从0开始执行直到绑定值-1
- write skew : 对ref进行解引用,但如果当前事务提交之前,ref被修改了,会导致当前事务重试
- ensure来避免write skew : 对ref进行解引用,但如果当前事务提交之前,ref被修改了,会导致当前事务重试
- *name*:一般期望通过binding来对根绑定进行覆盖的动态var,会在命名的时候以星号开头,以星号结尾
- alter-var-root : 对var的根绑定进行修改
- list:将传入参数生成一个列表,但是注意要让汗珠方法名加 ' 阻止求值
- ' : 引述,一个单引号,返回参数的不求值形式
- ` (就是键盘左上键+shift敲出来的符号): 语法引述,使用的是一个反引号
- ~ : 反引述,小波浪线,就是把引述内部的某元素求值
- ‘@ : 编接反引述,把另一个列表的内容解开加入到第一个列表里面去
- gensym:在宏里面建立本地绑定的时候,动态产生一个永远不会跟外部代码或者用户传入宏的代码冲突的名字,每次调用都是产生唯一的符号
- ~' : 强制使用没有命名空间限定的符号作为绑定的名字,P246
- # : 自动gensym,以#结尾的符号会被自动扩展,对于前缀相同的符号,也会被扩展成同一个符号,P247
- '~ : 先引述,再反引述,P250
- list*:简化创建指定多个值来创建seq的需求,P91
- defprotocol宏:定义一个协议
- repeat: 重复某个东西一定次数
- ->,把前面一个形式(简单说就是前面的结果,可能是个值也可能是个函数)插入到后面一个形式的第二个元素位置,对于清理多级函数调用以及多级Java方法调用的代码非常有用
- .. : 只支持Java方法调用的串行,还支持Java静态方法
- ->> : 把前面一个form插入到后面一个form的最后一个元素位置上,这个宏经常被用来对一个序列或者其他数据结构进行转换
- get-in:从嵌套的map中取数据
- #"":是一个正则表达式形式,利用re-pattern可以将字符串转换为正则表达式,但是也可以自己这么写
-
‘ 比较
44) *1:表示REPL最近print的值