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

限时加码!20+主流AI编程工具免费用 购周边加赠Coding Plan Lite,Claude Code、Cursor等即刻畅享,学习进阶更高效! 阅读详情

抽象是代码重用的基础。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)))
H3C模拟器HCL防火墙路由模式主备配置实验 摘要:本文详细介绍了网络拓扑规划与设备配置方案。运营商仅提供一个接口地址,通过虚IP技术实现双防火墙高可用部署:主备防火墙分别配置10.20.10.1/2910.20.10.2/29作为实际上联地址,共享10.10.10.2/24虚IP;下联配置172.168.10.0/24网段,核心交换机配置VLAN接口及192.168.10.1、192.168.20.1两个下联接口。文章包含运营商路由器、防火墙(HA配置、VVRP设置)核心交换机的具体命令行配置步骤,并强调需进行主备切换验证。整个方案实现了单运营商 阅读详情

相关推荐

别再只看成交量了!用通达信这个VOL监测公式,一眼识别主力吸筹与出货信号

本文详细介绍了通达信VOL监测公式的实战应用,通过彩色量柱换手率分层技术,帮助投资者识别主力吸筹与出货信号。该公式结合动态量能标准化多周期均线组合,提供可视化主力行为图谱,提升交易决策的准确性。

weixin_28693447的博客 609

Clojure编程乐趣:多态表达式

(defprotocol Connectable (my-cat [this other])) => Connectable #_ 扩展 List 类型 (extend-type java.util.List Connectable (my-cat [this other] (println "List my-cat") (concat this other))) => nil (my-cat [1 2 3] [4 5 6]) List my-cat => (1

ningDr的博客 291

OpenGL 进阶系列10 - 光线跟踪实现简介(Ray Tracing)

OpenGL 实现光线跟踪(Ray Tracing)是一个相对复杂但有趣的挑战,通常用于生成高度逼真的图像效果,如反射、折射、阴影等。OpenGL 本身并不是为光线跟踪设计的,但可以通过结合计算着色器现代 OpenGL 功能实现光线跟踪。光线跟踪的基本思路是从相机(或视点)出发,发射光线并计算这些光线与场景中物体的交点。光线与场景中物体相交的计算。计算交点的光照(基于表面法线、光源位置、材质等)。反射折射的处理(如果物体是透明或反射的)。

GPU全栈博主 1493

The Joy of Clojure 笔记

The Joy of Clojure 跳转至: 导航、 搜索 目录 1 基础2 Data types3 FP4 large-scale design 4.1 Macros4.2 Combining data and code4.3 Java.next4.4 Mutation 5 Tangential considera

cteng的专栏 1579

clojure实战——IO(1)

# clojure实战——IO(1) 本博客主要介绍clojure基本文件处理。除了clojure.core单元,还主要用到clojure.java.io单元。在实践本博客中的示例时,可以在repl中require相应命名空间: (require '[clojure.java.io :as cio]) (require '[clojure.edn :as edn])

蜗牛的专栏 1248

Clojure 为何不是面向对象的?

通常说来,学习新语言要在智力精力上都有极大的投入,只有程序员预期所学语言能够物有所值,这样的投入才是公平的。Clojure 出自 Rich Hickey 的手笔,他试图规避使用传统面向对象技术管理可变状态带来的诸多复杂性:既有本质的,也有偶然的。凭借对程序设计语言严肃研究而进行的贴心设计,以及对实用性的热切追求,Clojure 逐渐发展为一门重要的程序设计语言。它正扮演起一个不可忽视的重要角色,体

寻觅答案 653

为什么我喜欢Lisp语言(clojure)

时间2014-03-16 17:47:22 IT技术博客大学习 原文  http://blogread.cn/it/article/4471?f=hot1 这篇文章是我在 Simplificator ――我工作的地方――的一次座谈内容的摘录,座谈的题目叫做“为什么我喜欢Smalltalk语言Lisp语言”。在此之前,我曾发布过一篇叫做“ 为什么我喜欢Smalltalk? ”的文

Xiao_Qiang_的专栏 2200

如何写出极致优雅的Clojureprogramming-clojure的惰性求值与trampoline优化技巧

**programming-clojure** 是经典书籍《Programming Clojure》(第二版,Stuart Halloway 与 Aaron Bedra 著)的官方示例代码仓库,汇集了数十个可运行的 Clojure 示例。其中**惰性求值**(lazy sequences)与 **trampoline 优化**最能体现 Clojure 的优雅写法:让数据“用到才算”,让递归永不撑爆

gitblog_00675的博客 699

Clojure调Java互操作速查手册:programming-clojure的interop与import_static实例精讲

