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)))
- Programming Clojure笔记之六——协议和数据类型
- Programming Clojure笔记之二——探索Clojure
- Programming Clojure笔记之三——使用序列
- Programming Clojure笔记之四——函数式编程
- Programming Clojure笔记之五——状态
- Programming Clojure笔记之七——宏
- Programming Clojure笔记之八——多重方法
- Programming Clojure学习笔记——探索Clojure
- Programming Clojure学习笔记——探索Clojure
- Programming Clojure学习笔记——探索Clojure
- Programming Clojure学习笔记——探索Clojure
- Programming Clojure学习笔记——探索Clojure
- Programming Clojure学习笔记——探索Clojure
- Programming Clojure学习笔记——前言
- Programming Clojure学习笔记——开始
- Programming Clojure学习笔记——开始
- Programming Clojure学习笔记——开始
- Programming Clojure学习笔记——并发
- DOM对象
- 带你快速了解EDIUS各版本序列号的内容
- tomcat8和tomcat8之前的乱码问题解决方法
- Java集合类
- Haproxy+keepalived实现高可用负载均衡
- Programming Clojure笔记之六——协议和数据类型
- iOS利用九切片进行切图UI不会变形
- 让小米路由官方DDNS功能也支持二级路由
- 曾经,我们有一个芝麻大小的梦想
- (转载)从Apache Kafka 重温文件高效读写
- 如何优化 Android Studio 启动、编译和运行速度?
- In-memory Computing with SAP HANA读书笔记 - 第四章:SAP HANA integration scenarios
- 选择文件
- 给UIView 设置透明度,不影响其他sub views