M.Hiroi's Home Page

Clojure Programming

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


Copyright (C) 2025 Makoto Hiroi
All rights reserved.

マルチメソッド

一般的なオブジェクト指向言語では、メソッドは特定のクラス (データ型) と結びついた関数と考えることができます。メソッドは、同じ名前のメソッドをいくつも定義することができ、メソッドを呼び出すオブジェクトのデータ型によって、その中から実際に呼び出すメソッドを自動的に選択します。これを「ポリモーフィズム」といいます。Clojure では defrecord, deftype と defprotocol を使って、第 1 引数のデータ型によって適切なメソッドを選択することができます。

このように、引数をひとつ使ってメソッドを選択する方法を「単一ディスパッチ (Single dispatch)」といいいます。多くのオブジェクト指向言語はシングルディスパッチを採用しています。これに対し、複数の引数を使って、適切なメソッドを選択する方法もあります。これを「多重ディスパッチ (Multiple dispatch)」とか「マルチメソッド (Multimethods)」といいます。

多重ディスパッチを採用している言語はそれほど多くありませんが、有名なところでは Common Lisp Object System (CLOS) があります。最近の言語では、Julia が多重ディスパッチです。Clojure の場合、defmulti と defmethod を使うと、メソッドの選択に「マルチメソッド」を使うことができます。今回はマルチメソッドについて説明します。

●defmulti

CLOS では同一名のメソッドの集まりを「総称関数 (generic function)」と呼びます。CLOS は総称関数を defgeneric を使って定義します。Clojure の場合、defgeneric に相当するマクロが defmulti です。

defmulti name dispatch-func ...

引数 name がメソッド名、dispatch-func は引数をメソッド選択で使用する値に変換する関数です。この値をディスパッチ値と呼ぶことにします。メソッドの仮引数は dispatch-func の仮引数と同じになります。メソッド選択に複数のディスパッチ値を使う場合、それらの値をベクタに格納して返します。

たとえば、引数のデータ型でメソッドを選択したいのであれば関数 class や type を使うと簡単です。次の例を見てください。

