;; name params body
;; ----- ------ -------------------
(defn greet [name] (str "Hello, " name) )
Clojure 是一种函数式语言。函数是第一类公民,可以传递给其他函数或从其他函数返回。大多数 Clojure 代码主要由纯函数(没有副作用)组成,因此使用相同的输入调用它会产生相同的输出。
defn
定义一个命名函数
;; name params body
;; ----- ------ -------------------
(defn greet [name] (str "Hello, " name) )
此函数有一个名为 name
的参数,但是您可以在 params 向量中包含任意数量的参数。
使用函数名称在“函数位置”(列表的第一个元素)调用函数
user=> (greet "students")
"Hello, students"
可以定义函数以接受不同数量的参数(不同的“参数个数”)。不同的参数个数必须全部在同一个 defn
中定义 - 使用 defn
多次将替换先前的函数。
每个参数个数都是一个列表 ([param*] body*)
。一个参数个数可以调用另一个参数个数。主体可以包含任意数量的表达式,返回值是最后一个表达式的结果。
(defn messenger
([] (messenger "Hello world!"))
([msg] (println msg)))
此函数声明了两个参数个数(0 个参数和 1 个参数)。0 参数参数个数使用默认值调用 1 参数参数个数以进行打印。我们通过传递适当数量的参数来调用这些函数
user=> (messenger)
Hello world!
nil
user=> (messenger "Hello class!")
Hello class!
nil
函数还可以定义可变数量的参数 - 这被称为“可变参数”函数。可变参数必须出现在参数列表的末尾。它们将被收集到一个序列中供函数使用。
可变参数的开头用 &
标记。
(defn hello [greeting & who]
(println greeting who))
此函数接受一个参数 greeting
和可变数量的参数(0 个或更多),这些参数将被收集到一个名为 who
的列表中。我们可以通过使用 3 个参数调用它来看到这一点
user=> (hello "Hello" "world" "class")
Hello (world class)
您可以看到,当 println
打印 who
时,它被打印为一个包含两个元素的列表,这些元素已被收集。
可以使用 fn
创建匿名函数
;; params body
;; --------- -----------------
(fn [message] (println message) )
由于匿名函数没有名称,因此以后无法引用它。相反,匿名函数通常在其传递给另一个函数时创建。
或者可以立即调用它(这不是常见的用法)
;; operation (function) argument
;; -------------------------------- --------------
( (fn [message] (println message)) "Hello world!" )
;; Hello world!
在这里,我们在一个更大表达式的函数位置定义了匿名函数,该表达式立即使用参数调用该表达式。
许多语言都具有语句,这些语句以命令式方式执行某些操作并且不返回值,以及表达式,这些表达式执行操作并返回值。Clojure **仅**具有返回值的表达式。我们稍后将看到,这甚至包括像 if
这样的流程控制表达式。
defn
vs fn
将 defn
视为 def
和 fn
的缩写可能很有用。fn
定义函数,def
将其绑定到名称。它们是等效的
(defn greet [name] (str "Hello, " name))
(def greet (fn [name] (str "Hello, " name)))
在 Clojure 读取器中实现了一种更短的 fn
匿名函数语法形式:#()
。此语法省略了参数列表并根据参数的位置为参数命名。
%
用于单个参数
%1
、%2
、%3
等用于多个参数
%&
用于任何剩余的(可变)参数
嵌套匿名函数会产生歧义,因为参数没有命名,因此不允许嵌套。
;; Equivalent to: (fn [x] (+ 6 x))
#(+ 6 %)
;; Equivalent to: (fn [x y] (+ x y))
#(+ %1 %2)
;; Equivalent to: (fn [x y & zs] (println x y zs))
#(println %1 %2 %&)
apply
apply
函数使用 0 个或多个固定参数调用函数,并从最终序列中提取其余所需的参数。最终参数**必须**是序列。
(apply f '(1 2 3 4)) ;; same as (f 1 2 3 4)
(apply f 1 '(2 3 4)) ;; same as (f 1 2 3 4)
(apply f 1 2 '(3 4)) ;; same as (f 1 2 3 4)
(apply f 1 2 3 '(4)) ;; same as (f 1 2 3 4)
所有这 4 个调用都等效于 (f 1 2 3 4)
。当参数作为序列传递给您但您必须使用序列中的值调用函数时,apply
很有用。
例如,您可以使用 apply
来避免编写此内容
(defn plot [shape coords] ;; coords is [x y]
(plotxy shape (first coords) (second coords)))
相反,您可以简单地编写
(defn plot [shape coords]
(apply plotxy shape coords))
let
let
在“词法作用域”中将符号绑定到值。词法作用域为名称创建新的上下文,嵌套在周围的上下文中。在 let
中定义的名称优先于外部上下文中的名称。
;; bindings name is defined here
;; ------------ ----------------------
(let [name value] (code that uses name))
每个 let
可以定义 0 个或多个绑定,并且可以在主体中包含 0 个或多个表达式。
(let [x 1
y 2]
(+ x y))
此 let
表达式为 x
和 y
创建两个局部绑定。表达式 (+ x y)
位于 let
的词法作用域中,并将 x 解析为 1,将 y 解析为 2。在 let
表达式之外,x 和 y 将不再具有意义,除非它们已绑定到某个值。
(defn messenger [msg]
(let [a 7
b 5
c (clojure.string/capitalize msg)]
(println a b c)
) ;; end of let scope
) ;; end of function
messenger 函数接受一个 msg
参数。这里 defn
也为 msg
创建了词法作用域 - 它仅在 messenger
函数内才有意义。
在该函数作用域内,let
创建一个新作用域来定义 a
、b
和 c
。如果我们尝试在 let 表达式之后使用 a
,编译器将报告错误。
fn
特殊表单创建“闭包”。它“封闭”周围的词法作用域(如上面的 msg
、a
、b
或 c
)并捕获其超出词法作用域的值。
(defn messenger-builder [greeting]
(fn [who] (println greeting who))) ; closes over greeting
;; greeting provided here, then goes out of scope
(def hello-er (messenger-builder "Hello"))
;; greeting value still available because hello-er is a closure
(hello-er "world!")
;; Hello world!
1) 定义一个名为 greet
的函数,该函数不带任何参数并打印“Hello”。用实现替换 ___
:(defn greet [] _)
2) 使用 def
重新定义 greet
,首先使用 fn
特殊表单,然后使用 #()
读取器宏。
;; using fn
(def greet __)
;; using #()
(def greet __)
3) 定义一个名为 greeting
的函数,该函数
如果未提供参数,则返回“Hello, World!”
如果提供一个参数 x,则返回“Hello, x!”
如果提供两个参数 x 和 y,则返回“x, y!”
;; Hint use the str function to concatenate strings
(doc str)
(defn greeting ___)
;; For testing
(assert (= "Hello, World!" (greeting)))
(assert (= "Hello, Clojure!" (greeting "Clojure")))
(assert (= "Good morning, Clojure!" (greeting "Good morning" "Clojure")))
4) 定义一个名为 do-nothing
的函数,该函数接受一个参数 x
并原样返回它。
(defn do-nothing [x] ___)
在 Clojure 中,这是 identity
函数。就其本身而言,identity 没有什么用,但在使用高阶函数时有时是必要的。
(source identity)
5) 定义一个名为 always-thing
的函数,该函数接受任意数量的参数,忽略所有参数,并返回数字 100
。
(defn always-thing [__] ___)
6) 定义一个名为 make-thingy
的函数,该函数接受一个参数 x
。它应该返回另一个函数,该函数接受任意数量的参数并始终返回 x。
(defn make-thingy [x] ___)
;; Tests
(let [n (rand-int Integer/MAX_VALUE)
f (make-thingy n)]
(assert (= n (f)))
(assert (= n (f 123)))
(assert (= n (apply f 123 (range)))))
在 Clojure 中,这是 constantly
函数。
(source constantly)
7) 定义一个名为 triplicate
的函数,该函数接受另一个函数并调用它三次,不带任何参数。
(defn triplicate [f] ___)
8) 定义一个名为 opposite
的函数,该函数接受一个参数 f
。它应该返回另一个函数,该函数接受任意数量的参数,对它们应用 f
,然后对结果调用 not
。Clojure 中的 not
函数执行逻辑否定。
(defn opposite [f]
(fn [& args] ___))
在 Clojure 中,这是 complement 函数。
(defn complement
"Takes a fn f and returns a fn that takes the same arguments as f,
has the same effects, if any, and returns the opposite truth value."
[f]
(fn
([] (not (f)))
([x] (not (f x)))
([x y] (not (f x y)))
([x y & zs] (not (apply f x y zs)))))
9) 定义一个名为 triplicate2
的函数,该函数接受另一个函数和任意数量的参数,然后对这些参数调用该函数三次。重新使用你在前面 triplicate 练习中定义的函数。
(defn triplicate2 [f & args]
(triplicate ___))
10) 使用 java.lang.Math 类(Math/pow
、Math/cos
、Math/sin
、Math/PI
),演示以下数学事实
pi 的余弦是 -1
对于某些 x,sin(x)^2 + cos(x)^2 = 1
11) 定义一个函数,该函数接受 HTTP URL 作为字符串,从 Web 获取该 URL,并将内容作为字符串返回。
提示:使用 java.net.URL 类及其 openStream
方法。然后使用 Clojure slurp
函数将内容作为字符串获取。
(defn http-get [url]
___)
(assert (.contains (http-get "https://www.w3.org") "html"))
实际上,Clojure slurp
函数首先将其参数解释为 URL,然后再尝试作为文件名。编写一个简化的 http-get
(defn http-get [url]
___)
12) 定义一个名为 one-less-arg
的函数,该函数接受两个参数
f
,一个函数
x
,一个值
并返回另一个函数,该函数对 x
加上任何其他参数调用 f
。
(defn one-less-arg [f x]
(fn [& args] ___))
在 Clojure 中,partial
函数是更通用的版本。
13) 定义一个名为 two-fns
的函数,该函数接受两个函数作为参数,f
和 g
。它返回另一个函数,该函数接受一个参数,对其调用 g
,然后对结果调用 f
,并返回该结果。
也就是说,你的函数返回 f
和 g
的组合。
(defn two-fns [f g]
___)