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

来源:互联网 发布:js数组去重的方法 编辑:程序博客网 时间:2024/05/18 03:26

抽象是代码重用的基础。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 (-> srcFileInputStream.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 (-> dstFileOutputStream.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)))
0 0
原创粉丝点击