Clojure时,很多人卡在同一个地方:**Clojure怎么调用Java?** 本文基于《Programming Clojure》官方示例仓库 **programming-clojure**,用最少的代码把互操作(Java Interop)常用技巧一次讲清,带你读懂 [interop.clj](https://link.gitcode.com/i/2fb89cd3581275ac496d9ac

gitblog_00309的博客 1084

Programming Clojure学习笔记——与Java一起工作

3.3  使用Clojure创建编译Java类 Clojure的所有对象都实现了Java的特定接口: 1. Clojure数据结构实现了Java集合接口 2. Clojure函数实现了RunnableCallable接口 Clojure可以在需要时很方便地在磁盘上生成一次性的代理或类,使用必要的一些Java代码片段。 创建Java代理 在Clojure中,可以使用代理来扩展

蓝猫的专栏 2815

为什么选择Clojure做并发?programming-clojure并发编程示例深度剖析

做并发编程为什么越来越多团队选择 Clojure?答案在于它用**不可变数据 + 四大并发原语**(Atom、Ref、Agent、Trampoline)取代了传统的锁共享可变状态。本文以《Programming ClojureClojure编程)》第二版的官方示例仓库 programming-clojure 为主线,带你深度剖析这些并发编程示例,看看没有一行"锁"的代码是如何做到线程安全的。

gitblog_00068的博客 71

5步掌握Clerk:Clojure开发者的终极实时编程指南

Clerk是一款专为Clojure设计的Moldable Live Programming工具,它能将标准的Clojure命名空间转化为交互式笔记本,为开发者提供本地优先的实时编程体验。无论你是数据分析师、科研工作者还是Clojure开发者,Clerk都能帮助你快速构建数据分析可视化工作流。 ## 为什么选择Clerk?传统笔记本的革新方案 传统计算笔记本如Jupyter存在诸多问题:代码难

gitblog_00460的博客 492

Programming Clojure笔记之二——探索Clojure

Clojure具有同像性(homoiconic),即代码与其数据的结构一致。 Clojure代码主要由以下形式(Forms)构成。这些形式可以理解为其他程序设计语言中的数据类型。 Form Example(s) Boolean true, false Character \a Keyword :tag, :doc List (1 2 3), (println “h

coderock 1068

Programming Clojure学习笔记——并发

6.5 使用Vars管理线程本地状态 当你调用def或defn创建一个动态变量时,只是创建了一个变量。 当你传一个初始值给def时,该初始值就成了def创建变量的根绑定(root binding),如: (def foo 10) 变量foo的根绑定被所有线程共享,验证如下: 当前线程中 user=> foo 10 新起的线程中 user=> (.start (Thread. (f

蓝猫的专栏 938

Clerk:Clojure 的本地优先笔记

**示例演示**:[Clerk Demos](https://github.clerk.garden/nextjournal/clerk-demo/) **文档**:[Book of Clerk](https://book.clerk.vision)

gitblog_00530的博客 688

Clojure Programming读书笔记(0)

Down the Rabbit Hole   1. REPL is short for Read, Eval, Print and Loop 2. Clojure代码都是基于表达式(expressions)的,这些表达式基于以下规则: list,例如(+ 1 2),代表着调用,表达式的值就是调用的返回结果。list中第一个值相当于操作符,剩下的都是参数符号,比如

星星灿灿的笔记 760

Programming Clojure学习笔记——多重方法

8.2 定义多重方法 定义多重方法使用defmulti: (defmulti name dispatch-fn) name为多重方法的名字,Clojure将针对方法的参数调用dispatch-fn来选择多重方法的一个特定的方法(实现)。 考虑前一节的my-print函数,它只有一个参数,即需要打印的东西,可以基于参数的类型选择特定的实现。因此dispatch-fn必须是一个能返回参数类

蓝猫的专栏 1438

Clojure测试最佳实践:用programming-clojure学会Cucumber风格测试与expectorate快照

programming-clojure 是《Programming Clojure》一书的官方示例代码库,内含一套可运行的 Clojure 测试示例。本文带你用这个真实项目学会 Clojure 测试最佳实践:clojure.test 的 Cucumber 风格(行为驱动)测试写法、`are` 数据表测试,以及 expectorate 快照式校验,帮你写出既简洁又可读的高质量测试。 ## 📦 项

gitblog_01195的博客 1406

从0到1构建Clojure Web应用:Compojure+Ring服务器与Cryptovault实战教程

本教程带你**从0到1构建一个Clojure Web应用**:基于《Programming Clojure》官方示例代码库 `programming-clojure`,用 Compojure + Ring 搭出带数据库的完整 Web 服务器,并实战 AES 加密存储 CryptoVault。零 Java 服务端经验也能跑通,照做即可。 ## 🚀 环境准备与项目结构 **一键安装步骤**:只需

gitblog_00189的博客 370

Programming Clojure学习笔记——探索Clojure

2.5  流控制 if Clojure的if计算第一个参数值,如果第一个参数值为true,则返回第二个参数值;如果为false,则返回nil或第三个参数值(如果有第三个参数) user=> (defn is-small? [number]               (if ( #'user/is-small? user=> (is-small? 50) "yes" user=>

蓝猫的专栏 1380

Programming Clojure笔记之一——入门

Clojure是一个基于JVM的动态语言,支持函数式编程。 https://clojure.org/下载最新版本,获得一个jar文件如clojure-1.8.jar。 直接运行java -jar clojure-1.8.jar开启REPL。 如果要运行clojure程序文件,如hello.clj,直接运行java -jar clojure-1.8.jar hello.clj。或者在repl中加载,如

coderock 696
上一篇: Programming Clojure笔记之五——状态
下一篇: Programming Clojure笔记之七——宏
cwt8805
博客等级 码龄16年 43粉丝 42原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值