Clojure - Note 3

Part 3 - 2

Note for Clojure 入门教程

迭代

  • dotimes (宏)
  • while (宏)

dotimes 执行给定的表达式指定的次数。

while,当条件为 true 时一直执行相应的表达式。

详略。

列表推导式

(Python 中也有)

  • for
  • doseq

可以用 :when 和 :while 进行过滤。

for 只接受一个表达式,它返回一个惰性集合作为结果,用 dorun 强制提取 for 所返回的惰性集合 (不懂...)

doseq 接受任意数据表达式,以有副作用的方式执行它们,并且返回 nil。

示例,暂略。

递归

递归发生在一个函数直接或者间接调用自己的时候。一般来说递归的退出条件有检查一个集合是否为空,或者一个状态变量是否变成了某个特定的值 (比如 0)。这一种情况一般利用连续调用集合里面的 next 函数来实现。后一种情况一般是利用 dec 函数来递减某一个变量来实现。

尾递归,Java 和 Clojure 并不支持 (啊,真的吗...)

在 Clojure 中避免递归导致栈溢出的解决方案:

  • 使用 loop 和 recur 特殊表
  • 使用 trampoline 函数

loop / recur 组合把一个看似递归的调用变成一个迭代。loop 和 let 类似,会建立一个本地的 binding,但它同时也建立了一个递归点,loop 给这些 binding 一个初始值,对 recur 的调用使用程序的控制权返回给 loop 并且给那些 binding 赋给了新值。给 recur 传递的参数一定要和 loop 所创建的 binding 个数一样,且 recur 只能出现在 loop 的最后一行。(类似尾递归,但确实不是尾递归,因为至少它没有显式地调用自身)

recur 不支持那种一个函数调用另外一个函数,然后那个函数再回调这个函数的这种递归。

示例:

