查看一些Clojure代码的详细指南

241 阅读6分钟

你见过任何Clojure代码吗?

有吗?你几乎肯定不会在这里看到任何新的或有趣的东西。

没有吗?让我们来改变这种情况吧!我当然不是专家:但我一直在阅读Russ Olsen写的 "Getting Clojure",所以我至少知道足够的东西来快速浏览。

基础知识

你好!

(println "Hello World")

现在还不算太奇怪。这一行周围有括号,但它与类似print "Hello World" 的东西相差不大。

但在这里,下面一行是将1到4的整数相加:

(+ 1 2 3 4) ; 10

如果你从未见过Lisp语言,那么这可能看起来很奇怪。Clojure是我的第一种Lisp方言,一开始肯定看起来很奇怪。但我惊讶地发现,我很快就欣赏到了小括号的清晰结构。

xkcd Lisp Cycles comicxkcd Lisp Cycles 漫画

作为一种Lisp方言,Clojure的语法是小括号中的结构化数据。要在Clojure中实现任何事情,都必须在括号中进行。

Clojure中的注释是以分号为前缀的(双分号习惯上是指完全是注释的行)。

每一个(到目前为止我所看到的)Clojure调用的结构

(function arg0 arg1 arg2 arg3 … argN)

有了这些知识,我们就可以理解第一行(+ 1 2 3 4)+ 函数正在被调用,其参数是1 2 3 4 。在非Lisp语言中的对应行是1 + 2 + 3 + 4

Clojure的主要基础之一是一切都通过函数完成。算术、变量赋值、逻辑、条件,甚至地图中的键的查找都是通过将键作为一个函数来完成。

;; Arithmetic
(+ 1 2 3)  ; 6
(/ 6 2)    ; 3
(/ 24 2 3) ; 4

;; Variable Assignment
(def greeting "Hello")

;; Logic
(= 2 3)    ; false
(= 2 2 2)  ; true
(> 9 7 3)  ; true
(> 9 4 5)  ; false

;; Conditionals
(def a 9)
(def b 3)
(if (> a b) a b) ; 9

;; Map lookup
(def zork1 {:released 1980 :genre :text-adventure})
(:released zork1) ; 1980
(:genre zork1)    ; :text-adventure

你会注意到,这些函数的参数比你想象的要多。在许多语言中,平等是一个两个参数的表达式,例如:a == b ,但在Clojure中,你可以相当明确地比较任意数量的参数。

另外,if 这一行可能看起来很奇怪。如果它比b大,则返回a,否则返回b。(我们稍后会对这个问题进行分析)。

(if (> a b) a b)

条件式

在Clojure中,条件式是函数。if 函数被调用时有2个(可以是3个)参数:

  • 如果第一个参数被评估为true ,那么第二个参数被评估,其结果成为整个语句的结果。
  • 如果第一个参数的值是false ,那么结果是nil ,或者可选的第三个参数被评估。
user=> (if (= 2 2) "yes 2 equals 2")
"yes 2 equals 2"

user=> (if (= 2 3) "yes 2 equals 2")
nil

现在,之前的if 语句就更有意义了:

(if (> a b) a b)
;   ⌃⌃⌃⌃⌃⌃⌃ ⌃ ⌃
;         │ │ │
;         └────── first arg: the logic to evaluate
;           │ │
;           └──── second arg: evaluated if first arg is true
;             │
;             │
;             └── third arg: evaluated if first arg is false

变量

在Clojure中定义变量也是一个函数。def 这个函数接受一个名字和任何可以评估为值的东西。

user=> (def number 123)
#'user/number

user=> number
123

user=> (+ number 4)
127

函数

作为一种函数式语言,函数自然是Clojure的核心所在。它们可以用fn 函数来定义,该函数接受一个参数的向量(基本上是一个数组),后面是要评估的语句。最后一条语句的结果将是调用该函数的返回结果。

(fn [n] (* 2 n))

我们可以将该函数分配给一个名称,即def

user=> (def doubler (fn [n] (* 2 n)))
#'user/doubler

user=> (doubler 3)
6

user=> (def printy-doubler (fn [n] (println "Doubling" n) (* 2 n)))
#'user/printy-doubler

