Clojure
零基础
学习笔记
函数式编程
函数即是值。
终于,我们要介绍 Clojure 中最重要的部分了。
在此之前,你已经见到好多 Clojure 自带的函数了,比如打印函数 print
家族,给值取名的函数 def
,还有一些对集合操作的函数 first
assoc
…
你可能已经熟记了函数的几个特征:
- 函数都有返回值
- 函数可以接受参数,也可以不接受参数
- 如果函数改变了外部世界的一些状态(如修改了某个值,或者改变了屏幕上的显示效果),我们称这个函数存在副作用
小贴士:始终要记住,在 Clojure 中,函数总是能被求值,函数的值即为他的返回值。
所以当存在好多小括号进行嵌套的时候,我们可以分别求出内层函数的值,然后再一层层的计算出整体的值,从而得到程序的结果。
虽然 Clojure 内置了丰富的函数,但很多情况下,我们还是需要创造出自己的函数,或者组装拼接一些现有函数,使之更为强大。
定义函数
首先,让我们从创造一个简单的函数开始说起,
我们可以使用 fn
函数来创建一个属于我们自己的函数,
我们这个自定义函数的功能是,它接受一个数字作为参数,返回这个数字加上10之后的值。
=> (fn [x]
(+ x 10))
#<core$eval8059$fn__8060 my_clojure_study.core$eval8059$fn__8060@20302ac5>
看起来和我们之前接触过的 let
函数有点类似,没错,fn
函数隐式使用了 let
函数。也就是你可以在那个中括号里面使用之前我们学到的关于解构的知识!
-
fn
函数的第一个参数,用中括号包围,用来表示我们自定义函数的参数列表。
比如这里的x
。 - 而
fn
函数接下来的参数是一系列表达式,它们会被依次执行。最后一个被执行的表达式则作为自定义函数的返回值。 -
fn
函数本身的返回值,是我们的自定义函数本身。这里返回的是一串看似无意义的随机函数名,我们无视它。(还记得我们一开始所说的,“函数即是值么”,这意味着我们可以把函数作为返回值来返回!)
我们可以这样来使用我们创造出的函数:[1]
=> ((fn [x] (+ x 10)) 1)
11
是不是有点晕?
不要急,我们来仔细看一下。
首先,(fn [x] (+ x 10))
的值是一个函数,这个函数的功能就是我们自定义的 --- 它接受一个数字,返回这个数字加上10的结果。
由于我们的自定义函数放在了外层括号的第一个位置,所以它会当作函数来执行,而之后的数字1,则作为我们自定义函数的参数。
数字1传到了 x
的位置,所以此时 x
的值就成了 1。
执行自定函数内的代码 (+ x 10)
,由于 x
现在为1,此时我们的代码就等价于 (+ 1 10)
。
计算出结果11,返回。
还是不明白?不用担心,这次我们使用之前学过的 def
函数来给我们的“新朋友”取一个名字。
=> (def plus-ten (fn [some-num] (+ some-num 10)))
#'my-clojure-study.core/plus-ten
(我们把我们的自定义参数的参数名 x
换了一个新名字,这样并不会影响函数的功能,但是会增加少许可读性。)
现在我们的自定义函数就有了一个新名字 --- plus-ten
然后我们就可以这样来使用它了:
=> (plus-ten 24)
34
看起来工作的还不错。
由于我们经常需要定义诸如此类的有名字的自定义函数,
所以 Clojure 给我们内置了一个 def
和 fn
的合体函数! --- defn
很好记吧,它是这么来使用的:
=> (defn plus-ten
[some-num]
(+ some-num 10))
#'my-clojure-study.core/plus-ten
=> (plus-ten 45678)
45688
这里我们又使用了换行,来把参数列表和函数体分隔开,显得程序层次更加清晰。
如你所见:
-
defn
函数的第一个参数是我们给自定义函数取的名字。 - 第二个参数用中括号包围,里面是我们自定义函数的参数列表。
- 之后的代码是我们自定义函数的主体部分,并把最后一个作为自定义函数的返回值(这里只有一个,所以就把它作为返回值)。
使用起来非常方便。
解构参数列表
之前我们提到,fn
和 defn
都隐式使用了 let
,所以它们都支持解构。
下面我们来看一下具体的例子。
自定义函数所接受的参数可以不是单调的数字之类,我们可以直接接受一个列表。
假如我们想写一个自定义函数,它的功能是:
接受一个列表,把这个列表中第一个值和第三个值相加,并把结果返回。
例如,如果接受 (1 2 4)
或者 [1 2 4]
,它应该返回5。
我们可以这样来实现:
=> (defn plus-first-and-third
[[f _ t]]
(+ f t))
#'my-clojure-study.core/plus-first-and-third
=> (def some-collection [1 2 4])
#'my-clojure-study.core/some-collection
=> (plus-first-and-third some-collection)
5
这里和我们前面学的解构形式略有不同。defn
函数的解构首先直接对应了参数传入的位置,如此例中的第一参数,然后对这个位置再写一个中括号进行解构。
举个栗子。
如果传入了三个参数 [x y z]
,我们要解构 y
,
那么就可以写成 [x [first-y second-y] z]
。
这个例子中,plus-first-and-third
函数接受一个参数,
而这个参数又被解构成三个值 --- f
_
t
。
还有一种常见的解构形式是,解构剩余参数。
我们在绑定与解构一文的最后,介绍了使用 &
给剩余元素取名.
这使得我们可以创建不定长的,可变参数列表。
比如我们创建这样一个函数,
它接受大于三个参数,打印前两个参数的和的值,以及打印后续参数。返回 nil
。
我们可以这样实现它:
=> (defn print-plus-f-s
[f s & rest-num]
(println (+ f s))
(println rest-num))
#'my-clojure-study.core/print-plus-f-s
多于3个的参数会被作为一个 list
来绑定到 rest-num
。
现在看看它是如何工作的:
=> (print-plus-f-s 1 2 3 4 5)
3
(3 4 5)
nil
它打印了1和2的和,并且打印了后续数字的 list
形式。
由于最后一个表达式的值被作为自定义函数的值,而 println
函数的值始终为 nil
,所以我们的 print-plus-f-s
函数的返回值始终为 nil
。
实际上,刚才我们讨论的剩余参数一样可以继续被解构:
=> (defn print-plus-f-s
[f s & [third-num & rest-num]]
(println (+ f s))
(println third-num)
(println rest-num))
#'my-clojure-study.core/print-plus-f-s
=> (print-plus-f-s 1 2 3 4 5)
3
3
(4 5)
nil
解构是一个非常实用的技能,但是多层的解构对于初学者来说理解起来较为困难。
不过不要灰心,稍微复杂的例子留给大家仔细思考一下。
试着自己写出一些代码并运行是帮助学习的有效途径。
如果实在是找不到解答,可以在下方留言。
高阶函数
函数和数字、字符串、列表之类的数据一样,可以直接作为参数或者返回值来进行传递。
我们把接受函数为参数,或者把函数作为返回值的函数称之为高阶函数。
使用高阶函数,我们就能组装出更为强大和灵活的武器。
例如我们刚才学到的 fn
函数,就是一个高阶函数,因为它的返回值是我们的自定义函数。
我们同样可以创造出属于我们自己的高阶函数。
刚才我们创建了一个 plus-ten
的函数,它可以把一个数字加上10,并把这个结果返回过来。但是此时你的老板觉得我们还需要一个把一个数字减去10的函数。(虽然这个例子很幼稚,你大可以直接使用减法,但是我们只是以此来说明高阶函数的简单用法。)
你可以再创造一个函数:
=> (defn subtract-ten
[some-num]
(- some-num 10))
#'my-clojure-study.core/subtract-ten
但你可恶的老板(或者你的客户)又要求你写一个乘以10的函数,鬼知道他会不会再让你写更多的无聊函数。
如果有一个函数可以根据参数来生成不同的函数,那你就可以从无聊的重复代码上解脱了。
高阶函数就可以用来做这个:
=> (defn operate-ten
[operate]
(fn [x] (operate x 10)))
#'my-clojure-study.core/operate-ten
这是一个可以生成函数的函数,它接受一个函数作为参数,同时返回一个函数。
而它所返回的函数,则使用 operate-ten
所接受的那个参数来作为返回函数的执行部分的运算符。
说人话
好吧,我们直接看看它是怎么工作的:
=> (def subtract-ten (operate-ten -))
#'my-clojure-study.core/subtract-ten
=> (subtract-ten 9)
-1
- 首先,我们给这个能生成函数的函数传参数
-
,也就是减法函数。 - 然后,operate 这个值就变成了我们的减法函数
-
。
如果时间停止,展开operate-ten
这个高阶函数,那么它是这个样子的:
=> (defn operate-ten
[-]
(fn [x] (- x 10)))
没错!值就这样简单的被传进来的参数替换了而已!
- 时间继续流逝,现在它返回了
(fn [x] (- x 10))
。这正是我们想要的函数! - 然后我们使用
def
函数来命名这个由函数生成的函数。 - 使用这个函数,就如同之前一样。
哈哈,我们再也不用自己从头写一次代码了!
这个时候你的老板要求你写一个把一个数字乘以10的函数,你就可以这样来使用我们的高阶函数:
=> (def multiply-ten (operate-ten *))
#'my-clojure-study.core/multiply-ten
完事儿 =v=
仅此而已么?
当然不是!
我们甚至可以让这个高阶函数生成一个功能为:
“生成一个元素和10组成的有两个元素的列表”的函数。
=> (def list-with-ten-end (operate-ten list))
#'my-clojure-study.core/list-with-ten-end
=> (list-with-ten-end "我是第一个元素")
("我是第一个元素" 10)
或者:
“生成一个以10结尾的字符串”的函数。
=> (def string-with-ten-end (operate-ten str))
#'my-clojure-study.core/string-with-ten-end
=> (string-with-ten-end 123)
12310
=> (string-with-ten-end "head-")
head-10
小贴士:
str
函数可以把它的参数拼接成一个字符串,并以此作为它的返回值。
例子:
=> (str "hello" "world!" 10)
helloworld!10
仅仅因为,我们向 operate-ten
所传递的函数的不同!(这里是 list
和 str
)
此次我们学习了作为 Clojure 中“头等公民”的函数。
了解了自定义函数的几种声明方式,以及参数列表的常见解构形式。
最后我们窥见了函数式编程中高阶函数的概念,并体验了它所带来的灵活性。
最后的最后,我希望你能仔细理解本文中的代码,最好实际运行一下,并尝试修改它们。
-
这里你可能注意到,我们使用了
x
来表示我们的参数。在其他语言中,这种命名风格通常是不被建议的。但是在 Clojure 中,你可以使用类似x
y
a
b
之类的名称,来表示一个非常通用的类型,也就是表示这个函数支持各种类型的值。 ↩