(defn factorial-1 [number]
  (loop [n number factorial 1]
    (if (zero? n)
      factorial
      (recur (dec n) (* factorial n))))

(println (time (factorial-1 5))) ; -> "Elapsed time: 0.071 msecs"\n120

另外的实现方法,reduce / apply,略。比 loop / recur 慢很多。

trampoline 方法的实现,这里没有讲。

谓词

Clojure 提供很多函数来充当谓词 - 测试条件是否成立,它们的返回值是 true 或 false。在 Clojure 中,除了 false 和 nil 被解释成 false,其余都解释成 true。谓词函数的名字一般以问号 ? 结束。

反射是一种获取一个对象的特性,而不是它的值的过程。比如说对象的类型。有很多谓词函数进行反射。 测试一个对象的类型的谓词包括 class? , coll? , decimal? , delay? , float? , fn? , instance? , integer? , isa? , keyword? , list? , macro? , map? , number? , seq? , set? , string? 以及 vector? 。 一些非谓词函数也进行反射操作,包括 ancestors , bases , class , ns-publics 以及 parents。

测试两个值之间关系的谓词有:< , <= , = , not= , == , > , >= , compare , distinct? 以及 identical?。

测试逻辑关系的谓词有:and , or , not , true? , false? 和 nil?。

测试集合的一些谓词在前面已经讨论过了,包括:empty? , not-empty , every? , not-every? , some? 以及 not-any?。

测试数字的谓词有 even? , neg? , odd? , pos? 以及 zero?。

序列

序列可以看成是集合的一个逻辑视图。

LazySeq,很多函数返回的是一个 lazy 序列,这种序列里面的元素不是实际的数据,而是一些方法,它们直到用户真正需要时才被调用。

filter / for / map / range 这些常用的函数返回的都是 lazy 序列。

示例:

(map #(println %) [1 2 3])

当这句表达式在 REPL 中执行时,会马上三行,分别是 1 2 3,然后再输出三个 nil,因为 println 的返回值是 nil。REPL 总是解析执行所有表达式,但在脚本中执行是,这句表达式不会输出任何东西,因为 map 得到的是一个 LazySeq (还是没太理解...)

有很多方法可以强制对 LazySeq 里的方法进行调用,比如 first / second / nth / last 等。

(其余的还不太理解。)

dorun / doall 迫使一个 LazySeq 里面的函数被调用。

doseq 类似 dorun / doall,但它可以同时操作多个 LazySeq。

doall 会缓存结果,dorun / doseq 不会。

一般推荐使用 doseq...

(其余的还不太理解。我理解 doseq 差不多就是用来遍历的吧)

LazySeq 使得创建无限序列成为可能。因为只有需要使用的数据才会在用到的时候被调用创建。

输入和输出

三个特殊的符号:

  • *in* - 对应 stdin
  • *out* - stdout
  • *err* - stderr

可以手动对 *out* 重定向,比如重定向到文件:

(binding [*out* (java.io.FileWriter. "my.log")])
  ...
  (println "This goes to the file my.log")
  ...
  (flush))

打印:

  • print
  • println
  • newline - pirnt + newline = println

其余略,需要时再看文档。

解构

  • _
  • &
  • :as

详略。

命名空间

Java 用 class 组织方法,用包来组织 class。Clojure 用名字空间来组织事物,事物包括:

  • Vars
  • Refs
  • Atoms
  • Agents
  • 函数
  • 名字空间本身

(名字空间? 命名空间?)

符号 (symbols) 是用来给函数、宏以及 binding 分配名字的,符号属于名字空间。任何时候总有一个默认的名字空间,初始化为 "user",保存在 *ns* 特殊符号中。用 in-ns 函数和 ns 宏可以改变默认名字空间。

require 函数加载 Clojure 类库。

(require 'clojure.string) ; 注意前面的单引号

使用此类库中的 join 方法。

(clojure.string/join "$" [1 2 3]) ; -> 1$2$3

alias 函数为名字空间取别名。

(alias 'cs 'clojure.string)
(cs/join "$" [1 2 3])

refer 函数使得访问类库中的方法时不用全称。

(require 'clojure.string)
(refer 'clojure.string)
(join "$" [1 2 3])

require + refer = use

(use 'clojure.string)
(join "$" [1 2 3])

ns 宏,改变当前默认的名字空间,通常在源码第一行指定,支持 :require, :use, :import 等指令 (其实是上面函数的另一种形式,但推荐使用指令,而不是函数,为什么?)

(ns com.ociweb.demo
  (:require ...)
  (:use ...)
  (:import (java.text NumberFormat) (javax.swing JFrame JLabel)))
...

其余略。

元数据

(元编程? 好像并不是)

  • :private - 表示一个 Var 能否被包外的函数访问
  • :doc - 一个 Var 的文档字符串
  • :test - 此函数是否是测试函数
  • :tag - Var 在 Java 中对应的类型?
  • :file - 定义这个 Var 的文件的文件名
  • :line - 定义这个 Var 的行数
  • :name - Var 名字的 Symbol 对象
  • :ns - 所在命名空间
  • :macro - 标志是不是宏

函数及宏,都是有一个 Var 对象来表示的,它们都有关联的元数据。用 var 来获得此对象,用 meta 来查看 var 对象。

(meta (var reverse))
; or
^#'reverse

{
  :ns #<Namespace clojure.core>,
  :name reverse,
  :file "core.clj",
  :line 630,
  :arglists ([coll]),
  :doc "Returns a seq of the items in coll in reverse order. Not lazy."
}

source 函数查看函数源码。

(source reverse)

(def reverse
  ...)

宏是用来给语言添加新的结构,新的元素的。它们是一些在读入期 (而不是编译期) 就会实际代码替换的一个机制。

(读入期,就是预编译期吧)

函数的所有参数都会被求值,即使它没有用到,但宏不一样,如果没有用到的参数,不会被求值。

用 defmacro 定义宏。宏定义最开始的地方是一个反引号 "`",它用来防止宏体中的表达式被求值,宏体中如果想让表达式求值,在表达式前面加上波浪线 "~"。

(defmacro around-zero [number negative-expr zero-expr positive-expr]
  `(let [number# ~number] ; so number is only evaluated once
    (cond
      (< (Math/abs number#) 1e-15) ~zero-expr
      (pos? number#) ~positive-expr
      true ~negative-expr)))

其余的不是很明白,先跳过。

宏不能作为参数传递给函数,但可以用匿名函数对它进行包装再作为参数传递给函数。

并发

Clojure 对于并发是非常友好的,因为它的状态的不可变。

为了更好地进行并发编程是很多开发人员选择 Clojure 的原因。Clojure 的所有的数据都是只读的,除非你显示的用 Var, Ref, Atom 和 Agent 来标明它们是可以修改的。

future 宏可以把它 body 里面的表达式在另外一个线程执行。

pmap,map 的并行版本。

详略。

引用类型

引用类型是一种可变引用指向不可变数据的一种机制。Clojure 有 4 种引用类型:

  • Vars
  • Refs
  • Atoms
  • Agents

共同特征:

  • 都可以指向任意类型的对象
  • 都可以利用函数 deref 以及宏 @ 来读取它所指向的对象
  • 都支持验证函数,这些函数在它们把指向的值发生变化时自动调用,如果新值合法,验证函数返回 true,如果不合法,则返回 false 或抛出异常
  • Agents,支持 watchers。如果监听的引用的值发生变化,Agent 将得到通知 (类似验证函数?)

Vars

Vars 是一种可以有一个被所有线程共享的 root binding 并且每个线程线程还能有自己线程本地 (thread-local) 的值的一种引用类型。

Vars 的创建:

(def name value)

创建和修改本地线程的 Vars:

(binding [name expression] body)
(set! name expression)

不鼓励使用 Vars,因为这和函数式不可变的原则违背。

Refs

Refs 是用来协调对于一个或者多个 binding 的并发修改的。这个协调机制是利用 Software Transactional Memory (STM) 来实现的。 Refs 指定在一个事务里面修改。

对 Refs 的修改要在事务中进行,用 dosync 宏执行一个事务。

创建 Ref 变量,用 ref 函数:

(def name (ref value))

dosync 宏用来包裹一个事务,在事务中中 ref-set 来改变一个 Refs 的值并返回新值。

(dosync
  ...
  (ref-set name new-value)
  ...)

如果要赋的新值提基本旧值,需要三个步骤:

  1. deref 这个 Ref,取得它的旧值
  2. 计算新值
  3. 设置新值

alter 和 commute 函数在一个操作里面完成这三个步骤。示例:

(dosync
  ...
  (alter counter inc)
  ; or as
  (commute counter inc)
  ...)

其余略。

Validation 函数

举了个 validation 函数的例子。如下例所示,my-ref 只能是整数值。

(def my-ref (ref 0 :validator integer?))

(try
  (dosync
    (ref-set my-ref 1)      ; works
    (ref-set my-ref "foo")) ; doesn't work
  (catch IllegalStateException e
    ; do nothing
    ))

(println "my-ref =" @my-ref) ; due to validation failure -> 0

Atoms

Atoms 提供了一种比使用 Refs & STM 更简单的更新当前值的方法。它不受事务的影响,不需要在事务中修改值。

创建 Atom 变量:

(def my-atom (atom 1))

有三个函数可以修改一个 Atom 的值:reset! compare-and-set! swap!

(def my-atom (atom 1))
(reset! my-atom 2)
(println @my-atom) ; -> 2

compare-and-set! 接受三个参数,第一个为 Atom 变量,第二个为期望的 Atom 变量的旧值,第三个为新值。set 时,会先读取 Atom 变量此时的值,看是否与期望的旧值相等,如果相等,就赋新值,否则赋新值失败。

swap! 实际是 compare-and-set! 的封装,但不同的是,swap! 如果失败了,会自动重试,直到成功为止。

Agents

Agents 是用把一些事情放到另外一个线程来做 -- 一般来说不需要事务控制的。它们对于修改一个单个对象的值 (也就是 Agent 的值) 来说很方便。这个值是通过在另外的一个 thread 上面运行一个 "action" 来修改的。一个 action 是一个函数, 这个函数接受 Agent 的当前值以及一些其它参数。

创建 Agent 变量:

(def my-agent (agent value))

使用,说得很模糊,没有例子,先跳过。

Watchers

Agents 可以用作其它几种引用类型的监视器。当一个被监视的引用的值发生了改变之后,Clojure 会通过给 Agent 发送一个 action 的形式通知它。

两个常用方法:add-watch, remove-watch。

例子没太看懂,先跳过。

编译

当 Clojure 的源码作为脚本执行时,它们是在运行时被编译成 Java 的 ByteCode 的。但同时我们也可以提前编译 (AOT - ahead-of-time) 它们成 Java ByteCode,这会缩短程序启动时间,而且产生的 .class 文件还可以供 Java 使用。

编译步骤详略。

在 Java 中调用 Clojure

先跳过,需要时再看。

自动化测试

先跳过,需要时再看。

编辑器和 IDE

略。

桌面应用

略。

Web 应用

Compojure 框架,详略。

数据库

jdbc 库。

clj-record 提供了一个类似 Ruby on Rails 中 ActiveRecord 的数据库访问包。

详略。

常用的第三方库,详略。

总结

略。

参考

略。

results matching ""

    No results matching ""