M.Hiroi's Home Page

Clojure Programming

お気楽 Clojure プログラミング超入門


Copyright (C) 2025 Makoto Hiroi
All rights reserved.

関数の合成とトランスデューサー

今回は Clojure の関数型言語らしい機能として、「関数の合成」と「トランスデューサー」について説明します。

●関数の合成

関数 f(x) と g(x) を合成して新しい関数 h(x) を作ることを考えてみましょう。関数 h(x) を次のように定義します。

h(x) = f( g( x ) )

たとえば、f(x) = 2 * x + 1, g(x) = x * x + 3 * x とすると、h(x) は次のようになります。

h(x) = f( g( x ) )
     = 2 * (x * x + 3 * x) + 1
     = 2 * x * x + 6 * x + 1

実際のプログラムは数式を展開するのではなく、g(x) の評価結果を f(x) に渡すだけなので簡単です。第 1 引数と第 2 引数に関数 f と g を受け取り、それらを合成する関数 compose は次のようになります。

user=> (defn compose [f g] #(f (g %)))
#'user/compose

簡単な例を示しましょう。

user=> (defn f [x] (inc (* 2 x)))
#'user/f
user=> (defn g [x] (+ (* x x) (* 3 x)))
#'user/g

user=> (f (g 4))
57
user=> (def h (compose f g))
#'user/h
user=> (h 4)
57

関数 f と g を定義します。f と g の合成は f( g( x ) ) と表すことができます。実際に 4 を計算すると 57 になります。この関数は compose で合成することができます。(compose f g) の返り値を変数 h に束縛すると、h を合成関数として使うことができます。

clojure には compose と同じ働きをする関数 comp が用意されています。

(comp f g)
(comp f g & fs)

comp は 2 個以上の関数を合成することができます。簡単な例を示しましょう。

user=> (def h1 (comp f g))
#'user/h1

user=> (h1 4)
57

●Threading Macros

ところで、Clojure には Threading Macros という便利な機能があり、(f (g x)) という関数呼び出しを次のように表すことができます。

(-> x g f)
user=> (-> 4 g f)
57

-> はマクロ (Threading first Macro) で、次の S 式と同じ働きをします。

(-> item (form1 x1 y1 ...) (form2 x2 y2 ...) (form3 x3 y3 ...))
 ≡(form3 (from2 (form1 item x1 y1 ...) x2 y2 ...) x3 y3 ...)

item の評価結果が、次の form1 の第 1 引数に渡され、その評価結果が次の form2 の第 1 引数に渡されます。これを最後まで続けていき、最後の form の評価結果が -> の返り値になります。関数は左から右へ評価されていくので、関数合成 comp の引数とは逆順になることに注意してください。

-> は reduce の動作と似ています。簡単な例を示しましょう。

user=> (-> 0 (+ 1) (+ 2) (+ 3) (+ 4) (+ 5))
15

user=> (-> 0 inc inc inc inc inc)
5

Clojure には評価結果を form の最後の引数に渡すマクロ ->> (Threading last Macro) や、挿入位置を名前で指定するマクロ as-> もあります。

(as-> expr name (form1 ...) (form2 ...) ...)

as-> の場合、expr の評価結果が name の変数に束縛され、(form1 ...) の中でそれを参照することができます。そして、form1 の評価結果が name の変数に束縛され、次の form2 に渡されます。

簡単な例を示しましょう。

user=> (->> '(5 6 4 7 3 8 2 9 1) (map #(- % 5)) (filter pos?))
(1 2 3 4)

user=> (as-> '(5 6 4 7 3 8 2 9 1) xs (map #(- % 5) xs) (filter pos? xs))
(1 2 3 4)

このほかにも、Clojure には便利な Threading Macros が用意されています。興味のある方は Clojure のリファレンスをお読みくださいませ。

●トランスデューサー (transducer)

トランスデューサー (transducer) には「変換器」という意味があります。Clojure の場合、入力されたデータを変換する特別な関数のことをトランスデューサーといいます。Clojure の場合、コレクションなどを操作する関数において、入力データを表す引数を省略すると、トランスデューサーを返す関数が用意されています。なお、Clojure ではトランスデューサーのことを xf とか xform と記述することがあります。

(map #(* % %)) => 入力データを 2 乗する xform
(filter odd?)  => 入力データから奇数値を取り出す xform
(take 10)      => 入力データから要素を 10 個取り出す xform

Clojure の xform は関数 comp で簡単に合成できるのが特徴です。threading macro で高階関数を並べるよりも、xform を comp で合成したほうが、入力データを効率的に処理できる場合があります。

xform を comp で合成する場合、適用順序は左から右となり、通常の関数とは逆になることに注意してください。つまり、threading macro と同じ順番になります。

xform に入力データを適用するには、以下の関数を使います。

  1. transduce
  2. sequence
  3. into
  4. eduction

1 は畳み込み (reduce) の動作と似ています。

(transduce xform f coll)
(transduce xform f init coll)

引数 f は reduce に渡す関数と同じで、これを reducing 関数といいます。transduce は引数 coll を xform と f を合成した関数で畳み込みを行います。簡単な例を示しましょう。

user=> (def integers (range 1 ##Inf))
#'user/integers
user=> (take 10 integers)
(1 2 3 4 5 6 7 8 9 10)

user=> (reduce + 0 (take 10 integers))
55
user=> (transduce (take 10) + 0 integers)
55

user=> (reduce + 0 (take 10 (map #(* % %) integers)))
385
user=> (transduce (comp (map #(* % %)) (take 10)) + 0 integers)
385

user=> (reduce + 0 (take 10 (map #(* % %) (filter odd? integers))))
1330
user=> (transduce (comp (filter odd?) (map #(* % %)) (take 10)) + 0 integers)
1330

結果をシーケンスとして受け取りたい場合は 2 の sequence を使います。

(sequence xform coll)
(sequence xform coll & colls)

基本的には xform 用のマップ関数みたいな動作になります。簡単な例を示しましょう。

user=> (sequence (take 10) integers)
(1 2 3 4 5 6 7 8 9 10)
user=> (sequence (comp (map #(* % %)) (take 10)) integers)
(1 4 9 16 25 36 49 64 81 100)
user=> (sequence (comp (filter odd?) (map #(* % %)) (take 10)) integers)
(1 9 25 49 81 121 169 225 289 361)

user=> (sequence (map list) (range 1 11) (range 11 21))
((1 11) (2 12) (3 13) (4 14) (5 15) (6 16) (7 17) (8 18) (9 19) (10 20))
user=> (sequence (comp (map list) (take 5)) (range 1 11) (range 11 21))
((1 11) (2 12) (3 13) (4 14) (5 15))

3 の into は xform を使用することができます。

(into to xform from)

into はコレクション from の要素に xform を適用し、その結果をコレクション to に格納して返します。簡単な例を示しましょう。

user=> (into '() (take 10) integers)
(10 9 8 7 6 5 4 3 2 1)
user=> (into '(0 -1 -2) (take 10) integers)
(10 9 8 7 6 5 4 3 2 1 0 -1 -2)

user=> (into [] (take 10) integers)
[1 2 3 4 5 6 7 8 9 10]
user=> (into [0 -1 -2] (take 10) integers)
[0 -1 -2 1 2 3 4 5 6 7 8 9 10]

user=> (into '() (take 10) integers)
(10 9 8 7 6 5 4 3 2 1)
user=> (into [] (comp (map #(* % %)) (take 10)) integers)
[1 4 9 16 25 36 49 64 81 100]

最後の eduction はちょっと変わっています。

(eduction xform ... coll) => clojure.core.Eduction

eduction は Eduction 型のデータを直ぐに返します。Eduction 型は reduce や反復処理で使用することができます。eduction は複数の xform を受け取ることができます。comp で合成する必要はありません。評価する順番は comp と同じく左から右です。

eduction を評価するとき、xform は coll の要素にまだ未適用であることに注意してください。reduce や反復処理などで Eduction の値が必要になったとき、はじめて coll に xform が適用されて値が求まります。また、遅延評価と違って、求めた値はキャッシュされません。値が必要になるたび、その都度計算が行われます。

簡単な例を示しましょう。

user=> (def xf (comp (filter (fn [x] (println "filter" x) (odd? x)))
 (map (fn [x] (println "map" x) x))))
#'user/xf
user=> (def xs (eduction xf (range 10)))
#'user/xs
user=> xs
(filter 0
filter 1
map 1
filter 2
filter 3
map 3
filter 4
filter 5
map 5
filter 6
filter 7
map 7
filter 8
filter 9
map 9
1 3 5 7 9)

user=> (reduce + 0 xs)
filter 0
filter 1
map 1
filter 2
filter 3
map 3
filter 4
filter 5
map 5
filter 6
filter 7
map 7
filter 8
filter 9
map 9
25

user=> (doseq [x xs] (println x))
filter 0
filter 1
map 1
filter 2
filter 3
map 3
filter 4
filter 5
map 5
filter 6
filter 7
map 7
filter 8
filter 9
map 9
1
3
5
7
9
nil

●トランスデューサーの作成

トランスデューサーの実体は、reducing 関数を受け取り、reducing 関数を返す関数となります。ただし、2 引数の関数 (fn [a x] ...) を返すだけでは駄目で、引数なしと引数が 1 つの関数も定義する必要があります。Clojure のドキュメント トランスデューサー(transducer) (翻訳) より引用します。

(fn [rf]
  (fn ([] ...)
      ([result] ...)
      ([result input] ...)))
  1. Init (arity 0) - ネストされた変換rfの init arity を呼び出す必要があります。これは最終的に変換プロセスを呼び出します。
  2. ステップ(アリティ2) - これは標準的なリダクション関数ですが、トランスデューサー内で必要に応じてrfステップアリティを0回以上呼び出すことが想定されています。例えば、filterは述語に基づいてrfを呼び出すかどうかを選択します。mapは常にrfを1回だけ呼び出します。catは入力に応じてrfを複数回呼び出す場合があります。
  3. 完了(アリティ1) - 一部のプロセスは終了しませんが、終了するプロセス(transduceなど)では、完了アリティを使用して最終値またはフラッシュ状態を生成します。このアリティは、 rf完了アリティを1回だけ呼び出す必要があります。

1 は畳み込みで初期値が与えられなかった場合です。(rf) の評価結果が初期値になります。2 は通常の畳み込みで呼び出される処理です。3 は入力データがなくなったときの処理です。xform は連鎖していることがあるので、引数 rf の関数に result を渡して処理することになります。

簡単な例として map のトランスデューサーを作ってみましょう。簡単にするため、コレクションは一つだけ受け取ることにすると、プログラムは次のようになります。

リスト : map (トランスデューサー)

(defn my-map [f]
  (fn [rf]
    (fn ([] (rf))
        ([result] (rf result))
        ([result input] (rf result (f input))))))

引数無しは (rf) を評価し、引数 1 つの場合は (rf result) を評価します。引数 2 つの場合、入力データ input に関数 f を適用し、それを result を rf に渡して呼び出します。これでマッピングの動作になります。簡単な実行例を示します。

user=> (sequence (my-map #(* % %)) (range 1 11))
(1 4 9 16 25 36 49 64 81 100)
user=> (transduce (my-map #(* % %)) + 0 (range 1 11))
385
user=> (transduce (my-map #(* % %)) + (range 1 11))
385

user=> (sequence (comp (filter odd?) (my-map #(* % %))) (range 1 11))
(1 9 25 49 81)
user=> (transduce (comp (filter odd?) (my-map #(* % %))) + 0 (range 1 11))
165

正常に動作していますね。

●畳み込みを途中で終了する

Clojure には畳み込みを途中で終了する機能が用意されています。

関数 reduce で畳み込みの処理を途中で終了するのは簡単です。reducing 関数の中で reduced を呼び出すだけです。簡単な実行例を示します。

user=> (reduce (fn [a x] (if (neg? x) (reduced :neg) (+ a x))) '(1 2 3 4 5))
15

user=> (reduce (fn [a x] (if (neg? x) (reduced :neg) (+ a x))) '(1 2 -3 4 5))
:neg

要素 x が負ならば、reduced で :neg を返します。負の要素が無ければ合計値を返します。

トランスデューサーの場合、halt-when を使うと途中で処理を終了することができます。

halt-when pred
halt-when pred retf

halt-when はトランスデューサー版の reduced と考えてください。halt-when は pred が真を返す要素を見つけると、処理を中断して、その要素をすぐに返します。引数 retf は 2 引数の関数で、最初が今までに計算された値、第 2 引数は pred が偽を返す要素になります。

簡単な実行例を示します。

user=> (transduce (halt-when neg?) + 0 '(1 2 3 4 5))
15
user=> (transduce (halt-when neg?) + 0 '(1 2 -3 4 5))
-3
user=> (transduce (halt-when neg? #(list %1 %2)) + 0 '(1 2 -3 4 5))
(3 -3)
user=> (transduce (halt-when neg? #(list %1 %2)) + 0 '(1 2 3 4 -5))
(10 -5)

●チャネルとトランスデューサー

ライブラリ core.async のチャネルはトランスデューサーを指定することができます。

(chan buf-or-n xform)
(chan buf-or-n xform ex-handler)

xform を使用する場合、チャネルにバッファを設定する必要があります。ex-handlerは 1 つの引数を持つ関数で、xform で例外が送出された場合に呼び出され、ex-handler の返り値に置き換えられます。

簡単な実行例を示します。

user=> (require '[clojure.core.async :as a])
nil
user=> (def c (a/chan 4 (map #(* % %))))
#'user/c

user=> (a/>!! c 1)
true
user=> (a/>!! c 2)
true
user=> (a/>!! c 3)
true
user=> (a/>!! c 4)
true

user=> (a/<!! c)
1
user=> (a/<!! c)
4
user=> (a/<!! c)
9
user=> (a/<!! c)
16

user=> (def c1 (a/chan 1 (map (fn [x] (if (neg? x) 
(throw (Exception. "xform error")) (* x x)))) (fn [e] (.getMessage e))))
#'user/c1
user=> (a/>!! c1 10)
true
user=> (a/<!! c1)
100
user=> (a/>!! c1 -1)
true
user=> (a/<!! c1)
"xform error"

xform に mapcat を指定すると、シーケンスの要素をチャネルに送信することができます。簡単な実行例を示します。

user=> (def c2 (a/chan 4 (mapcat identity)))
#'user/c2
user=> (a/>!! c2 (range 4))
true

user=> (a/<!! c2)
0
user=> (a/<!! c2)
1
user=> (a/<!! c2)
2
user=> (a/<!! c2)
3

トランスデューサーの基本的な機能を簡単に説明しました。トランスデューサーにはもっと高度な使い方もあると思いますが、Clojure 初心者 (M.Hiroi も含む) にはちょっと難しいかもしれません。トランスデューサーはとても興味深い機能なので、高度な使い方は今後の課題にしたいと思います。


Java のクラスの使い方

Clojure は JVM 上で動作するため、Java のクラスやライブラリを利用することができます。また、Java から Clojure の関数を呼び出すこともできるようです。今回は Clojure から Java のクラス (インターフェース) を利用する方法について簡単に説明します。

●Java のクラスを使用する

Java のクラスは import でロードします。

(import 'パッケージ.名前)
(import '(パッケージ 名前1 名前2 ...))

ns マクロのオプション :import を使っても同じことができます。クラス名を ClassName とすると、Java は new ClassName(...) でインスタンスを生成しますが、Clojure は次の構文で生成します。

(ClassName. ...) => object

クラス名の後ろにドット ( . ) を付けるとコンストラクタを呼び出すことができます。

Java のインスタンスメソッド methodName は object.methodName(...) で呼び出しますが、Clojure では次の形式で呼び出します。

(.methodName object ...)

methodName の前にドット ( . ) を付け、第 1 引数に object を、それ以降にメソッドの引数を指定します。

Java の場合、インスタンス object のフィールド変数 field は object.field, object.field = value で読み書きできますが、Clojure では次の形式でアクセスします。

(.-field object)
(set! (.-field object) new-value)

変数名の前に '-' を付けることをお忘れなく。

Java のクラスメソッド (スタティックメソッド) methodName は ClassName.methodName(...) で呼び出しますが、Clojure では次の形式で呼び出します。

(ClassName/methodName ...)

ClassName と methodName を / でつなげます。スタティック変数 VarName は ClassName/VarName でアクセスすることができます。

これらの機能は今までにも使ったことがあるので、難しいところはないと思います。

●簡単な実行例

簡単な実行例を示しましょう。Java で挨拶を表示するクラス Hello を定義します。

リスト : クラス Hello (hellojava/Hello.java)

package hellojava;

public class Hello {
    public String name;

    public Hello (String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public String greeting () {
        return "Hello, " + this.name;
    }
}

サブディレクトリ hellojava にソースファイル Hello.java を格納してコンパイルします。

$ javac hellojava/Hello.java
$ ls hellojava
Hello.class  Hello.java

カレントディレクトリが classpath に含まれていれば、Clojure から次のようにインポートすることができます。

user=> (import 'hellojava.Hello)
hellojava.Hello

user=> (def a (Hello. "Clojure"))
#'user/a

user=> (.greeting a)
"Hello, Clojure"

user=> (let [a (Hello. "Clojure")] (.greeting a))
"Hello, Clojure"

user=> (.-name a)
"Clojure"

user=> (set! (.-name a) "Java")
"Java"

user=> (.-name a)
"Java"

user=> (.greeting a)
"Hello, Java"

Hello. でインスタンスを生成し、.greeting でメソッド greeting を呼び出すことができます。フィールド変数 name は mutable なので、値を書き換えることができます。

●proxy

proxy は Java のクラスを継承、または Java のインターフェースを実装した匿名 (無名) クラスのインスタンスを生成します。

(proxy [class-or-interface] [args ...] & fs)

args は継承するクラスのコンストラクタに渡す引数です。引数がない場合は空のベクタになります。fs は実装する関数 (メソッド) です。以下のように定義します。

(name [args ...] body)
(name ([args1] body1) ([args1 args2] body2) ...)

マルチアリティ関数も定義することができます。

それでは簡単な例題として、クラス Hello のメソッド greeting の表示を、Hello, から Hey! に変更することを考えてみましょう。proxy を使うと次のようになります。

user=> (def b (proxy [Hello] ["Clojure"] (greeting [] (str "Hey! " (.getName this)))))
#'user/b

user=> (.greeting b)
"Hey! Clojure"

user=> (let [b (proxy [Hello] ["Clojure"] (greeting [] (str "Hey! " (.getName this))))]
(.greeting b))
"Hey! Clojure"

メソッドの中の this は自分自身のインスタンスを表します。たとえば、(.greeting b) を評価したとき、第 1 引数 b の値が変数 this に束縛されます。

スーパークラスのメソッドを呼び出すには proxy-super を使います。

(proxy-super method-name & args)

たとえば、greeting の表示で末尾に !! を追加する場合は次のようになります。

user=> (def c (proxy [Hello] ["Clojure"] (greeting [] (str (proxy-super greeting) "!!"))))
#'user/c

user=> (.greeting c)
"Hello, Clojure!!"

user=> (let [c (proxy [Hello] ["Clojure"] (greeting [] (str (proxy-super greeting) "!!")))]
(.greeting c))
"Hello, Clojure!!"

●reify

reify は Java のインターフェース、または Clojure のプロトコルを実装するときに使います。匿名クラスを定義して、そのインスタンスを生成するところは proxy と似ています。reify は Java のクラスを指定することはできませんが、例外として Java のクラス Object は指定することができます。

(reify 
  p-name1
  (method-name11 [this ...] body)
    ・・・
  (method-name1Z [this ...] body)

    ・・・ 

  p-nameZ
  (method-nameZ1 [this ...] body)
    ・・・
  (method-nameZZ [this ...] body)
)

reify の構文は、プロトコルを使って関数を定義するときの defrecord と似ています。p-name にプロトコル、interface、Object を指定し、そこに定義されているメソッドを実装します。メソッドの第 1 引数 (this) には、自分自身のインスタンスを渡します。なお、reify で実装したメソッドはクロージャと同様に、そのとき有効な局所変数を参照することができます。

簡単な例を示しましょう。

user=> (defprotocol hello (getName [this]) (greeting [this]))
hello

user=> (def a (reify hello (getName [this] "Clojue") 
(greeting [this] (str "Hello, " (getName this)))))
#'user/a

udrt~> (getName a)
"Clojue"

user=> (greeting a)
"Hello, Clojue"

user=> (let [a (reify hello (getName [this] "Clojue")
(greeting [this] (str "Hello, " (getName this))))]
(.greeting a))
"Hello, Clojue"

●gen-class

proxy や reify は匿名クラスを作りましたが、Clojure 側でも名前付きのクラスを定義したい場合もあるでしょう。このような場合、マクロ gen-class を使います。

(gen-class & options)

gen-class は ns マクロのオプション :gen-class でも指定することができます。gen-class はクラスの仕様を定義するだけで、クラスを生成するわけではありません。gen-class を含む Cllojure のソースファイルを関数 compile でコンパイルして、Java の class ファイルを生成する必要があります。class ファイルは Clojure の大域変数 *compile-path* にセットされているパスに出力されます。

$ clj
Clojure 1.12.0
user=> *compile-path*
"classes"

M.Hiroi の環境ではサブディレクトリ classes に設定されています。コンパイルする前にディレクトリ classes を作っておいてください。また、classes は Clojure の classpath に含まれている必要があります。そうしないと、Clojure が class ファイルを見つけることができません。M.Hiroi はカレントディレクトリにある設定ファイル deps.edn の :src に "classes" を追加しました。

リスト : deps.edn

{
 :paths ["." "classes"]
 :deps { org.clojure/core.async {:mvn/version "1.8.741"} }
}

簡単な設定ですが、これで class ファイルを生成して、それを import で読み込むことができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : gen-class の使用例 (Foo.clj)

(ns Foo (:gen-class))

(defn -main [] (println "Hello, Foo"))

ns のオプションに :gen-class を指定しただけですが、これだけでクラスを定義することができます。この場合、クラス名は名前空間と同じ Foo になり、Foo は Java のクラス Object を暗黙のうちに継承します。関数 -main は Java の main 関数と同じ働きをします。Java のコマンド java を使ってプログラムを実行することができます。

それでは実際に試してみましょう。

$ clj
Clojure 1.12.0
user=> (compile 'Foo)
Foo
user=> (import 'Foo)
Foo

user=> (Foo.)
#object[Foo 0x68e62ca4 "Foo@68e62ca4"]
$ java -cp `clj -Spath` Foo
Hello, Foo

Foo.clj をコンパイルして class ファイルを作成し、それを import で読み込みます。(Foo.) を評価すれば、Foo のインスタンスを生成することができます。

コマンド java を使う場合、クラスパスを指定するオプション -cp に、Clojure のクラスパスを指定してください。Clojure のクラスパスは clj -Spath で求めることができます。` ... ` は bash の機能で、コマンドの出力を文字列に展開します。

●クラス名の指定

クラス名はオプション :name で指定することができます。

簡単な例を示しましょう。次のリストを見てください。

リスト : クラス名の指定 (bar.clj)

(ns bar
  (:gen-class :name bar.Bar))

(defn -main [] (println "Hello, bar.Bar"))

:name に bar.Bar を指定すると、Java のパッケージ bar が生成され、そこにクラス Bar が配置されます。具体的には、bar.clj をコンパイルすると classes にサブディレクトリ bar が作成され、そこに Bar.class が出力されます。

それでは実際に試してみましょう。

$ clj
Clojure 1.12.0
user=> (compile 'bar)
bar

user=> (import 'bar.Bar)
bar.Bar

user=> (Bar.)
#object[bar.Bar 0x29d37757 "bar.Bar@29d37757"]

user=> 

$ java -cp `clj -Spath` bar.Bar
Hello, bar.Bar

●継承とメソッド

クラスの継承はオプション :extends で、インターフェースの継承は :implements で指定します。

:extends aclass
:implements [interface ...]

Java は単一継承なので、:extends で指定できるスーパークラスは一つだけです。省略された場合、クラス Object が継承されます。Java の場合、複数のインターフェースを継承することができます。

メソッドを追加する場合はオプション :methods で仕様 (シグネチャ) を定義します。

:methods [ [name [param-types] return-type], ... ]

スーパークラスやインターフェースのプライベート以外のメソッドは自動的に定義されるので、:methods でシグネチャを定義する必要はありません。シグネチャはメソッド名 name、引数の型 param-types、返り値の型 return-type を指定します。インスタンスメソッドの場合、第 1 引数に自分自身を表すインスタンスが渡されますが、それを記述する必要はありません。静的メソッドを定義する場合は、シグネチャのメタデータ ^{:static true} で指定します。

実際のメソッドは名前空間の中で定義します。このとき、メソッド名 name の前に文字列 "-" を付けます。この値はオプション :prefix で指定することができます。

:prefix string

:prefix のデフォルトが "-" です。実際のメソッドは prefix + name という名前で検索されます。第 1 引数には自分自身のインスタンスが渡されるので、それを受け取る引数 (たとえば this など) を定義するようにしてください。

簡単な例を示しましょう。Java のクラス hellojava.Hello (hellojava/Hello.java) を継承したクラス helloclojure.Hello を作ってみましょう。次のリストを見てください。

リスト : 継承の使用例 (helloclojure.clj)

(ns helloclojure
  (:gen-class :name helloclojure.Hello
              :extends hellojava.Hello
              :methods [[goodbye [] String]]))

(defn -greeting [this] (str "Hey!, " (.getName this)))
(defn -goodbye [this] (str "Bye!, " (.getName this)))
(defn -main [] (println "helloclojure.Hello"))

greeting はスーパークラスのメソッドをオーバーライドします。goodbye は新しく追加するメソッドです。;gen-class オプションの :methods で goodbye のシグネチャを定義します。

簡単な実行例を示しましょう。

$ clj
Clojure 1.12.0
user=> (compile 'helloclojure)
helloclojure

user=> (import 'helloclojure.Hello)
helloclojure.Hello

user=> (def a (Hello. "Clojure"))
#'user/a

user=> (.greeting a)
"Hey!, Clojure"

user=> (.goodbye a)
"Bye!, Clojure"

メソッドをオーバーライドした場合、スーパークラスのメソッドを呼び出すことができると便利です。この場合、オプション :exposes-methods を使います。

:exposes-methods {super-method-name exposed-name, ...}

オーバーライドしたメソッドの中で (.exposed-name this ...) とすれば、スーパークラスのメソッド super-method-name を呼び出すことができます。

簡単な実行例を示しましょう。

リスト : exposes-methods の使い方 (helloclojure.clj)

(ns helloclojure
  (:gen-class :name helloclojure.Hello
              :extends hellojava.Hello
              :methods [[goodbye [] String]]
              :exposes-methods {greeting greetingSuper}))

(defn -greeting [this] (str (.greetingSuper this) "!!"))
(defn -goodbye [this] (str "Bye!, " (.getName this)))
(defn -main [] (println "helloclojure.Hello"))
$ clj
Clojure 1.12.0
user=> (compile 'helloclojure)
helloclojure

user=> (import 'helloclojure.Hello)
helloclojure.Hello

user=> (def a (Hello. "Clojure"))
#'user/a

user=> (.greeting a)
"Hello, Clojure!!"

●フィールド変数

gen-class はオプション :state でフィールド変数を定義することができます。

:state name

:state で指定した名前 name のフィールド変数を作成します。この変数は public で final です。初期化はオプション :init で指定した関数で行います。Java の場合、final 変数の値を変更することはできませんが、Clojure の atom や ref などを使うことで、mutable な変数を実現することができます。変数の値は (.name this) で取得することができます。

:init name

:init はコンストラクタの引数とともに呼び出される関数の名前を指定します。[ [スーパークラスのコンストラクタ引数] state] を返す必要があります。指定されていない場合、コンストラクタの引数はスーパークラスのコンストラクタに直接渡され、state は nil になります。

簡単な例を示します。

リスト : :state と ;init の使用例

(ns State
  (:gen-class :state state1
              :init init
              :methods [[getState [] String]
                        [setState [String] void]]))

(defn -init [] [[] (atom "")])
(defn -getState [this] @(.state1 this))
(defn -setState [this value] (reset! (.state1 this) value))
$ clj
Clojure 1.12.0

user=> (compile 'State)
State

user=> (import 'State)
State

user=> (def a (State.))
#'user/a

user=> (.getState a)
""

user=> (.setState a "Hello")
nil

user=> (.getState a)
"Hello"
user=>

この他にも、:gen-class にはいろいろなオプションが用意されています。興味のある方は Clojure のドキュメントをお読みくださいませ。


初版 2025 年 8 月 31 日, 9 月 19 日