Clojure

学习 Clojure - 函数

创建函数

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 视为 deffn 的缩写可能很有用。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 %&)

陷阱

一个常见的需求是接受一个元素并将其包装在向量中的匿名函数。您可能会尝试将其编写为

;; DO NOT DO THIS
#([%])

此匿名函数扩展为等效的

(fn [x] ([x]))

此表单将包装在向量中 **并且** 尝试在没有参数的情况下调用向量(额外的括号对)。反而

;; Instead do this:
#(vector %)

;; or this:
(fn [x] [x])

;; or most simply just the vector function itself:
vector

应用函数

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 表达式为 xy 创建两个局部绑定。表达式 (+ 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 创建一个新作用域来定义 abc。如果我们尝试在 let 表达式之后使用 a,编译器将报告错误。

闭包

fn 特殊表单创建“闭包”。它“封闭”周围的词法作用域(如上面的 msgabc)并捕获其超出词法作用域的值。

(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!

Java 交互

调用 Java 代码

以下是从 Clojure 调用 Java 的调用约定的摘要

任务 Java Clojure

实例化

new Widget("foo")

(Widget. "foo")

实例方法

rnd.nextInt()

(.nextInt rnd)

实例字段

object.field

(.-field object)

静态方法

Math.sqrt(25)

(Math/sqrt 25)

静态字段

Math.PI

Math/PI

Java 方法 vs 函数

  • Java 方法不是 Clojure 函数

  • 无法存储它们或将它们作为参数传递

  • 必要时可以将它们包装在函数中

;; make a function to invoke .length on arg
(fn [obj] (.length obj))

;; same thing
#(.length %)

测试你的知识

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/powMath/cosMath/sinMath/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 的函数,该函数接受两个函数作为参数,fg。它返回另一个函数,该函数接受一个参数,对其调用 g,然后对结果调用 f,并返回该结果。

也就是说,你的函数返回 fg 的组合。

(defn two-fns [f g]
  ___)