1. (defmulti name class)                      ; (defmulti name (fn [x] (class x))) と同じ
2. (defmulti name #(vector (class %1) (class %2)))
3. (defmulti name (fn [x y z] [(class x) (class y)]))
4. (defmulti name (fn [x y & zs] [(class x) (class y)]))

1 は引数が一つの場合です。引数に class を適用してデータ型を求め、その値を使ってメソッドを選択します。2 は引数が複数あり、それらのデータ型を使ってメソッド選択する場合です。各々の引数のデータ型を求め、ベクタに格納して返します。3, 4 のように一部の引数を使ってメソッドを選択することもできます。4 のように、可変個引数を使うこともできます。

●defmethod

メソッドの定義にはマクロ defmethod を使います。

defmethod name dispatch-val [仮引数 ...] S式 ...

defmethod は defn と構造が似ていまが、名前 name と仮引数を格納するベクタの間に、ディスパッチ値 dispatch-val を指定するところが異なっています。メソッドを選択するとき、defmulti で指定した関数 dispatch-fun の返り値と dispatch-val を照合して、マッチングが成功するメソッドが選択されます。あとで説明しますが、ディスパッチ値は階層構造を持たせることができます。階層構造が無ければ、単純な等値 (=) でメソッドを選択します。

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

user=> (defrecord Foo [a])
user.Foo
user=> (defrecord Bar [a])
user.Bar
user=> (defrecord Baz [a])
user.Baz

user=> (defmulti oops class)
#'user/oops

user=> (defmethod oops Foo [this] (println "Foo" (:a this)))
#object[clojure.lang.MultiFn 0x205b132e "clojure.lang.MultiFn@205b132e"]
user=> (defmethod oops Bar [this] (println "Bar" (:a this)))
#object[clojure.lang.MultiFn 0x205b132e "clojure.lang.MultiFn@205b132e"]

user=> (def a (->Foo 1))
#'user/a
user=> (def b (->Bar 10))
#'user/b
user=> (def c (->Baz 100))
#'user/c

user=> (oops a)
Foo 1
nil
user=> (oops b)
Bar 10
nil
user=> (oops c)
Execution error (IllegalArgumentException) at user/eval213 (REPL:1).
No method in multimethod 'oops' for dispatch value: class user.Baz

レコード Foo, Bar, Baz を定義します。次に、defmulti でマルチメソッド oops を定義し、Foo と Bar のメソッド oops の実体を defmethod で定義します。そして、Foo, Bar, Baz の値を変数 a, b, c にセットして oops に適用します。(oops a) は Foo のメソッドを呼び出し、(oops b) は Bar のメソッドを呼び出します。(oops c) は Baz のメソッドが定義されていないのでエラーになります。

ここで、defmethod のディスパッチ値 dispatch-val に :default を指定すると、メソッドが見つからない場合は :default のメソッドが呼び出されます。次の例を見てください。

user=> (defmethod oops :default [this] (println "default" (:a this)))
#object[clojure.lang.MultiFn 0x205b132e "clojure.lang.MultiFn@205b132e"]

user=> (oops c)
default 100
nil

defmethod で :default のメソッドを定義します。そして、(oops c) を評価すると、そのメソッドが呼び出されます。

もう一つ簡単な例題として、2 次元の点 Point と 3 次元の点 Point3d と、距離を求めるメソッド distance をマルチメソッドで書き直してみましょう。次のリストを見てください。

リスト : Point と Pind3d (point1.clj)

;; 2 次元の点
(defrecord Point [x y])

;; 3 次元の点
(defrecord Point3d [x y z])

;; 距離を求めるメソッド
(defmulti distance #(vector (class %1) (class %2)))

;; 距離を求める
(defmethod distance [Point Point] [p1 p2]
  (let [{x1 :x, y1 :y} p1
        {x2 :x, y2 :y} p2
        dx (- x1 x2)
        dy (- y1 y2)]
    (Math/sqrt (+ (* dx dx) (* dy dy)))))

(defmethod distance [Point3d Point3d] [p1 p2]
  (let [{x1 :x, y1 :y, z1 :z} p1
        {x2 :x, y2 :y, z2 :z} p2
        dx (- x1 x2)
        dy (- y1 y2)
        dz (- z1 z2)]
    (Math/sqrt (+ (* dx dx) (* dy dy) (* dz dz)))))

最初に Point と Point3d を定義します。これは簡単ですね。次に、defmulti でマルチメソッド distance を定義します。dispatch-func の返り値は 2 つの引数のデータ型を格納したベクタです。最後に defmethod で distance の実体を定義します。最初の distance は 2 つの引数が Point の場合に呼び出されます。次の distance は 2 つの引数が Point3d の時に呼び出されます。2 つ引数の型が異なる場合、たとえば Point と Point3d の場合はエラーになります。

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

user=> (load-file "point1.clj")
#object[clojure.lang.MultiFn 0x21fdfefc "clojure.lang.MultiFn@21fdfefc"]

user=> (def p1 (->Point 0 0))
#'user/p1
user=> (def p2 (->Point 1 1))
#'user/p2

user=> (def p3 (->Point3d 0 0 0))
#'user/p3
user=> (def p4 (->Point3d 1 1 1))
#'user/p4

user=> (distance p1 p2)
1.4142135623730951
user=> (distance p3 p4)
1.7320508075688772
user=> (distance p1 p3)
Execution error (IllegalArgumentException) at user/eval209 (REPL:1).
No method in multimethod 'distance' for dispatch value: [user.Point user.Point3d]
user=> (distance p3 p2)
Execution error (IllegalArgumentException) at user/eval211 (REPL:1).
No method in multimethod 'distance' for dispatch value: [user.Point3d user.Point]

●データ型以外の値でメソッドを選択する

Clojure はメソッド選択でデータ型以外の値でも使用することができます。このとき、よく使われるデータがキーワードです。たとえば、図形の面積を求めるメソッド area をマルチメソッドで書き直してみましょう。次のリストを見てください。

リスト : 図形の面積を求める (figure1.clj)

;; 三角形
(defn make-triangle [a b]
  {:id :triangle, :altitude a, :base b})

;; 四角形
(defn make-rectangle [w h]
  {:id :rectangle, :width w, :height h})

;; 円
(defn make-circle [r]
  {:id :circle, :radius r})

;; マルチメソッド
(defmulti area :id)   ; (fn [x] (:id x)) と同じ

(defmethod area :triangle [this]
  (/ (* (:altitude this) (:base this)) 2.0))

(defmethod area :rectangle [this]
  (* (:width this) (:height this)))

(defmethod area :circle [this]
  (* (:radius this) (:radius this) Math/PI))

図形は defrecord を使わずに、マップにデータを格納します。このとき、図形の種別を表す :id をキーワードで指定します。関数 make-XXXX は図形 XXXX を表すマップを返します。次に defmulti でマルチメソッド area を定義します。dispath-fun は :id になります。これで引数から :id のデータ (図形の種別) を取り出すことができます。あとは defmethod を使って各図形のメソッド area を定義します。このとき、ディスパッチ値は図形を表すキーワードになります。

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

user=> (load-file "figure1.clj")
#object[clojure.lang.MultiFn 0x2571066a "clojure.lang.MultiFn@2571066a"]

user=> (def a (make-triangle 2.0 2.0))
#'user/a
user=> (def b (make-rectangle 2.0 2.0))
#'user/b
user=> (def c (make-circle 2.0))
#'user/c

user=> a
{:id :triangle, :altitude 2.0, :base 2.0}
user=> b
{:id :rectangle, :width 2.0, :height 2.0}
user=> c
{:id :circle, :radius 2.0}

user=> (area a)
2.0
user=> (area b)
4.0
user=> (area c)
12.566370614359172

user=> (reduce (fn [a x] (+ a (area x))) 0.0 [a b c])
18.566370614359172

きちんとポリモーフィズムが機能していますね。

●ディスパッチ値の階層構造

ディスパッチ値は関数 derive により親子関係を設定することができます。メソッド選択だけですが、一般的なオブジェクト指向言語の「継承」と似た動作を行わせることができます。

derive tag parent

derive の引数は名前空間付きシンボルか、名前空間付きキーワードでなければいけません。引数 tag は Java のクラスでもかまいませんが、親にすることはできません。

名前空間付きキーワードは :namespace/keyword で定義します。::keyword とすると、現在の名前空間にキーワード keyword が定義されます。:keyword と :name/keyword は異なる値になります。述語 = で比較すると false になります。

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

user=> ::foo
:user/foo
user=> :foo
:foo
user=> (= :foo ::foo)
false

(derive ::bar ::foo)
nil
user=> (isa? ::bar ::foo)
true
user=> (isa? ::foo ::bar)
false

user=> (isa? ::baz ::foo)
false
user=> (derive ::baz ::foo)
nil
user=> (isa? ::baz ::foo)
true

(derive ::bar ::foo) を評価すると、::bar は ::foo の子になります。isa? は親子関係を調べる述語です。(isa? ::bar ::foo) は真になりますが、(isa? ::foo ::bar) は偽になります。同様に (isa? ::baz ::foo) は偽ですが、(derive ::baz ::foo) を評価すると ::baz は ::foo の子になるので、(isa? ::baz ::foo) は真になります。

このように、親は一つだけで子が複数ある場合を、一般的なオブジェクト指向言語では「単一継承」といいます。なお、親子関係は何段階に渡って行われてもかまいません。::bar や ::baz の子を定義することもできます。次の例を見てください。

user=> (derive ::oops ::bar)
nil
user=> (derive ::oops ::baz)
nil

user=> (isa? ::oops ::Foo)
true
user=> (isa? ::oops ::bar)
true
user=> (isa? ::oops ::baz)
true

::oops は ::bar と ::baz を親に持ちます。複数の親を持つ場合、一般的なオブジェクト指向言語では「多重継承」といいます。::oops は ::foo, ::bar, ::baz の子なので、isa? で判定すると true になります。

isa? のほかに、直接の親を求める parents や、祖先を求める ancestors、子孫を求める descendants があります。

user=> (parents ::oops)
#{:user/baz :user/bar}
user=> (parents ::baz)
#{:user/foo}
user=> (parents ::bar)
#{:user/foo}

user=> (ancestors ::foo)
nil
user=> (ancestors ::oops)
#{:user/baz :user/bar :user/foo}
user=> (descendants ::Foo)
#{:user/baz :user/bar :user/oops}

●メソッドの選択

それでは具体的にどのようにメソッドが選択されるのか、プログラムを作って確認してみましょう。次のリストを見てください。

リスト : メソッドの選択 (sample_isa_clj)

(defn make-foo [a b]
  {:id ::foo, :a a, :b b})

(defn make-bar [a b c]
  {:id ::bar, :a a, :b b, :c c})

(derive ::bar ::foo)

(defmulti get-a :id)
(defmulti get-b :id)
(defmulti get-c :id)

(defmethod get-a ::foo [this] (:a this))
(defmethod get-b ::foo [this] (:b this))
(defmethod get-c ::bar [this] (:c this))

関数 make-foo と make-bar は、:id が ::foo と ::bar のマップを生成します。Clojure の場合、マップのフィールド変数を継承する機能はないので、make-bar では自分で初期化しています。また、次のように make-foo を呼び出して a, b を初期化することもできます。

(defn make-bar [a b c]
  (assoc (make-foo a b) :id ::bar :c c))

オブジェクト指向言語に似せるのであれば、このような方法も考えられると思います。

次に derive で ::bar と ::foo に親子関係を設定します。そして、マルチメソッド get-a, get-b, get-c を定義します。::bar には get-a, get-b を定義していませんが、::bar の親 ::foo のメソッド get-a, get-b が呼び出されるので、変数 a, b の値を求めることができます。

実行例を示します。

user=> (load-file "sample_isa.clj")
#object[clojure.lang.MultiFn 0x659a2455 "clojure.lang.MultiFn@659a2455"]

user=> (def x (make-foo 1 2))
#'user/x
user=> (def y (make-bar 10 20 30))
#'user/y

user=> (get-a x)
1
user=> (get-a y)
10
user=> (get-b x)
2
user=> (get-b y)
20

user=> (get-c y)
30
user=> (get-c x)
Execution error (IllegalArgumentException) at user/eval174 (REPL:1).
No method in multimethod 'get-c' for dispatch value: :user/foo

get-a, get-b は ::bar のデータ y に適用しても呼び出すことができます。::foo にメソッド get-c は定義されていなので、(get-c x) はエラーになります。

●メソッドの競合

親が複数存在していて、そこに同じ名前のマルチメソッドが定義されている場合、メソッドの競合が発生します。CLOS や Python などでは、継承関係からメソッドの優先順位を決定することができるので、競合が生じることはありません。Clojure の場合、どのメソッドを優先するかプログラマが指定する必要があります。

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

リスト : メソッドの競合 (sample_isa1.clj)

(defn make-foo [a]
  {:id ::foo, :a a})

(defn make-bar [a b]
  {:id ::bar, :a a, :b b})

(defn make-baz [a b]
  {:id ::baz, :a a, :b b})

(defn make-oops [a b c]
  {:id ::oops, :a a, :b b, :c c})

(derive ::bar ::foo)
(derive ::baz ::foo)
(derive ::oops ::bar)
(derive ::oops ::baz)

(defmulti get-a :id)
(defmulti get-b :id)
(defmulti get-c :id)

(defmethod get-a ::foo [this] (:a this))
(defmethod get-b ::bar [this] (println ::bar) (:b this))
(defmethod get-b ::baz [this] (println ::baz) (:b this))
(defmethod get-c ::oops [this] (:c this))

(def w (make-foo 1))
(def x (make-bar 10 20))
(def y (make-baz 100 200))
(def z (make-oops -1 -2 -3))

::oops は ::bar と ::baz という親を持ち、::bar と ::baz にはメソッド get-b が定義されています。::oops のデータに get-b を適用した場合、どちらのメソッドが呼び出されるのでしょうか。実際に試してみましょう。

user=> (load-file "sample_isa1.clj")
#'user/z

user=> (get-a z)
-1
user=> (get-c z)
-3

user=> (get-b z)
Execution error (IllegalArgumentException) at user/eval180 (REPL:1).
Multiple methods in multimethod 'get-b' match dispatch value: 
:user/oops -> :user/baz and :user/bar, and neither is preferred

このように、呼び出すメソッドを決定できずにエラーとなります。この場合、関数 prefer-method を使って優先するディスパッチ値を指定します。

prefer-method method-name dispatch-val-x dispatch-val-y

prefer-method はマルチメソッド method-name が競合したとき、dispatch-val-y よりも dispatch-val-x の一致を優先するように設定します。実際にメソッド get-b は ::bar を優先するように設定すると、次のようになります。

user=> (prefer-method get-b ::bar ::baz)
#object[clojure.lang.MultiFn 0x57540fd0 "clojure.lang.MultiFn@57540fd0"]

user=> (get-b z)
:user/bar
-2

user=> (get-b x)
:user/bar
20

user=> (get-b y)
:user/baz
200

(get-b z) はエラーとならずに、::bar のメソッド get-b が呼び出されます。(get-b x) と (get-b y) は、メソッドの競合が発生しないので、そのまま ::bar のメソッドと ::baz のメソッドが呼び出されます。


複素数

今回は Clojure で「複素数 (complex number)」の演算プログラムを作ってみましょう。数学では複素数 z を \(x + iy\) と表記します。x を実部、y を虚部、i を虚数単位といいます。虚数単位は 2 乗すると -1 になる数です。実部と虚部の 2 つの数値を格納するデータ構造を用意すれば、プログラミング言語で複素数を表すことができます。

●Clojure の実数

Clojure の数は大きく分けると、整数、実数、有理数 (分数) の 3 種類があります。その中で、実数は浮動小数点数 (floating point number) として表現されます。浮動小数点数には IEEE 754 という標準仕様があり、近代的なプログラミング言語のほとんどは、IEEE 754 に準拠した浮動小数点数をサポートしています。浮動小数点数はすべての小数を正確に表現することはできません。このため、実数は近似的な値になります。

IEEE 754 には通常の数値以外にも、負のゼロ (-0.0)、正負の無限大 (∞, -∞)、NaN (Not a Number, 非数) といった値が定義されています。これらの値は Clojure でも取り扱うことができます。負のゼロは -0.0、正負の無限大は ##Inf と ##-Inf、NaN は ##NaN と表記されます。

無限大は infinite? で、NaN は NaN? でチェックすることができます。負のゼロは、1.0 / 0.0 = ##Inf, 1.0 / -0.0 = ##-Inf になることを利用するとチェックすることができます。簡単な実行例を示します。

user=> -0.0
-0.0
user=> (zero? 0.0)
true
user=> (zero? -0.0)
true

user=> (/ 1.0 0.0)
##Inf
user=> (/ 1.0 -0.0)
##-Inf

user=> (infinite? ##Inf)
true
user=> (infinite? ##-Inf)
true
user=> (infinite? -0.0)
false
user=> (infinite? 0.0)
false

user=> (/ 0.0 0.0)
##NaN
user=> (NaN? (/ 0.0 0.0))
true
user=> (NaN? 0.0)
false

user=> (defn neg-zero? [n] (and (zero? n) (neg? (/ 1.0 n))))
#'user/neg-zero?
user=> (neg-zero? 0.0)
false
user=> (neg-zero? -0.0)
true
user=> (neg-zero? -1.0)
false
user=> (neg-zero? 1.0)
false

なお、-0.0 は数学関数 (Java の数学関数 Math/atan2 など) や複素数の演算処理などで使われます。

●データ型の定義

今回は Clojure のレコードを使って複素数を表すことにしましょう。次のリストを見てください。

リスト : 複素数の定義

;; 複素数型
(defrecord Complex [realpart imagpart])

;; コンストラクタ
(defn make-complex [a b]
  (->Complex (double a) (double b)))

;; 表示
(defmulti cprint class)
(defmethod cprint Number [n] (print n))
(defmethod cprint Complex [z]
  (printf "#C(%s %s)\n" (:realpart z) (:imagpart z)))

レコード名は Complex としました。実部をフィールド realpart に、虚部を imagpart にセットします。格納する数値の型は実数 (Double) とします。実部は :realpart で、虚部は :imagpart で取得します。名前は Common Lisp から拝借しました。

複素数は関数 make-complex で生成します。引数 x が実部で、y が虚部です。x, y をフィールド realpart, imagpart にセットするとき、関数 double を使って Double に変換しています。関数 cprint は標準出力に複素数 C(x y) を出力します。表記は Common Lisp を参考にしました。

●基本的な関数

複素数 \(z = x + iy\) の虚部の符号を反転した数 \(x - iy\) を複素共役といいます。複素数を極形式 \(z = |z|(\cos \theta + i \sin \theta)\) で表した場合、\(|z|\) を絶対値、\(\theta\) を偏角といいます。絶対値 \(|z|\) の定義は \(\sqrt{x^2 + y^2}\) です。偏角は数学関数 atan2(y, x) で求めることができます。これをプログラムすると次のようになります。

リスト : 複素共役, 絶対値, 偏角

;; 複素共役
(defn conjugate [z]
  (make-complex (:realpart z) (- (:imagpart z))))

;; 絶対値
(defmulti cabs class)
(defmethod cabs Number [n] (abs n))
(defmethod cabs Complex [z]
  (Math/hypot (:realpart z) (:imagpart z)))

;; 偏角
(defmulti carg class)
(defmethod carg Complex [z]
  (Math/atan2 (:imagpart z) (:realpart z)))

複素共役を求める関数 conjugate と偏角を求める関数 carg は簡単ですね。(Math/atan2 y x) は直交座標においてベクトル (x, y) と x 軸との角度を求める関数です。角度 \(\theta\) の範囲は \(-\pi \leq \theta \leq \pi\) になります。簡単な例を示しましょう。

user=> (Math/atan2 0 1)
0.0
user=> (Math/atan2 1 1)
0.7853981633974483
user=> (Math/atan2 1 0)
1.5707963267948966
user=> (Math/atan2 0 -1)
3.141592653589793
user=> (Math/atan2 -1 1)
-0.7853981633974483
user=> (Math/atan2 -1 0)
-1.5707963267948966
user=> (Math/atan2 -1 -1)
-2.356194490192345
user=> (Math/atan2 -0.1 -1)
-3.0419240010986313
user=> (Math/atan2 -0.01 -1)
-3.131592986903128
user=> (Math/atan2 -0.001 -1)
-3.1405926539231266
user=> (Math/atan2 -0.0 -1)
-3.141592653589793

Clojure の場合、-1.0+0.0j の偏角 \(\theta\) は \(\pi\) になり、-1.0-0.0j の偏角は \(-\pi\) になります。-0.0 は負のゼロを表します。ゼロと負のゼロを区別しないプログラミング言語では、偏角 \(\theta\) の範囲を \(-\pi \lt \theta \leq \pi\) に制限して、-1.0 + 0.0j (== -1.0 - 0.0j) の偏角を \(\pi\) とします。

絶対値を求める関数 cabs は、定義をそのままプログラムすると二乗の計算でオーバーフローすることがあります。たとえば、#C(1e300 1e300) の絶対値を求めてみましょう。このとき、1e300 の二乗でオーバーフローします。

user=> (* 1e300 1e300)
##Inf

参考文献『C言語による最新アルゴリズム事典』には、\(x^2\) や \(y^2\) で生じ得る上位桁あふれを回避する方法が記載されていますが、今回は Java の数学関数 Math/hypot を使うことにしましょう。

(Math/hypot x y) = \(\sqrt{x^2 + y^2}\)
user=> (Math/hypot 1e300 1e300)
1.4142135623730952E300

user=> (Math/hypot 1e300 1e301)
1.004987562112089E301

user=> (Math/hypot 1e301 1e301)
1.414213562373095E301

このように、hypot を使うとオーバーフローせずに計算することができます。

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

user=> (cprint (complex 1 1))
#C(1.0 1.0)
NIL
user=> (cprint (conjugate (make-complex 1 1)))
#C(1.0 -1.0)
NIL
user=> (cprint (conjugate (conjugate (make-complex 1 1))))
#C(1.0 1.0)
NIL

user=> (cabs (make-complex 1 1))
1.4142135623730951
user=> (cabs (make-complex 1 -1))
1.4142135623730951
user=> (cabs (make-complex 1e300 1e300))
1.4142135623730952E300
user=> (cabs (make-complex 1e301 1e300))
1.004987562112089E301
user=> (cabs (make-complex 1e300 1e301))
1.004987562112089E301

user=> (carg (make-complex 1 1))
0.7853981633974483
user=> (carg (make-complex 0 1))
1.5707963267948966
user=> (carg (make-complex -1 1))
2.356194490192345
user=> (carg (make-complex -1 0))
3.141592653589793
user=> (carg (make-complex 1 -1))
-0.7853981633974483
user=> (carg (make-complex 0 -1))
-1.5707963267948966
user=> (carg (make-complex -1 -1))
-2.356194490192345
user=> (carg (make-complex -1 -0.0))
-3.141592653589793

●四則演算

複素数の四則演算は次のようになります。

\( \begin{array}{l} (a + bi) + (c + di) = (a + c) + (b + d)i \\ (a + bi) - (c + di) = (a - c) + (b - d)i \\ (a + bi) \times (c + di) = (ac - bd) + (bc + ad)i \\ (a + bi) \div (c + di) = \dfrac{ac + bd + (bc - ad)i}{c^2 + d^2} \end{array} \)

除算の場合、絶対値の計算と同様にオーバーフローの対策が必要になります。今回は参考文献『C言語による最新アルゴリズム事典』のプログラムを Clojure に移植しました。プログラムは次のようになります。

リスト : 複素数の四則演算

(defmulti cadd #(vector (class %1) (class %2)))
(defmulti csub #(vector (class %1) (class %2)))
(defmulti cmul #(vector (class %1) (class %2)))
(defmulti cdiv #(vector (class %1) (class %2)))

(defmethod cadd [Number Number] [x y]
  (make-complex (+ x y) 0.0))
(defmethod cadd [Complex Complex] [x y]
  (make-complex (+ (:realpart x) (:realpart y)) (+ (:imagpart x) (:imagpart y))))
(defmethod cadd [Complex Number] [x y] (cadd x (make-complex y 0.0)))
(defmethod cadd [Number Complex] [x y] (cadd (make-complex x 0.0) y))

(defmethod csub [Number Number] [x y]
  (make-complex (- x y) 0.0))
(defmethod csub [Complex Complex] [x y]
  (make-complex (- (:realpart x) (:realpart y)) (- (:imagpart x) (:imagpart y))))
(defmethod csub [Complex Number] [x y] (csub x (make-complex y 0.0)))
(defmethod csub [Number Complex] [x y] (csub (make-complex x 0.0) y))

(defmethod cmul [Number Number] [x y]
  (make-complex (* x y) 0.0))
(defmethod cmul [Complex Complex] [x y]
  ;; (a + bi)(c + di) = ac + adi + bci - bd = (ac - bd) + (ad + bc)i
  (make-complex (- (* (:realpart x) (:realpart y)) (* (:imagpart x) (:imagpart y)))
                (+ (* (:realpart x) (:imagpart y)) (* (:imagpart x) (:realpart y)))))
(defmethod cmul [Complex Number] [x y] (cmul x (make-complex y 0.0)))
(defmethod cmul [Number Complex] [x y] (cmul (make-complex x 0.0) y))

(defmethod cdiv [Number Number] [x y]
  (make-complex (/ x y) 0.0))
(defmethod cdiv [Complex Complex] [x y]
  (if (>= (abs (:realpart y)) (abs (:imagpart y)))
    (let [u (/ (:imagpart y) (:realpart y))
          v (+ (:realpart y) (* (:imagpart y) u))]
      (make-complex (/ (+ (:realpart x) (* (:imagpart x) u)) v)
                    (/ (- (:imagpart x) (* (:realpart x) u)) v)))
    (let [u (/ (:realpart y) (:imagpart y))
          v (+ (* (:realpart y) u) (:imagpart y))]
      (make-complex (/ (+ (* (:realpart x) u) (:imagpart x)) v)
                    (/ (- (* (:imagpart x) u) (:realpart x)) v)))))
(defmethod cdiv [Complex Number] [x y] (cdiv x (make-complex y 0.0)))
(defmethod cdiv [Number Complex] [x y] (cdiv (make-complex x 0.0) y))

他の数値と混在しても計算できるように、ディスパッチ値を [Complex Complex] だけではなく、[Number Number], [Complex Number], [Number Complex] のメソッドも定義しています。

●実行例

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

user=> (require '[complex :refer :all])
nil

user=> (def a (make-complex 1 2))
#'user/a
user=> (cprint a)
#C(1.0 2.0)
nil

user=> (def b (make-complex 3 4))
#'user/b
user=> (cprint b)
#C(3.0 4.0)
nil

user=> (cprint (cadd a b))
#C(4.0 6.0)
nil
user=> (cprint (csub a b))
#C(-2.0 -2.0)
nil
user=> (cprint (cmul a b))
#C(-5.0 10.0)
nil
user=> (cprint (cdiv a b))
#C(0.44 0.08)
nil

user=> (cprint (cdiv 1 (make-complex 1e300 1e300)))
#C(5.0E-301 -5.0E-301)
nil
user=> (cprint (cdiv 1 (make-complex 1e301 1e300)))
#C(9.9009900990099E-302 -9.9009900990099E-303)
nil
user=> (cprint (cdiv 1 (make-complex 1e300 1e301)))
#C(9.9009900990099E-303 -9.9009900990099E-302)
nil

正常に動作しているようです。興味のある方はいろいろ試してみてください。


●プログラムリスト

;;;
;;; complex.clj : 複素数
;;;
;;;               Copyright (C) 2025 Makoto Hiroi
;;;
(ns complex)

;; 複素数型
(defrecord Complex [realpart imagpart])

;; コンストラクタ
(defn make-complex [a b]
  (->Complex (double a) (double b)))

;; 表示
(defmulti cprint class)
(defmethod cprint Number [n] (print n))
(defmethod cprint Complex [z]
  (printf "#C(%s %s)\n" (:realpart z) (:imagpart z)))

;; 複素共役
(defn conjugate [z]
  (make-complex (:realpart z) (- (:imagpart z))))

;; 絶対値
(defmulti cabs class)
(defmethod cabs Number [n] (abs n))
(defmethod cabs Complex [z]
  (Math/hypot (:realpart z) (:imagpart z)))

;; 偏角
(defmulti carg class)
(defmethod carg Complex [z]
  (Math/atan2 (:imagpart z) (:realpart z)))

;;; 四則演算
(defmulti cadd #(vector (class %1) (class %2)))
(defmulti csub #(vector (class %1) (class %2)))
(defmulti cmul #(vector (class %1) (class %2)))
(defmulti cdiv #(vector (class %1) (class %2)))

(defmethod cadd [Number Number] [x y]
  (make-complex (+ x y) 0.0))
(defmethod cadd [Complex Complex] [x y]
  (make-complex (+ (:realpart x) (:realpart y)) (+ (:imagpart x) (:imagpart y))))
(defmethod cadd [Complex Number] [x y] (cadd x (make-complex y 0.0)))
(defmethod cadd [Number Complex] [x y] (cadd (make-complex x 0.0) y))

(defmethod csub [Number Number] [x y]
  (make-complex (- x y) 0.0))
(defmethod csub [Complex Complex] [x y]
  (make-complex (- (:realpart x) (:realpart y)) (- (:imagpart x) (:imagpart y))))
(defmethod csub [Complex Number] [x y] (csub x (make-complex y 0.0)))
(defmethod csub [Number Complex] [x y] (csub (make-complex x 0.0) y))

(defmethod cmul [Number Number] [x y]
  (make-complex (* x y) 0.0))
(defmethod cmul [Complex Complex] [x y]
  ;; (a + bi)(c + di) = ac + adi + bci - bd = (ac - bd) + (ad + bc)i
  (make-complex (- (* (:realpart x) (:realpart y)) (* (:imagpart x) (:imagpart y)))
                (+ (* (:realpart x) (:imagpart y)) (* (:imagpart x) (:realpart y)))))
(defmethod cmul [Complex Number] [x y] (cmul x (make-complex y 0.0)))
(defmethod cmul [Number Complex] [x y] (cmul (make-complex x 0.0) y))

(defmethod cdiv [Number Number] [x y]
  (make-complex (/ x y) 0.0))
(defmethod cdiv [Complex Complex] [x y]
  (if (>= (abs (:realpart y)) (abs (:imagpart y)))
    (let [u (/ (:imagpart y) (:realpart y))
          v (+ (:realpart y) (* (:imagpart y) u))]
      (make-complex (/ (+ (:realpart x) (* (:imagpart x) u)) v)
                    (/ (- (:imagpart x) (* (:realpart x) u)) v)))
    (let [u (/ (:realpart y) (:imagpart y))
          v (+ (* (:realpart y) u) (:imagpart y))]
      (make-complex (/ (+ (* (:realpart x) u) (:imagpart x)) v)
                    (/ (- (* (:imagpart x) u) (:realpart x)) v)))))
(defmethod cdiv [Complex Number] [x y] (cdiv x (make-complex y 0.0)))
(defmethod cdiv [Number Complex] [x y] (cdiv (make-complex x 0.0) y))

初版 2025 年 7 月 24, 26 日