跳转到内容

Clojure 编程/概念

来自 Wikibooks,开放世界的开放书籍
数字类型
[编辑 | 编辑源代码]

Clojure 支持以下数字类型

  • 整数
  • 浮点数
  • 比率
  • 十进制

Clojure 中的数字 基于 java.lang.Number。BigInteger 和 BigDecimal 受支持,因此我们在 Clojure 中具有任意精度的数字。

Ratio 类型在 Clojure 页面上描述

比率
表示整数之间的比率。不能约化为整数的整数的除法会产生一个比率,即 22/7 = 22/7,而不是一个浮点数或截断值。

比率允许计算以数字形式保持。这有助于避免长时间计算中的不准确性。

以下是一个小实验。我们首先尝试计算 (1/3 * 3/1) 作为浮点数。之后,我们尝试使用 Ratio 进行相同的计算。

 (def a (/ 1.0 3.0))
 (def b (/ 3.0 1.0))

 (* a b)
 ;; ⇒ 1.0

 (def c (* a a a a a a a a a a)) ; ⇒ #'user/c
 (def d (* b b b b b b b b b b)) ; ⇒ #'user/d

 (* c d)
 ;; ⇒ 0.9999999999999996

我们想要的结果是 1,但上面 (* c d) 的值为 0.9999999999999996。这是由于在创建 c 和 d 时 a 和 b 相乘造成的不准确性。你真的不希望你的工资单中出现这样的计算 :)

使用比率执行相同的计算:

 (def a1 (/ 1 3))
 (def b1 (/ 3 1))

 (def c (* a1 a1 a1 a1 a1 a1 a1 a1 a1 a1))
 (def d (* b1 b1 b1 b1 b1 b1 b1 b1 b1 b1))

 (* c d)
 ;; ⇒ 1

结果如我们所愿为 1

数字输入格式
[编辑 | 编辑源代码]

Clojure 支持下面显示的常用输入格式

 user=> 10       ; decimal
 10
 user=> 010      ; octal
 8
 user=> 0xff     ; hex
 255
 user=> 1.0e-2   ; double
 0.01
 user=> 1.0e2    ; double
 100.0

为了简化操作,还支持以 <radix>r<number> 形式的基数输入格式。其中基数可以是 2 到 36 之间的任何自然数。

 2r1111
 ;; ⇒ 15

这些格式可以混合使用。

 (+ 0x1 2r1 01)
 ;; ⇒ 3

Clojure API 也支持许多位运算。

 (bit-and 2r1100 2r0100)
 ;; ⇒ 4

其他一些是

  • (bit-and x y)
  • (bit-and-not x y)
  • (bit-clear x n)
  • (bit-flip x n)
  • (bit-not x)
  • (bit-or x y)
  • (bit-set x n)
  • (bit-shift-left x n)
  • (bit-shift-right x n)
  • (bit-test x n)
  • (bit-xor x y)

查看 Clojure API 以获取完整文档。

将整数转换为字符串
[编辑 | 编辑源代码]

将任何数据格式化为打印的一种通用方法是使用 java.util.Formatter

