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-protocolClass/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でも問題ない。