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)))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值