预定义的便捷函数 format 使使用 Formatter 变得简单(%#x 中的哈希将数字显示为以 0x 为前缀的十六进制数)

 
 (format "%#x" (bit-and 2r1100 2r0100))
 ;; ⇒ "0x4"

使用 java.lang.Integer 将整数转换为字符串更加容易。请注意,由于方法是静态的,我们必须使用“/”语法而不是“。method”

 
 (Integer/toBinaryString 10)
 ;; ⇒ "1010"
 (Integer/toHexString 10)
 ;; ⇒ "a"
 (Integer/toOctalString 10)
 ;;⇒ "12"

以下是指定字符串表示的基数的另一种方法

 
 (Integer/toString 10 2)
 ;; ⇒"1010"

其中 10 是要转换的数字,2 是基数。

注意:除了上面用于访问静态字段或方法的语法之外,还可以使用“.”(点)。这是一种特殊形式,用于访问 Java 中的任意(非私有)字段或方法,如 Clojure 参考 (Java 交互操作) 中所述。例如

 
 (. Integer toBinaryString 10)
 ;; ⇒ "1010"

对于静态访问,建议使用“/”语法。

将字符串转换为整数
[编辑 | 编辑源代码]

要将字符串转换为整数,我们还可以使用 java.lang.Integer。如下所示。

   user=> (Integer/parseInt "A" 16)      ; hex
   10
   user=> (Integer/parseInt "1010" 2)    ; bin
   10
   user=> (Integer/parseInt "10" 8)      ; oct
   8
   user=> (Integer/parseInt "8")         ; dec
   8

以上各节概述了整数到字符串和字符串到整数的格式化。Java 库中有一组非常丰富的文档齐全的函数(这里不方便全部列出)。这些函数可以轻松地用于满足各种需求。

Clojure 中的结构与 Java 或 C++ 等语言中的结构略有不同。它们也与 Common Lisp 中的结构不同(尽管我们在 Clojure 中有 defstruct)。

在 Clojure 中,结构是映射的特殊情况,并在参考的 数据结构 部分进行了解释。

其理念是,结构的多个实例需要使用字段名称(基本上是映射)访问其字段值。这既快速又方便,尤其是在 Clojure 自动将定义为结构实例的访问器的情况下。

以下是处理结构的重要函数

  • defstruct
  • create-struct
  • struct
  • struct-map

有关完整的 API,请参阅 Clojure 参考的 数据结构 部分。

结构是使用 defstruct 创建的,defstruct 是一个包装 create-struct 函数的宏,而 create-struct 函数实际上是创建结构的函数。defstruct 使用 create-struct 创建结构,并将其绑定到提供给 defstruct 的结构名称

create-struct 返回的对象称为结构基础。这不是一个结构实例,但包含结构实例应该是什么样子的信息。新的实例是使用 structstruct-map 创建的。

类型为 **关键字** 或 **符号** 的结构体字段名称可以自动用作函数来访问结构体的字段。这是因为结构体是映射,并且此功能由映射支持。 这对于其他类型的字段名称(例如字符串或数字)**不可行**。由于上述原因,使用关键字作为结构体字段名称**非常常见**。此外,Clojure 优化了结构体以共享基本键信息。以下是示例用法。

 (defstruct employee :name :id)
 (struct employee "Mr. X" 10)                           ; ⇒ {:name "Mr. X", :id 10}
 (struct-map employee :id 20 :name "Mr. Y")             ; ⇒ {:name "Mr. Y", :id 20}

 (def a (struct-map employee :id 20 :name "Mr. Y"))
 (def b (struct employee "Mr. X" 10))'

 ;; :name and :id are accessors
 (:name a)                                              ; ⇒ "Mr. Y"
 (:id b)                                                ; ⇒ 10
 (b :id)                                                ; ⇒ 10
 (b :name)                                              ; ⇒ "Mr. X"

Clojure 也支持 accessor 函数,该函数可用于获取字段的访问器函数,以方便访问。当字段名称的类型不是关键字或符号时,这很重要。这在下面的交互中可以看到。

 (def e-str (struct employee "John" 123))
 e-str
 ;; ⇒ {:name "John", :id 123}

 ("name" e-str) ; ERROR: string not an accessor
 ;; ERROR ⇒
 ;; java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn
 ;; java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn
 ;;         at user.eval__2537.invoke(Unknown Source)
 ;;         at clojure.lang.Compiler.eval(Compiler.java:3847)
 ;;         at clojure.lang.Repl.main(Repl.java:75)

 (def e-name (accessor employee :name))  ; bind accessor to e-name
 (e-name e-str) ; use accessor
 ;; ⇒ "John"

由于结构体是映射,因此可以使用 assoc 将新字段添加到结构体实例中。dissoc 可用于删除这些特定于实例的键。但是请注意,**无法**删除**结构体基本键**。

 b
 ;; ⇒ {:name "Mr. X", :id 10}

 (def b1 (assoc b :function "engineer"))
 b1
 ;; ⇒ {:name "Mr. X", :id 10, :function "engineer"}

 (def b2 (dissoc b1 :function)) ; this works as :function is instance
 b2
 ;; ⇒ {:name "Mr. X", :id 10}

 (dissoc b2 :name)  ; this fails. base keys cannot be dissociated
 ;; ERROR ⇒ java.lang.Exception: Can't remove struct key

assoc 也可以用于“更新”结构体。

 a
 ;; ⇒ {:name "Mr. Y", :id 20}

 (assoc a :name "New Name")
 ;; ⇒ {:name "New Name", :id 20}

 a                   ; note that 'a' is immutable and did not change
 ;; ⇒ {:name "Mr. Y", :id 20}

 (def a1 (assoc a :name "Another New Name")) ; bind to a1
 a1
 ;; ⇒ {:name "Another New Name", :id 20}

请注意,与 Clojure 中的其他序列一样,结构体也是不可变的,因此,简单地执行上面的 assoc 不会改变 a。因此,我们将它重新绑定到 a1。虽然可以将新值重新绑定回 a,但这**不**被认为是好的风格。

异常处理

[编辑 | 编辑源代码]

Clojure 支持 基于 Java 的异常。对于习惯于 Common Lisp 条件系统 的 Common Lisp 用户来说,这可能需要一些适应。

Clojure 不支持条件系统,并且根据 此消息,短期内也不打算支持。也就是说,Clojure 采用的更常见的异常系统非常适合大多数编程需求。

如果您不熟悉异常处理,那么 Java 异常教程 是学习它们的好地方。

在 Clojure 中,可以使用以下函数来处理异常

  • (try expr* catch-clause* finally-clause?)
    • catch-clause -> (catch classname name expr*)
    • finally-clause -> (finally expr*)
  • (throw expr)

您可能希望在 Clojure 中处理的两种类型的异常是

  • Clojure 异常:这些是 Clojure 或底层 Java 引擎生成的异常
  • 用户定义异常:这些是您可能为应用程序创建的异常
Clojure 异常
[编辑 | 编辑源代码]

下面是在 REPL 中抛出异常的简单交互

user=> (/ 1 0)
java.lang.ArithmeticException: Divide by zero
java.lang.ArithmeticException: Divide by zero
        at clojure.lang.Numbers.divide(Numbers.java:142)
        at user.eval__2127.invoke(Unknown Source)
        at clojure.lang.Compiler.eval(Compiler.java:3847)
        at clojure.lang.Repl.main(Repl.java:75)

在上面的情况下,我们看到一个 java.lang.ArithmeticException 被抛出。这是一个由底层 JVM 抛出的运行时异常。对于新用户来说,冗长的消息有时会令人望而生畏,但诀窍是只关注异常(java.lang.ArithmeticException: Divide by zero)而不必理会其他跟踪信息。

编译器在 REPL 中也可能会抛出类似的异常。

user=> (def xx yy)
java.lang.Exception: Unable to resolve symbol: yy in this context
clojure.lang.Compiler$CompilerException: NO_SOURCE_FILE:4: Unable to resolve symbol: yy in this context
        at clojure.lang.Compiler.analyze(Compiler.java:3669)
        at clojure.lang.Compiler.access$200(Compiler.java:37)
        at clojure.lang.Compiler$DefExpr$Parser.parse(Compiler.java:335)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:3814)
        at clojure.lang.Compiler.analyze(Compiler.java:3654)
        at clojure.lang.Compiler.analyze(Compiler.java:3627)
        at clojure.lang.Compiler.eval(Compiler.java:3851)
        at clojure.lang.Repl.main(Repl.java:75)

在上面的情况下,编译器找不到 yy 的绑定,因此它抛出异常。如果您的程序是正确的(即,在这种情况下 yy 已定义为 (def yy 10)),您将不会看到任何编译时异常。