user=> (printy-doubler 3)
Doubling 3
6

使用deffn 可以工作,但不是Clojure希望其程序员拥有的伟大经验。Clojure提供了defn 函数,可以在一次调用中轻松命名和定义一个函数。

user=> (defn tripler [n] (* 3 n))
#'user/tripler

user=> (tripler 3)
9

为常见的动作提供有用的函数这一主题是Clojure的一个主要特征。它提供了许多函数,仅仅是为了使其编程更简洁、更有表现力、更快乐。

常见的数据结构

Clojure拥有你应该期待的现代编程语言的常用数据结构。

作为一种函数式语言,Clojure有不可变的数据结构。如果你不熟悉它们的使用,快速的要点是,每当你操作数据时,例如向一个列表添加一个新的项目,那么你就不会变异该列表:一个新的列表被创建,并带有额外的项目。

矢量

在一个连续的内存块(即一个数组)中的项目的有序集合,用括号定义:

[1 2 3 "four" ["any data even another vector"]]

(def v [1 2 3 4])
(count v) ; 4
(first v) ; 1
(rest v)  ; (2 3 4)

列表

列表也是一个有序的项目集合,但不是存储在一个连续的内存块中,而是作为链接列表实现。这对何时适合使用这两种数据结构产生了影响:例如,在列表的开头添加一个新项是很容易的,但在向量的开头添加一个新项则相对昂贵。

在实践中,向量的使用要普遍得多。

列表是用圆括号定义的,这意味着它们需要比我们所期望的稍作调整,这样Clojure就不会把它们与表达式混淆:我们需要在它们前面加上'

'(1 2 3 "four" ("a nested list"))

(def l '(1 2 3 4))
(count l) ; 4
(first l) ; 1
(rest l)  ; (2 3 4)

逗号?

你可能已经注意到,上面的向量和列表都没有在值之间使用逗号。Clojure通过将它们视为空白,巧妙地消除了任何逗号参数。如果你必须使用,你可以使用它们,但通常的做法是简单地省略它们。

;; these vectors are all the same data
(= [1 2 3 4] [1,2,3,4] [1,,,,,2,3,4] [1,2,3 4]) ; true

;; commas are whitespace even for expressions
(+,1,2,3,4)   ; 10
(+,1,2,3,4,)  ; 10

地图

将键和值联系起来的数据结构(即字典或哈希值)。注意在键/值的集合之间不需要逗号(因为逗号是空白的)。

任何值都可以是一个键,但Clojure程序通常使用关键字作为键,例如::keyword 。关键词本身可以作为函数使用,以从地图中检索值。

;; a bare map
{:key "value" :another-key "another value"}

;; assigned to a variable
(def ps5 {:name "Playstation 5"
          :publisher "Sony"
          :released 2020})

(:publisher ps5) ; "Sony"
(keys ps5)       ; (:name :publisher :released)

(conj ps5 {:output :hdmi})
;; {:name "Playstation 5", :publisher "Sony", :released 2020, :output :hdmi}

ps5
;; {:name "Playstation 5", :publisher "Sony", :released 2020}

注意,上面的conj (conjoin)函数并没有修改ps5变量。它返回一个带有额外数据的新地图。

文档

Clojure有内联文档,你可以在它的副本中访问:

user=> (doc conj)
-------------------------
clojure.core/conj
([coll x] [coll x & xs])
  conj[oin]. Returns a new collection with the xs
    'added'. (conj nil item) returns (item).  The 'addition' may
    happen at different 'places' depending on the concrete type.

user=> (doc +)
-------------------------
clojure.core/+
([] [x] [x y] [x y & more])
  Returns the sum of nums. (+) returns 0. Does not auto-promote
  longs, will throw on overflow. See also: +'

user=> (doc doc)
-------------------------
clojure.repl/doc
([name])
Macro
  Prints documentation for a var or special form given its name,
   or for a spec if given a keyword

总结

你现在已经看到了一些Clojure了!这还不是一个完整的过程。这还不是对这种强大而富有表现力的语言的全面概述。Clojure还有很多结构,可以帮助你简单地编写强大的程序。