Clojure

函数式编程

Clojure 是一种函数式编程语言。它提供了避免可变状态的工具,将函数作为一等对象,并强调递归迭代而不是基于副作用的循环。Clojure 是不纯的,因为它不会强制你的程序具有引用透明性,也不会追求“可证明的”程序。Clojure 背后的理念是,大多数程序的大部分都应该采用函数式,并且更具函数性的程序更健壮。

一等函数

fn 创建一个函数对象。它像其他任何值一样产生一个值 - 你可以将其存储在一个变量中,传递给函数等。

(def hello (fn [] "Hello world"))
-> #'user/hello
(hello)
-> "Hello world"

defn 是一个宏,它使定义函数稍微简单一些。Clojure 支持在单个函数对象中进行元数重载、自引用和使用&的变元函数。

;trumped-up example
(defn argcount
  ([] 0)
  ([x] 1)
  ([x y] 2)
  ([x y & more] (+ (argcount x y) (count more))))
-> #'user/argcount
(argcount)
-> 0
(argcount 1)
-> 1
(argcount 1 2)
-> 2
(argcount 1 2 3 4 5)
-> 5

您可以使用 let 在函数内部为值创建局部名称。任何局部名称的作用域都是词法的,因此在局部名称作用域内创建的函数将闭包其值。

(defn make-adder [x]
  (let [y x]
    (fn [z] (+ y z))))
(def add2 (make-adder 2))
(add2 4)
-> 6

使用 let 创建的局部变量不是变量。一旦创建,它们的值永远不会改变!

不可变数据结构

避免修改状态的最简单方法是使用不可变的 数据结构。Clojure 提供了一组不可变的列表、向量、集合和映射。由于它们不能被更改,“添加”或“删除”不可变集合中的某些内容意味着创建一个与旧集合完全相同的新集合,但包含所需的更改。持久性是一个术语,用于描述集合的旧版本在“更改”后仍然可用的属性,以及集合为大多数操作保持其性能保证的属性。具体来说,这意味着新版本不能使用完整副本创建,因为这将需要线性时间。不可避免地,持久性集合是使用链接数据结构实现的,以便新版本可以与先前版本共享结构。单链表和树是基本的功能性数据结构,Clojure 在此基础上添加了散列映射、集合和向量,它们都基于数组映射散列尝试。这些集合具有可读的表示形式和通用的接口。

(let [my-vector [1 2 3 4]
      my-map {:fred "ethel"}
      my-list (list 4 3 2 1)]
  (list
    (conj my-vector 5)
    (assoc my-map :ricky "lucy")
    (conj my-list 5)
    ;the originals are intact
    my-vector
    my-map
    my-list))
-> ([1 2 3 4 5] {:ricky "lucy", :fred "ethel"} (5 4 3 2 1) [1 2 3 4] {:fred "ethel"} (4 3 2 1))

应用程序通常需要关联属性和其他与数据逻辑值正交的数据。Clojure 为此提供了直接支持 元数据。符号和所有集合都支持元数据映射。可以使用 meta 函数访问它。元数据不会影响相等性语义,也不会在集合值的运算中看到元数据。元数据可以读取,也可以打印。

(def v [1 2 3])
(def attributed-v (with-meta v {:source :trusted}))
(:source (meta attributed-v))
-> :trusted
(= v attributed-v)
-> true

可扩展的抽象

Clojure 使用 Java 接口来定义其核心数据结构。这允许将 Clojure 扩展到这些接口的新具体实现,并且库函数将与这些扩展一起工作。与将语言硬编码到其数据类型的具体实现相比,这是一个很大的改进。

一个很好的例子是 seq 接口。通过将核心 Lisp 列表构造转换为抽象,大量库函数扩展到任何可以为其内容提供顺序接口的数据结构。所有 Clojure 数据结构都可以提供 seqs。Seqs 可以像其他语言中的迭代器或生成器一样使用,但具有 seqs 不可变且持久的显著优势。Seqs 非常简单,提供了一个first 函数,它返回序列中的第一个项目,以及一个rest 函数,它返回序列的其余部分,该部分本身要么是 seq 要么是 nil。

(let [my-vector [1 2 3 4]
      my-map {:fred "ethel" :ricky "lucy"}
      my-list (list 4 3 2 1)]
  [(first my-vector)
   (rest my-vector)
   (keys my-map)
   (vals my-map)
   (first my-list)
   (rest my-list)])
-> [1 (2 3 4) (:ricky :fred) ("lucy" "ethel") 4 (3 2 1)]

许多 Clojure 库函数惰性地生成和使用 seqs。

;cycle produces an 'infinite' seq!
(take 15 (cycle [1 2 3 4]))
-> (1 2 3 4 1 2 3 4 1 2 3 4 1 2 3)

您可以使用 lazy-seq 宏定义您自己的惰性 seq 生成函数,该宏采用一个表达式体,这些表达式将在需要时被调用以生成 0 个或多个项目的列表。这是一个简化的 take

(defn take [n coll]
  (lazy-seq
    (when (pos? n)
      (when-let [s (seq coll)]
       (cons (first s) (take (dec n) (rest s)))))))

递归循环

在没有可变局部变量的情况下,循环和迭代必须采用与具有内置forwhile 构造的语言不同的形式,这些构造由更改状态来控制。在函数式语言中,循环和迭代由递归函数调用替换/实现。许多此类语言保证在尾部位置进行的函数调用不会消耗堆栈空间,因此递归循环利用恒定空间。由于 Clojure 使用 Java 调用约定,因此它不能也不进行相同的尾调用优化保证。相反,它提供了 recur 特殊运算符,它通过重新绑定和跳转到最近的封闭循环或函数帧来执行恒定空间递归循环。虽然不如尾调用优化通用,但它允许大多数相同的优雅结构,并且提供了检查对 recur 的调用只能发生在尾部位置的优点。

(defn my-zipmap [keys vals]
  (loop [my-map {}
         my-keys (seq keys)
         my-vals (seq vals)]
    (if (and my-keys my-vals)
      (recur (assoc my-map (first my-keys) (first my-vals))
             (next my-keys)
             (next my-vals))
      my-map)))
(my-zipmap [:a :b :c] [1 2 3])
-> {:b 2, :c 3, :a 1}

对于需要互递归的情况,不能使用 recur。相反,trampoline 可能是一个不错的选择。