以下交互显示了如何处理运行时异常,例如 ArithmeticException

user=> (try (/ 1 0)
            (catch Exception e (prn "in catch"))
            (finally (prn "in finally")))
"in catch"
"in finally"
nil

try 块的语法是 (try expr* catch-clause* finally-clause?)

可以看出,在 Clojure 中处理异常非常容易。需要注意的一点是,(catch Exception e ...) 是对异常的全面捕获,因为 Exception 是所有异常的超类。也可以捕获**特定**的异常,这通常是一个好主意。

在下面的示例中,我们专门捕获 ArithmeticException

user=> (try (/ 1 0) (catch ArithmeticException e (prn "in catch")) (finally (prn "in finally")))
"in catch"
"in finally"
nil

当我们在 catch 块中使用其他异常类型时,我们发现 ArithmeticException 没有被捕获,而是被 REPL 看到。

user=> (try (/ 1 0) (catch IllegalArgumentException e (prn "in catch")) (finally (prn "in finally")))
"in finally"
java.lang.ArithmeticException: Divide by zero
java.lang.ArithmeticException: Divide by zero
        at clojure.lang.Numbers.divide(Numbers.java:142)
        at user.eval__2138.invoke(Unknown Source)
        at clojure.lang.Compiler.eval(Compiler.java:3847)
        at clojure.lang.Repl.main(Repl.java:75)
用户定义异常
[编辑 | 编辑源代码]

如前所述,Clojure 中的所有异常都必须是 java.lang.Exception(或通常来说是 java.lang.Throwable,它是 Exception 的超类)的子类。这意味着即使您想在 Clojure 中定义自己的异常,也需要从 Exception 派生它。

别担心,这比听起来容易得多:)

Clojure API 提供了一个名为 gen-and-load-class 的函数,该函数可用于扩展 java.lang.Exception 以用于用户定义的异常。gen-and-load-class 会生成并立即加载指定类的字节码。

现在,与其说得太多,不如我们快速看一下代码。

