Programming Clojure笔记之六——协议和数据类型

抽象是代码重用的基础。Clojure语言本身对序列,容器和可调用性进行了抽象。在Java中,这通常是通过接口和类来实现的。在Clojure中一般使用protocol来完成这些任务。

面向抽象编程

Clojure内置的spit和slurp函数建构在两个抽象的基础上,即写和读。可以将之使用在很多的源和目标类型上。包括文件、URL和socket,并且还可以扩展到其他已经存在或者新创建的类型上。

gulp和expectorate

我们试着创建两个函数gulp和expectorate,分别对应于Clojure的slurp和spit函数。

;当前只能操作java.io.File类型的对象
(ns examples.gulp (:import (java.io FileInputStream InputStreamReader BufferedReader)))
(defn gulp [src] (let [sb (StringBuilder.)] (with-open [reader (-> src FileInputStream. InputStreamReader. BufferedReader.)] (loop [c (.read reader)] (if (neg? c) (str sb) (do (.append sb (char c)) (recur (.read reader))))))))

(ns examples.expectorate (:import (java.io FileOutputStream OutputStreamWriter BufferedWriter)))
(defn expectorate [dst content] (with-open [writer (-> dst FileOutputStream. OutputStreamWriter. BufferedWriter.)] (.write writer (str content))))

;如果我们想要这两个函数支持其他类型呢,如socket、URL
;首先我们想到创建其他两个函数make-reader和make-writer,使用条件表达式从这些类型中创建BufferedReader或者BufferedWriter。
(defn make-reader [src] (-> (condp = (type src) java.io.InputStream src java.lang.String (FileInputStream. src) java.io.File (FileInputStream. src) java.net.Socket (.getInputStream src) java.net.URL (if (= "file" (.getProtocol src)) (-> src .getPath FileInputStream.) (.openStream src))) InputStreamReader. BufferedReader.))


(defn make-writer [dst] (-> (condp = (type dst) java.io.OutputStream dst java.io.File (FileOutputStream. dst) java.lang.String (FileOutputStream. dst) java.net.Socket (.getOutputStream dst) java.net.URL (if (= "file" (.getProtocol dst)) (-> dst .getPath FileOutputStream.) (throw (IllegalArgumentException. "Can't write to non-file URL")))) OutputStreamWriter. BufferedWriter.))

;对应的gulp和expectorate分别改写如下
(defn gulp [src] (let [sb (StringBuilder.)] (with-open [reader (make-reader src)] (loop [c (.read reader)] (if (neg? c) (str sb) (do (.append sb (char c)) (recur (.read reader))))))))

(defn expectorate [dst content] (with-open [writer (make-writer dst)] (.write writer (str content))))

这种抽象机制很原始,原因在于其封闭性。假如要支持其他类型,那么make-reader和make-writer就必须得改写。为了处理这种问题,应运而生的就是接口机制。

接口

将make-reader和make-writer抽象成一个接口如下

(definterface IOFactory (^java.io.BufferReader make-reader [this]) (^java.io.BufferedWriter make-writer [this]))

需要得到支持的类型只需要实现这个接口就行。
然而接口机制依旧有问题,如果要支持已经存在的类型,怎么办。
事实上,Clojure中有一个更好的机制,那就是协议(protocol)。

协议

将make-reader和make-writer抽象成一个协议如下

(defprotocol IOFactory "A protocol for things that can be read from and written to." (make-reader [this] "Creates a BufferedReader.") (make-writer [this] "Creates a BufferedWriter."))

然后可以使用该协议扩展InputStream和OutStream类型

(extend InputStream IOFactory {:make-reader (fn [src] (-> src InputStreamReader. BufferedReader.)) :make-writer (fn [dst] (throw (IllegalArgumentException. "Can't open as an InputStream.")))})

(extend OutputStream IOFactory {:make-reader (fn [src] (throw (IllegalArgumentException. "Can't open as an OutputStream."))) :make-writer (fn [dst] (-> dst OutputStreamWriter. BufferedWriter.))})

extend-type宏的语法更加清晰一点

;注意递归的调用了make-reader和make-writer对InputStream和OutStream的实现
(extend-type File IOFactory (make-reader [src] (make-reader (FileInputStream. src))) (make-writer [dst] (make-writer (FileOutputStream. dst))))

使用extend-protocol宏,可以一次性添加多个类型对该协议的实现

(extend-protocol IOFactory Socket (make-reader [src] (make-reader (.getInputStream src))) (make-writer [dst] (make-writer (.getOutputStream dst))) URL (make-reader [src] (make-reader (if (= "file" (.getProtocol src)) (-> src .getPath FileInputStream.) (.openStream src)))) (make-writer [dst] (make-writer (if (= "file" (.getProtocol dst)) (-> dst .getPath FileInputStream.) (throw (IllegalArgumentException. "Can't write to non-file URL"))))))

数据类型

接下来,我们使用deftype宏定义一个新的数据类型CryptoVault,该数据类型将会实现两个协议,其中包括IOFactory。

;该数据类型包含三个字段
(deftype CryptoVault [filename keystore password])

;创建该类型的一个实例
(def vault (->CryptoVault "vault-file" "keystore" "toomanysecrets"))

;获取实例的字段值
(.filename vault)

;给CryptoVault添加方法,即定义一个协议
(defprotocol Vault (init-vault [vault]) (vault-output-stream [vault]) (vault-input-stream [vault]))

