Clojureでプロトコルをよく使う。defprotocol
で作ってextend-protocol
で各種の型に実装すると、いろんな入力値にスムーズに対応できる。例えば暗号化周りで、ハッシュや署名の算出のためにバイト配列が必要な際、プロトコルを活かしていい感じのAPIが提供できる。
(defprotocol Byteish
(->bytes ^bytes [input]))
(extend-protocol Byteish
String
(->bytes [input] (.getBytes input)))
じゃぁもともと入力がバイト配列なら何もしなくていいだろう。返り値の型ヒントとしては bytes
と書けるが、それはそのままでは厳密に型・クラスではないのでextend-protocol
では使えない。そのための技として、バイト配列のJVM内部表現([B
)でクラスを取得して、それに対してプロトコルを実装する。
(extend-protocol Byteish
(Class/forName "[B")
(->bytes [input] input))
基本データ型ならこれでしょうと思っていたが、衝撃にリント用で導入したclj-kondoに「Function name must be simple symbol but got: “[B”」と怒られる。なぜだ?
どうやらextend-protocol
でClass/forName
が動くのはただの偶然で、本来はシンボルじゃないとだめらしい。例えば複数のClass/forName
の実装をやろうとすると、エラーにまでなる
(extend-protocol Foo
(Class/forName "[B")
(foo [bs] bs)
(Class/forName "[J")
(foo [ls] ls))
;; Syntax error (IllegalArgumentException) compiling, Don't know how to create ISeq from: java.lang.Character
Clojure 1.11以前ならこの対応は、extend-protocol
マクロが展開するextend
呼び出しを自前で書くことだ。そこなら問題なくClass/forName
使える。
(extend (Class/forName "[B")
Foo
{:->foo (fn foo [bs] bs) })
嬉しい副作用もある。extend-protocol
で作られる関数はただのgensym
された名称でエラーになるとわかりにくい。一方で直でextend
書くと関数名が普通に指定できるので、ちょっとわかりやすくなる。
Clojure 1.12なら配列型の文法が新設されたので(Class/forName "[B")
のところをbyte/1
(バイトの一次元配列)と書けるようになって、1.12以降対応でよければextend-protocol
でも問題ない。