夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Clojure勉強日記(その27 データ型(その1

徐々に抽象度・・・というか単体では動かしにくいコードが揃ってきましたが、
とりあえず続けます。

前回はプロトコルを使って既存の型に新たなメソッドを追加する方法を見てきました。
次は、Clojureで新たな型を作りたくなったらどうするか?を実現する「データ型」を見てみます。

実際に試してみるのはCryptoFilterというデータ型とします。
CryptoFilterはインスタンスを生成するときに暗号化ストアへのパスとパスワードを渡し、
CryptoFilterに書き込まれたデータは暗号化して保存され、
CryptoFilterから取得するデータは複合化して取得される・・・というものとします。

・・・明らかに「データ」と聞いて想像するようなエンティティではないんですが、
とりあえずここではClojureの「データ型」のため、気にせず進めます。

まず、CryptoFilterは以下のコードで定義可能です。

=> (deftype CryptoFilter[filename keystore password])
user.CryptoFilter

定義された型は型と同じ関数名を実行することでインスタンス化可能です。
フィールドにもJavaメソッド呼び出しを行うように「.【フィールド名】」アクセス可能になっています。

=> (def crypt-filter (CryptoFilter. "filter-file" "keystore" "password"))
#'user/crypt-filter
=> (class crypt-filter)
user.CryptoFilter # CryptoFilter のインスタンスとして具現化していることを確認
=> (.filename crypt-filter)
"filter-file"
=> (.keystore crypt-filter)
"keystore"
=> (.password crypt-filter)
"password"
=> (str crypt-filter)
"user.CryptoFilter@3e9513b7" # オブジェクトのデフォルトtoStringメソッドの結果が返ってくることを確認

では、このデータ型にメソッドによる挙動を追加しましょう。
データ型はプロトコルかインタフェースで指定されたメソッドのみ実装可能となっています。
・・・というのは今までとは異なる形ではありますね。

まずはFilterというプロトコルを作成して、初期化、OutputStream取得、InputStream取得のメソッドを持たせます。

(defprotocol Filter
  (init-filter [filter])
  (filter-output-stream [filter])
  (filter-input-stream [filter]))

で、実際にコードに組み込んだものがこれです。

  • src/reader/io.clj
(ns reader.io)

(defprotocol IOFactory
  "汎用の読込み/書込みを定義したプロトコル"
  (makeReader  [this] "Creates a BufferedReader.")
  (makeWriter  [this] "Creates a BufferedWriter."))
  • src/reader/crypto.clj
(ns reader.crypto
  (:use [reader.io :only [IOFactory makeReader makeWriter]])
  (:require [clojure.java.io :as io])
  (:import (java.security KeyStore KeyStore$SecretKeyEntry KeyStore$PasswordProtection)
           (javax.crypto KeyGenerator Cipher CipherOutputStream CipherInputStream)
           (java.io FileOutputStream)))

(defprotocol Filter
  (init-filter [filter])
  (filter-output-stream [filter])
  (filter-input-stream [filter]))


(deftype CryptoFilter [filename keystore password]
  Filter
  (init-filter [filter] (str filter))
  
  (filter-output-stream [filter] (str filter))
  
  (filter-input-stream [filter] (str filter))
  
  IOFactory
  (makeReader [filter]
    (makeReader (filter-input-stream filter)))
  (makeWriter [filter]
    (makeWriter (filter-output-stream filter)))
  )

ですが、実際に実行しようとした際に問題が。

=> (load-file "src/reader/io.clj")
IOFactory
=> (load-file "src/reader/crypto.clj")
reader.crypto.CryptoFilter
=> (def test-filter (CryptoFilter. "filter-file" "keystore" "toomanysecrets"))
CompilerException java.lang.IllegalArgumentException: Unable to resolve classname: CryptoFilter, compiling:(NO_SOURCE_PATH:1:18) 
=> (def test-filter (reader.crypto/CryptoFilter "filter-file" "keystore" "toomanysecrets"))
CompilerException java.lang.RuntimeException: No such var: reader.crypto/CryptoFilter, compiling:(NO_SOURCE_PATH:1:12) 
=> (use reader.crypto/CryptoFilter as CryptoFilter)
CompilerException java.lang.RuntimeException: No such var: reader.crypto/CryptoFilter, compiling:(NO_SOURCE_PATH:1:1) 

こんな感じで、実際に作成したデータ型が読めませんでした(汗
一応、「(in-ns 'reader.crypto)」を実行して自分が所属するネームスペースを変えておけば以下のコードでデータ型を生成できます。

=> (in-ns 'reader.crypto)
#<Namespace reader.crypto>
=> (def test-filter (CryptoFilter. "filter-file" "keystore" "toomanysecrets"))
#'reader.crypto/vault

・・・なのですが、特定のデータ型を使う際にプログラムのネームスペースを変えるというやり方自体がないよなぁ、
Twitter上で困っていたところ・・・

と返信を頂いたので、試してみると・・・

=> (import 'reader.crypto.CryptoFilter)
reader.crypto.CryptoFilter
=> (def test-filter (CryptoFilter. "filter-file" "keystore" "toomanysecrets"))
#'user/test-filter

データ型をうまく読むことができました。
importを使うあたり、作成したデータ型は裏ではJavaクラスという扱いになっているように見えます。
というかClojure自体Javaのクラスにマッピングされるので、
Clojureのuseやrequire使う方が例外的なのかもしれませんが。

で、実際に動作を確認してみます。

=> (.init-filter test-filter)
"reader.crypto.CryptoFilter@36564290"
=> (str test-filter)
"reader.crypto.CryptoFilter@36564290"

と、実際にメソッドを呼び出すことができ、実行結果も実装した通りとなっていました。
この先は、実際に中身を書き変えて動作することを確認してみます。