(deftype CryptoVault [filename keystore password] Vault (init-vault [vault] (let [password (.toCharArray (.password vault)) key (.generateKey (KeyGenerator/getInstance "AES")) keystore (doto (KeyStore/getInstance "JCEKS") (.load nil password) (.setEntry "vault-key" (KeyStore$SecretKeyEntry. key) (KeyStore$PasswordProtection. password)))] (with-open [fos (FileOutputStream. (.keystore vault))] (.store keystore fos password)))) (vault-output-stream [vault] (let [cipher (doto (Cipher/getInstance "AES") (.init Cipher/ENCRYPT_MODE (vault-key vault)))] (CipherOutputStream. (io/output-stream (.filename vault)) cipher))) (vault-input-stream [vault] (let [cipher (doto (Cipher/getInstance "AES") (.init Cipher/DECRYPT_MODE (vault-key vault)))] (CipherInputStream. (io/input-stream (.filename vault)) cipher))) proto/IOFactory (make-reader [vault] (proto/make-reader (vault-input-stream vault))) (make-writer [vault] (proto/make-writer (vault-output-stream vault))))

;为了使得内置的spit和slurp函数能作用于CryptoVault。需要扩展以实现clojure.java.io/IOFactory,这个版本的IOFactory有四个方法,除了我们定义的两个,还有两个默认方法定义在default-streams-impl映射表中。我们需要重写这两个方法。
(extend CryptoVault clojure.java.io/IOFactory (assoc clojure.java.io/default-streams-impl :make-input-stream (fn [x opts] (vault-input-stream x)) :make-output-stream (fn [x opts] (vault-output-stream x))))

记录(record)

简而言之,记录是一种特殊的数据类型,实现了PersistentMap,因而可以当做map使用。

;定义了一个record
(defrecord Note [pitch octave duration]) -> user.Note

;创建一个实例
(->Note :D# 4 1/2) -> #user.Note{:pitch :D#, :octave 4, :duration 1/2}

;访问字段
(.pitch (->Note :D# 4 1/2)) -> :D#

;记录同样也是一个map
(map? (->Note :D# 4 1/2)) -> true

;因而可以像map一样使用关键字访问字段
(:pitch (->Note :D# 4 1/2)) -> :D#

;使用assoc和update-in修改记录
(assoc (->Note :D# 4 1/2) :pitch :Db :duration 1/4) -> #user.Note{:pitch :Db, :octave 4, :duration 1/4}
(update-in (->Note :D# 4 1/2) [:octave] inc) -> #user.Note{:pitch :D#, :octave 5, :duration 1/2}

;关联一个额外的字段
(assoc (->Note :D# 4 1/2) :velocity 100) -> #user.Note{:pitch :D#, :octave 4, :duration 1/2, :velocity 100}

;assoc和update-in函数返回一个新的记录,然而dissoc函数情况比较复杂,如果dissoc删除的字段是可选的,如上例添加的:velocity,那么同样返回记录。如果dissoc删除的字段是定义记录时指定的,则返回一个普通的map。
(dissoc (->Note :D# 4 1/2) :octave) -> {:pitch :D#, :duration 1/2}

;跟map的不同的是,记录不可以当关键字的函数使用
((->Note. :D# 4 1/2) :pitch) -> user.Note cannot be cast to clojure.lang.IFn

;如下定义了一个代表音符(Note)的record,并且实现了MidiNote接口,并且定义了一个演奏函数perform,用于演奏Note序列。
(ns examples.datatypes.midi
  (:import [javax.sound.midi MidiSystem]))

(defprotocol MidiNote
  (to-msec [this tempo])
  (key-number [this])
  (play [this tempo midi-channel]))

(defn perform [notes & {:keys [tempo] :or {tempo 88}}]
  (with-open [synth (doto (MidiSystem/getSynthesizer).open)]
    (let [channel (aget (.getChannels synth) 0)]
      (doseq [note notes]
        (play note tempo channel)))))

(defrecord Note [pitch octave duration]
  MidiNote
  (to-msec [this tempo]
    (let [duration-to-bpm {1 240, 1/2 120, 1/4 60, 1/8 30, 1/16 15}]
      (* 1000 (/ (duration-to-bpm (:duration this))
                 tempo))))

  (key-number [this]
    (let [scale {:C 0, :C# 1, :Db 1, :D 2,
                 :D# 3, :Eb 3, :E 4, :F 5,
                 :F# 6, :Gb 6, :G 7, :G# 8,
                 :Ab 8, :A 9, :A# 10, :Bb 10,
                 :B 11}]
      (+ (* 12 (inc (:octave this)))
         (scale (:pitch this)))))

  (play [this tempo midi-channel]
    (let [velocity (or (:velocity this) 64)]
      (.noteOn midi-channel (key-number this) velocity)


;演奏大白鲨的插曲
(def jaws (for [duration [1/2 1/2 1/4 1/4 1/8 1/8 1/8 1/8]
                pitch [:E :F]]
            (Note. pitch 2 duration))) -> #'user/jaws

(perform jaws) -> nil

reify(使具体化)

reify宏用于实现接口或者协议的匿名对象。

(import '[examples.datatypes.midi MidiNote])
(let [min-duration 250 min-velocity 64 rand-note (reify MidiNote (to-msec [this tempo] (+ (rand-int 1000) min-duration)) (key-number [this] (rand-int 100)) (play [this tempo midi-channel] (let [velocity (+ (rand-int 100) min-velocity)] (.noteOn midi-channel (key-number this) velocity) (Thread/sleep (to-msec this tempo)))))] (perform (repeat 15 rand-note)))

你可能感兴趣的:(Programming Clojure笔记之六——协议和数据类型)