(gen-and-load-class 'user.UserException :extends Exception)

(defn user-exception-test []
  (try
    (throw (new user.UserException "msg: user exception was here!!"))
    (catch user.UserException e
      (prn "caught exception" e))
    (finally (prn "finally clause invoked!!!"))))

在这里,我们正在创建一个名为 'user.UserException 的新类,该类扩展了 java.lang.Exception。我们使用特殊形式 (new Classname-symbol args*) 创建 user.UserException 的实例。然后将其**抛出**。

有时您可能会遇到类似 (user.UserException. "msg: user exception was here!!") 的代码。这只是另一种说 new 的方式。请注意 user.UserException 后的 .(点)。这做了完全相同的事情。

以下是交互

user=> (load-file "except.clj")
#'user/user-exception-test

user=> (user-exception-test)
"caught exception" user.UserException: msg: user exception was here!!
"finally clause invoked!!!"
nil
user=>

因此,在这里,我们同时调用了 catchfinally 子句。就这样。

借助 Clojure 对 Java 交互的支持,用户也可以在 Java 中创建异常,并在 Clojure 中捕获它们,但通常在 Clojure 中创建异常更方便。

变异工具

[编辑 | 编辑源代码]
员工记录操作
[编辑 | 编辑源代码]

Clojure 中的数据结构和序列是不可变的,如 Clojure_Programming/Concepts#Structures 中介绍的示例所示(建议读者先阅读该部分)。

虽然不可变数据有其优点,但任何规模合理的项目都将要求程序员维护某种状态。在使用不可变序列和数据结构的语言中管理状态是习惯于允许对数据进行变异的编程语言的人们经常感到困惑的地方。

Rich Hickey 撰写了一篇关于 Clojure 方法的好文章 [https://clojure.net.cn/state 值和变化 - Clojure 对身份和状态的处理方法]。

观看 Clojure 并发屏幕广播 可能会有所帮助,因为本节中使用了一些这些概念。特别是**ref** 和**事务**。

在本节中,我们创建了一个简单的员工记录集,并提供了以下功能

  • 添加员工
  • 按姓名删除员工
  • 按姓名更改员工角色

此示例有意保持简单,因为目的是展示状态和变异工具,而不是提供完整的功能。

让我们深入研究代码。

(alias 'set 'clojure.set)   ; use set/fn-name rather than clojure.set/fn-name

(defstruct employee
           :name :id :role) ; == (def employee (create-struct :name :id ..))

(def employee-records (ref #{}))

;;;===================================
;;; Private Functions: No Side-effects
;;;===================================

(defn- update-role [n r recs]
  (let [rec    (set/select #(= (:name %) n) recs)
        others (set/select #(not (= (:name %) n)) recs)]
    (set/union (map #(set [(assoc % :role r)]) rec) others)))

(defn- delete-by-name [n recs]
  (set/select #(not (= (:name %) n)) recs))

;;;=============================================
;;; Public Function: Update Ref employee-records
;;;=============================================
(defn update-employee-role [n r]
  "update the role for employee named n to the new role r"
  (dosync 
    (ref-set employee-records (update-role n r @employee-records))))

(defn delete-employee-by-name [n]
  "delete employee with name n"
  (dosync
    (ref-set employee-records
             (delete-by-name n @employee-records))))

(defn add-employee [e]
  "add new employee e to employee-records"
  (dosync (commute employee-records conj e)))

;;;=========================
;;; initialize employee data
;;;=========================
(add-employee (struct employee "Jack" 0 :Engineer))
(add-employee (struct employee "Jill" 1 :Finance))
(add-employee (struct-map employee :name "Hill" :id 2 :role :Stand))

在开头几行中,我们定义了 employee 结构体。之后有趣的定义是 employee-records

(def employee-records (ref #{}))

在 Clojure 中,ref 允许使用事务来变异存储位置。

user=> (def x (ref [1 2 3]))
#'user/x
user=> x
clojure.lang.Ref@128594c
user=> @x
[1 2 3]
user=> (deref x)
[1 2 3]
user=>

接下来,我们使用 defn- 定义了私有函数 update-roledelete-by-name(请注意末尾的减号“-”)。请注意,这些是 纯函数,没有任何副作用。

update-role 接受员工姓名 n、新角色 r 和员工记录表 recs。由于序列是不可变的,因此此函数返回一个**新的**记录表,其中员工角色已相应更新。delete-by-name 也以类似的方式工作,在删除相关员工记录后返回一个新的员工表。

有关 set API 的说明,请参阅 Clojure API 参考

我们还没有查看如何维护状态。这是通过清单中的公共函数 update-employee-roledelete-employee-by-nameadd-employee 来完成的。

这些函数将记录处理的工作委托给私有函数。需要注意的重要事项是使用了以下函数

  • ref-set 设置 ref 的值。
  • dosync 是必需的,因为 ref 只能在事务中更新,而 dosync 会设置事务。
  • commute 更新 ref 的事务内值。

有关这些函数的详细说明,请参阅 API 参考中的 ref 部分

add-employee 函数非常简单,因此没有将其分解为私有函数和公共函数。

源代码清单在最后用示例数据初始化记录。

以下是该程序的交互方式。

user=> (load-file "employee.clj")
#{{:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}

user=> @employee-records
#{{:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}

user=> (add-employee (struct employee "James" 3 :Bond))
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}
user=> @employee-records
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}

user=> (update-employee-role "Jill" :Sr.Finance)
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Sr.Finance}}
user=> @employee-records
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Sr.Finance}}

user=> (delete-employee-by-name "Hill")
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Jill", :id 1, :role :Sr.Finance}}
user=> @employee-records
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Jill", :id 1, :role :Sr.Finance}}

关于该程序需要注意两点

  • 使用引用和事务使该程序天生线程安全。如果我们想要扩展该程序以用于多线程环境(使用 Clojure 代理),它将在进行最小更改的情况下进行扩展。
  • 将纯功能与管理状态的公共函数分开,更容易确保功能的正确性,因为纯函数更容易测试。

命名空间 [1]

[编辑 | 编辑源代码]
  • 使用require加载Clojure库
  • 使用refer引用当前命名空间中的函数
  • 使用use在一个步骤中加载并引用所有内容
  • 使用import引用当前命名空间中的Java类

Require

1. 您可以使用(require libname)加载任何Clojure库的代码。尝试使用clojure.contrib.math

     (require clojure.contrib.math)

2. 然后打印命名空间中可用名称的目录

     (dir clojure.contrib.math)

3. 展示使用lcm计算最小公倍数

     1	(clojure.contrib.math/lcm 11 41)
     2	-> 451

4. 在每个函数调用中写出命名空间前缀很麻烦,因此您可以使用as指定一个更短的别名

     (require [clojure.contrib.math :as m])

5. 调用更短的格式要容易得多

     1	(m/lcm 120 1000)
     2	-> 3000

6. 您可以使用以下命令查看所有加载的命名空间

     (all-ns)
Refer和Use
[编辑 | 编辑源代码]

1. 使用没有命名空间前缀的函数会更容易。您可以通过引用名称来做到这一点,这会在当前命名空间中创建一个对该名称的引用

     (refer 'clojure.contrib.math)

2. 现在您可以直接调用lcm

     1	(lcm 16 30)
     2	-> 240

3. 如果您想在一步骤中加载并引用所有内容,请调用use

     (use 'clojure.contrib.math)

4. 引用库会引用其所有名称。这通常不可取,因为

  • 它没有清楚地记录对阅读者的意图
  • 它引入了比您需要的更多名称,这会导致命名冲突

相反,请使用以下风格仅指定您想要的那些名称

     (use '[clojure.contrib.math :only (lcm)])

:only选项在所有命名空间管理表单中都可用。(还有一个:exclude选项,它按照您的预期工作。)

5. 变量*ns*始终包含当前命名空间,您可以通过调用以下命令查看当前命名空间引用的名称

     (ns-refers *ns*)

6. refers映射通常非常大。如果您只对一个符号感兴趣,请将该符号传递给调用ns-refers的结果

     1	((ns-refers *ns*) 'dir)
     2	-> #'clojure.contrib.ns-utils/dir

1. 导入类似于引用,但用于Java类而不是Clojure命名空间。而不是

     (java.io.File. "woozle")

你可以说

     1	(import java.io.File)
     2	(File. "woozle")

2. 您可以使用以下表单导入Java包中的多个类

     (import [package Class Class])

例如

     1	(import [java.util Date Random])
     2	(Date. (long (.nextInt (Random.))))

3. 对Lisp不熟悉的新手程序员通常会被像上面日期创建这样的“内向外”读取形式所困扰。从内部开始,您

  • 获得一个新的Random
  • 获取下一个随机整数
  • 将其转换为long
  • 将long传递给Date构造函数

您不必在Clojure中编写内向外代码。->宏接受其第一个形式,并将其作为第一个参数传递给其下一个形式。然后,结果成为下一个形式的第一个参数,依此类推。它比描述更容易阅读

     1	(-> (Random.) (.nextInt) (long) (Date.))
     2	-> #<Date Sun Dec 21 12:47:20 EST 1969>
Load和Reload
[编辑 | 编辑源代码]

REPL并非适合所有情况。对于您计划保留的工作,您需要将源代码放在单独的文件中。以下是在创建自己的Clojure命名空间时需要记住的经验法则。

1. Clojure命名空间(也称为库)等效于Java包。

2. Clojure遵循Java的目录和文件命名约定,但遵循Lisp的命名空间名称命名约定。因此,一个Clojure命名空间com.my-app.utils将位于名为com/my_app/utils.clj的路径中。特别注意下划线/连字符之间的区别。

3. Clojure文件通常以命名空间声明开头,例如

     (ns com.my-app.utils)

4. 在上一节中介绍的import/use/refer/require语法用于REPL。命名空间声明允许类似的表单 - 足够相似以帮助记忆,但也足够不同以至于让人困惑。在REPL中的以下表单

     1	(use 'foo.bar)
     2	(require 'baz.quux)
     3	(import '[java.util Date Random])

在源代码文件中看起来像这样

     1	(ns
     2	 com.my-app.utils
     3	 (:use foo.bar)
     4	 (:require baz.quux)
     5	 (:import [java.util Date Random]))

符号变成关键字,不再需要引用。

5. 在撰写本文时,使用命名空间错误执行的错误消息很模糊。小心。

现在让我们尝试创建一个源代码文件。我们现在不会费心进行显式编译。Clojure会自动(并且快速)编译类路径上的源代码文件。相反,我们只需将Clojure(.clj)文件添加到src目录中即可。

1. 在src目录中创建一个名为student/dialect.clj的文件,其中包含适当的命名空间声明

     (ns student.dialect)

2. 现在,实现一个简单的canadianize函数,该函数接受一个字符串,并在其末尾添加,eh?

     (defn canadianize [sentence] (str sentence ", eh"))

3. 从您的REPL中,使用新的命名空间

     (use 'student.dialect)

4. 现在试一试。

     1	(canadianize "Hello, world.")
     2	-> "Hello, world., eh"

5. 糟糕!我们需要从输入的末尾删除句号。幸运的是,clojure.contrib.str-utils2提供了chop。返回student/dialect.clj并添加clojure.contrib.str-utils2中的require

     (ns student.dialect (:require [clojure.contrib.str-utils2 :as s]))

6. 现在,更新canadianize以使用chop

     (defn canadianize [sentence] (str (s/chop sentence) ", eh?"))

7. 如果您只是尝试从REPL中调用canadianize,您将看不到新的更改,因为代码已经加载了。但是,您可以使用带有reload(或reload-all)的命名空间表单来重新加载命名空间(及其依赖项)。

     (use :reload 'student.dialect)

8. 现在您应该看到canadianize的新版本

     1	(canadianize "Hello, world.")
     2	-> "Hello, world, eh?"

函数式编程

[编辑 | 编辑源代码]

匿名函数

[编辑 | 编辑源代码]

Clojure使用fn或更短的reader宏 #(..)支持匿名函数#(..)很方便,因为它简洁,但有些限制,因为#(..)形式不能嵌套

以下是一些使用两种形式的示例

user=> ((fn [x] (* x x)) 3)
9

user=> (map #(list %1 (inc %2)) [1 2 3] [1 2 3])
((1 2) (2 3) (3 4))

user=> (map (fn [x y] (list x (inc y))) [1 2 3] [1 2 3])
((1 2) (2 3) (3 4))

user=> (map #(list % (inc %)) [1 2 3])
((1 2) (2 3) (3 4))

user=> (map (fn [x] (list x (inc x))) [1 2 3])
((1 2) (2 3) (3 4))

user=> (#(apply str %&) "Hello")
"Hello"

user=> (#(apply str %&) "Hello" ", " "World!")
"Hello, World!"

请注意,在#(..)形式中,%N用于参数(从1开始),%&用于剩余参数。%%1的同义词。

序列的惰性求值

[编辑 | 编辑源代码]

本节尝试逐步浏览一些代码,以更好地了解Clojure对序列的惰性求值以及它可能如何有用。我们还测量内存和时间以更好地了解发生了什么。

假设我们想要对包含十亿个项目的列表中的记录进行big-computation(每次1秒)。通常我们可能不需要处理所有十亿个项目(例如,我们可能只需要过滤后的子集)。

让我们定义一个名为free-mem的小型实用程序函数来帮助我们监控内存使用情况,以及另一个名为big-computation的函数,该函数需要1秒钟才能完成其工作。

(defn free-mem [] (.freeMemory (Runtime/getRuntime)))

(defn big-computation [x] (Thread/sleep 1000) (* 10 x))

在上面的函数中,我们使用java.lang.Runtimejava.lang.Thread来获取可用内存并支持休眠。

我们还将使用内置函数time来测量我们的性能。

以下是REPL中的简单用法

user=> (defn free-mem [] (.freeMemory (Runtime/getRuntime)))
#'user/free-mem

user=> (defn big-computation [x] (Thread/sleep 1000) (* 10 x))
#'user/big-computation

user=> (time (big-computation 1))
"Elapsed time: 1000.339953 msecs"
10

现在我们定义一个包含十亿个数字的列表,名为nums

user=> (time (def nums (range 1000000000)))
"Elapsed time: 0.166994 msecs"
#'user/nums

请注意,Clojure创建包含十亿个数字的列表需要0.17毫秒。这是因为列表实际上并没有创建。用户只是从Clojure那里得到一个承诺,即在需要时将返回来自该列表的适当数字。

现在,假设我们想要对来自该列表的10000到10005的x应用big-computation

这是它的代码

;; The comments below should be read in the numbered order
;; to better understand this code.

(time                              ; [7] time the transaction
  (def v                           ; [6] save vector as v
    (apply vector                  ; [5] turn the list into a vector
           (map big-computation    ; [4] process each item for 1 second
                (take 5            ; [3] take first 5 from filtered items
                      (filter      ; [2] filter items 10000 to 10010
                        (fn [x] (and (> x 10000) (< x 10010)))
                        nums)))))) ; [1] nums = 1 billion items

将此代码放在REPL中,我们将得到以下结果

user=> (free-mem)
2598000
user=> (time (def v (apply vector (map big-computation (take 5 (filter (fn [x] (and (> x 10000) (< x 10010))) nums))))))
"Elapsed time: 5036.234311 msecs"
#'user/v
user=> (free-mem)
2728800

代码块中的注释指示了这段代码的工作原理。它花了我们大约5秒钟来执行这段代码。以下是一些需要注意的地方

  • 从列表中过滤出编号为10000到10010的项目没有花费我们10000秒
  • 从10个过滤后的列表中获取前5个项目没有花费我们10秒
  • 总的来说,计算只花了大约5秒钟,这基本上就是计算时间。
  • 即使我们现在有处理十亿个记录的承诺,可用内存数量实际上也相同。(它实际上似乎有所增加,因为发生了垃圾回收)


现在,如果我们访问v,它只需要忽略不计的时间。

user=> (time (seq v))
"Elapsed time: 0.042045 msecs"
(100010 100020 100030 100040 100050)
user=>

需要注意的另一点是,惰性序列并不意味着每次都会进行计算;一旦计算完成,它就会被缓存。

尝试以下操作

user=> (time (def comps (map big-computation nums)))
"Elapsed time: 0.113564 msecs"
#'user/comps

user=> (defn t5 [] (take 5 comps))
#'user/t5

user=> (time (doall (t5)))
"Elapsed time: 5010.49418 msecs"
(0 10 20 30 40)

user=> (time (doall (t5)))
"Elapsed time: 0.096104 msecs"
(0 10 20 30 40)

user=>

在第一步中,我们将big-computation映射到十亿个nums。然后,我们定义一个名为t5的函数,该函数从comps中获取5个计算。观察到,t5第一次需要5秒,之后它只需要忽略不计的时间。这是因为一旦计算完成,结果就会被缓存以备将来使用。由于t5的结果也是惰性的,因此需要doall来强制它在time返回REPL之前被急切地求值。

惰性数据结构可以提供显著的优势,前提是程序被设计为利用它。为惰性序列和无限数据结构设计程序是与在C和Java等语言中急切地执行计算相比的一种范式转变,而是在提供计算承诺

本节内容基于这封邮件中的 Clojure 论坛讨论。

无限数据源

[编辑 | 编辑源代码]

由于 Clojure 支持惰性求值序列,因此可以在 Clojure 中使用无限数据源。可以使用以下代码定义无限序列 (0 1 2 3 4 5 ....)(range)从 Clojure 1.2 版开始:[2]

 (def nums (range))         ; Old version (def nums (iterate inc 0))
 ;; ⇒ #'user/nums
 (take 5 nums)
 ;; ⇒ (0 1 2 3 4)
 (drop 5 (take 11 nums))
 ;; ⇒ (5 6 7 8 9 10)

这里我们看到了两个用于创建从 0 开始的无限数字列表的函数。由于 Clojure 支持惰性序列,因此只会生成所需的项目并从该列表的头部取出。在上述情况下,如果您在提示符处直接输入(range)(iterate inc 0)直接在提示符处,[https://clojure.net.cn/reader读取器] 将会不断获取下一个数字,您需要终止进程。

(iterate f x) 是一个函数,它不断将 f 应用于之前将 f 应用于 x 的结果。这意味着,结果是 ...(f(f(f(f .....(f(f(f x)))))...(iterate inc 0) 首先返回 0 作为结果,然后 (inc 0) => 1,然后 (inc (inc 0)) => 2,依此类推。

(take n coll) 基本上会从集合中删除 n 个项目。此主题有很多变体

  • (take n coll)
  • (take-nth n coll)
  • (take-last n coll)
  • (take-while pred coll)
  • (drop n coll)
  • (drop-while pred coll)

建议读者查看Clojure 序列 API以了解更多详细信息。

列表推导

[编辑 | 编辑源代码]

列表推导是语言提供的构造,使从旧列表中轻松创建新列表成为可能。听起来很简单,但它是一个非常强大的概念。Clojure 对列表推导提供了良好的支持。

假设我们想要一个包含 x + 1 的集合,其中 x 可被 4 整除,x0 开始。

以下是使用 Clojure 完成此操作的一种方法

(def nums (iterate inc 0))
;; ⇒ #'user/nums
(def s (for [x nums :when (zero? (rem x 4))] (inc x)))
;; ⇒ #'user/s
(take 5 s)
;; ⇒ (1 5 9 13 17)

nums 是我们在上一节中看到的无限数字列表。我们需要 (def s ...) 用于集合,因为我们正在创建一个无限的数字源。在提示符处直接运行它会导致 读取器 不断从该源中提取数字。

这里的关键构造是 for 宏。这里表达式 [x nums ... 表示 x 会从 nums 中逐个提取出来。下一条子句 .. :when (zero? (rem x 4)) .. 基本上表示只有当 x 满足此条件时才会将其提取出来。一旦提取了 x,inc 就会应用于它。将所有这些绑定到 s 会得到一个无限集合。因此,我们看到了 (take 5 s) 和预期的结果。

另一种实现相同结果的方法是使用 mapfilter

(def s (map inc (filter (fn [x] (zero? (rem x 4))) nums)))
;; ⇒ #'user/s
(take 5 s)
;; ⇒ (1 5 9 13 17)

这里我们创建了一个谓词 (fn [x] (zero? (rem x 4))),并且只有当此谓词满足时才会从 nums 中提取 x。这是通过 filter 完成的。请注意,由于 Clojure 是惰性的,因此 filter 给出的只是一个提供满足谓词的下一个数字的承诺。它不会(在这种情况下也不能)评估整个列表。一旦我们有了这个 x 流,只需将 inc 映射到它 (map inc ... 即可。

列表推导(即 for)和 map/filter 之间的选择很大程度上取决于用户偏好。两者之间没有主要优势。

序列函数

[编辑 | 编辑源代码]
(first coll)
[编辑 | 编辑源代码]

获取序列的第一个元素。对于空序列或 nil 返回 nil

 (first (list 1 2 3 4))
 ;; ⇒ 1
 (first (list))
 ;; ⇒ nil
 (first nil)
 ;; ⇒ nil
 (map first [[1 2 3] "Test" (list 'hi 'bye)])
 ;; ⇒ (1 \T hi)
 (first (drop 3 (list 1 2 3 4)))
 ;; ⇒ 4
(rest coll)
[编辑 | 编辑源代码]

获取序列中除了第一个元素之外的所有元素。对于空序列或 nil 返回 nil

 (rest (list 1 2 3 4))
 ;; ⇒ (2 3 4)
 (rest (list))
 ;; ⇒ nil
 (rest nil)
 ;; ⇒ nil
 (map rest [[1 2 3] "Test" (list 'hi 'bye)])
 ;; ⇒ ((2 3) (\e \s \t) (bye))
 (rest (take 3 (list 1 2 3 4)))
 ;; ⇒ (2 3)
(map f colls*)
[编辑 | 编辑源代码]

f 惰性应用于序列中的每个项目,返回一个包含 f 返回值的惰性序列。

由于提供的函数始终返回 true,因此这两个函数都返回一个包含 true 的序列,重复十次。

 (map (fn [x] true) (range 10))
 ;; ⇒ (true true true true true true true true true true)
 (map (constantly true) (range 10)) 
 ;; ⇒ (true true true true true true true true true true)

这两个函数都将其参数乘以 2,因此 (map ...) 返回一个序列,其中原始序列中的每个项目都乘以 2。

 (map (fn [x] (* 2 x)) (range 10))
 ;; ⇒ (0 2 4 6 8 10 12 14 16 18)
 (map (partial * 2) (range 10))
 ;; ⇒ (0 2 4 6 8 10 12 14 16 18)

(map ...) 可以接受您提供的任意多个序列(尽管它至少需要一个序列),但函数参数必须接受与序列数量相同的参数。

因此,这两个函数给出了乘在一起的序列

 (map (fn [a b] (* a b)) (range 10) (range 10))
 ;; ⇒ (0 1 4 9 16 25 36 49 64 81)
 (map * (range 10) (range 10))
 ;; ⇒ (0 1 4 9 16 25 36 49 64 81)

但第一个函数只接受两个序列作为参数,而第二个函数接受任意多个序列作为参数。

 (map (fn [a b] (* a b)) (range 10) (range 10) (range 10))
 ;; ⇒ java.lang.IllegalArgumentException: Wrong number of args passed
 (map * (range 10) (range 10) (range 10))
 ;; ⇒ (0 1 8 27 64 125 216 343 512 729)

(map ...) 将在到达任何提供的序列的末尾时停止求值,因此在这三个示例中,(map ...) 在 5 个项目处停止求值(最短序列的长度),尽管第二和第三个示例提供的序列长度大于 5 个项目(在第三个示例中,较长的序列是无限长度的)。

它们中的每一个都接受一个仅包含数字 2 的序列和一个包含数字 (0 1 2 3 4) 的序列,并将它们乘在一起。

 (map * (replicate 5 2) (range 5))
 ;; ⇒ (0 2 4 6 8)
 (map * (replicate 10 2) (range 5))
 ;; ⇒ (0 2 4 6 8)
 (map * (repeat 2) (range 5))
 ;; ⇒ (0 2 4 6 8)
(every? pred coll)
[编辑 | 编辑源代码]

如果 pred 对于序列中的每个项目都为 true,则返回 true。否则返回 false。pred 在这里是一个接受单个参数并返回 true 或 false 的函数。

由于此函数始终返回 true,因此 (every? ...) 的值为 true。请注意,这两个函数表达了相同的内容。

 (every? (fn [x] true) (range 10))
 ;; ⇒ true
 (every? (constantly true) (range 10))
 ;; ⇒ true

(pos? x) 当其参数大于零时返回 true。由于 (range 10) 给出一个从 0 到 9 的数字序列,而 (range 1 10) 给出一个从 1 到 10 的数字序列,因此 (pos? x) 对于第一个序列返回 false 一次,而对于第二个序列永远不会返回 false。

 (every? pos? (range 10))
 ;; ⇒ false
 (every? pos? (range 1 10))
 ;; ⇒ true

此函数当其参数为偶数时返回 true。由于 1 到 10 之间的范围和序列 (1 3 5 7 9) 包含奇数,因此 (every? ...) 返回 false。

由于序列 (2 4 6 8 10) 仅包含偶数,因此 (every? ...) 返回 true。

 (every? (fn [x] (= 0 (rem x 2))) (range 1 10))
 ;; ⇒ false
 (every? (fn [x] (= 0 (rem x 2))) (range 1 10 2))
 ;; ⇒ false
 (every? (fn [x] (= 0 (rem x 2))) (range 2 10 2))
 ;; ⇒ true

如果我需要在其他地方检查一个数字是否为偶数,我可能会这样写,在将 (even? num) 作为参数传递给 (every? ...) 之前,先将其定义为一个实际的函数

 (defn even? [num] (= 0 (rem num 2)))
 ;; ⇒ #<Var: user/even?>
 (every? even? (range 1 10 2))
 ;; ⇒ false
 (every? even? (range 2 10 2))
 ;; ⇒ true


补充函数: (not-every? pred coll)

返回 (every? pred coll) 的补充值。如果 pred 对于序列中的所有项目都为 true,则返回 false;否则返回 true。

 (not-every? pos? (range 10))
 ;; ⇒ true
 (not-every? pos? (range 1 10))
 ;; ⇒ false

循环和迭代

[编辑 | 编辑源代码]

三种不同的方式循环从 1 到 20,步长为 2,每次打印循环索引(来自邮件列表讨论

 ;; Version 1
 (loop [i 1]
   (when (< i 20)
     (println i)
     (recur (+ 2 i))))
 
 ;; Version 2
 (dorun (for [i (range 1 20 2)]
          (println i)))
 
 ;; Version 3
 (doseq [i (range 1 20 2)]
   (println i))

相互递归

[编辑 | 编辑源代码]

相互递归在 Clojure 中很棘手,但却是可行的。(defn ...) 的形式只允许函数体引用自身或以前存在的名称。但是,Clojure 允许以以下方式动态重新定义函数绑定

 ;;; Mutual recursion example
 
 ;; Forward declaration
 (def even?)
 
 ;; Define odd in terms of 0 or even
 (defn odd? [n]
   (if (zero? n)
       false
       (even? (dec n))))
 
 ;; Define even? in terms of 0 or odd
 (defn even? [n]
   (if (zero? n)
       true
       (odd? (dec n))))
 
 ;; Is 3 even or odd?
 (even? 3) 
 ;; ⇒ false

在使用 let 定义的内部函数中,相互递归是不可能的。要声明一组私有递归函数,可以使用上述技术,将 defn 替换为 defn-,这将生成私有定义。

但是,可以使用 looprecur 模拟相互递归函数。

(use 'clojure.contrib.fcase)

(defmacro multi-loop
  [vars & clauses]
  (let [loop-var  (gensym "multiloop__")
        kickstart (first clauses)
        loop-vars (into [loop-var kickstart] vars)]
    `(loop ~loop-vars
       (case ~loop-var
          ~@clauses))))

(defn even?
  [n]
  (multi-loop [n n]
    :even (if (zero? n)
            true
            (recur :odd (dec n)))
    :odd  (if (zero? n)
            false
            (recur :even (dec n)))))

集合抽象

[编辑 | 编辑源代码]

http://blog.n01se.net/?p=33 上可以找到有关如何编写宏的详细介绍,作者是 Chouser。

宏用于在编译时转换数据结构。让我们开发一个新的 `do1` 宏。Clojure 的 `do` 特殊形式会评估所有包含的表达式以获取副作用,并返回最后一个表达式的返回值。`do1` 的行为类似,但返回第一个子表达式的值。

一开始应该先考虑宏应该如何调用。

(do1
  :x
  :y
  :z)

返回值应该是 `:x`。接下来要考虑的是如何手动实现这个功能。

(let [x :x]
  :y
  :z
  x)

首先评估 :x,然后是 :y 和 :z。最后,let 评估到 :x 的评估结果。这可以通过使用 `defmacro` 和 `` 来转换为宏。

(defmacro do1
  [fform & rforms]
  `(let [x# ~fform]
     ~@rforms
     x#))

所以这里发生了什么。这只是一个简单的翻译。我们使用 `let` 为第一个表达式的结果创建一个临时位置。由于我们不能简单地使用某个名称(它可能在用户代码中被使用),因此我们使用 `x#` 生成一个新的名称。# 是 Clojure 的特殊符号,它帮助我们:它生成一个新的名称,保证不会被用户代码使用。`~` 对第一个表达式进行“解引用”,即 `~fform` 被第一个参数替换。然后使用 `~@` 插入剩余的表达式。使用 `@` 基本上从以下表达式中删除了一组 ()。最后,我们再次通过 `x#` 引用第一个表达式的结果。

我们可以使用 `(macroexpand-1 '(do1 :x :y :z))` 检查宏的展开。

来自 `clojure.contrib` 的 lib 包现在已集成到 clojure 中。定义可以由其他脚本加载的库很容易。假设我们有一个很棒的 `add1` 函数,我们想提供给其他开发者。那么我们需要什么?首先,我们确定一个命名空间,例如 `example.ourlib`。现在,我们必须在类路径中创建一个文件名“example/ourlib.clj”的文件。内容非常简单。

(ns example.ourlib)

(defn add1
  [x]
  (add x 1))

现在我们只需要使用 `ns` 的功能。假设我们有另一个文件,我们想使用我们的函数。`ns` 允许我们通过多种方式指定我们的需求。最简单的是 `:require`

(ns example.otherns
  (:require example.ourlib))

(defn check-size
  [x]
  (if (too-small x)
    (example.ourlib/add1 x)
    x))

但是如果我们多次需要 `add1` 函数呢?我们必须在前面始终键入命名空间。我们可以添加一个 `(refer 'example.ourlib)`,但是我们可以更容易地做到这一点。只需使用 `:use` 而不是 `:require`!`:use` 加载库,就像 `:require` 一样,并立即引用命名空间。

因此,我们现在已经有两个小型库,它们可能在第三个程序中使用。

(ns example.thirdns
  (:require example.ourlib)
  (:require example.otherns))

同样,我们也可以节省一些输入。类似于 `import`,我们可以提取库命名空间的通用前缀。

(ns example.thirdns
  (:require (example ourlib otherns)))

当然,`ourlib` 包含 738 个以上的函数,而不仅仅是上面显示的那些。我们并不真正想使用 `use`,因为引入如此多的名称会导致冲突,但我们也不想一直键入命名空间。所以我们首先使用 `alias`。但是等等!你猜对了:`ns` 又帮助我们了。

(ns example.otherns
  (:require (example [ourlib :as ol])))

`:as` 负责别名处理,现在我们可以将 `add1` 函数称为 `ol/add1`!

到目前为止,它已经相当不错了。但是,如果我们稍微考虑一下我们的源代码组织,我们可能会发现,在一个文件中放 739 个函数可能不是最好的主意。因此,我们决定进行一些重构。我们创建一个文件“example/ourlib/add1.clj”并将我们的函数放在那里。我们不希望用户必须加载多个文件而不是一个文件,因此我们修改“example/ourlib.clj”文件以如下方式加载任何其他文件。

(ns example.ourlib
  (:load "ourlib/add1"
         "ourlib/otherfunc"
         "ourlib/morefuncs"))

因此,用户仍然加载“公共”example.ourlib 库,它负责加载其余部分。(`:load` 实现包含代码,为要加载的文件提供“.clj”后缀)

有关更多信息,请参阅 `require` 的文档字符串 - `(doc require)`。

参考文献

[编辑 | 编辑源代码]
华夏公益教科书