Scalaで複素数計算
Table of Contents
1 概要
Scalaで,複素数計算を行うクラスを設計する.
1.1 注意
本Webページの作成には Emacs org-mode を用い, 数式等の表示は MathJax を用いています. IEでは正しく表示されないことがあるため, Firefox, Safari等のWebブラウザでJavaScriptを有効にしてお使いください. また org-info.js を利用しており, 「m」キーをタイプするとinfoモードでの表示になります. 利用できるショートカットは「?」で表示されます.
2 case class
Scalaでクラスを定義する場合 case class を用いるのが便利である.
複素数の実数部を re で,虚数部を im で表すことにすると, 複素数のクラス Complex は以下のように定義できる.
1: case class Complex(re: Double, im: Double)
これを Complex.scala というファイルに記述して, REPL からロードする.
scala> :load Complex.scala Loading Complex.scala... defined class Complex
以下のようにすれば Complex オブジェクトを生成できる.
scala> Complex(1, 2) res: Complex = Complex(1.0,2.0)
このように case class には,自動的に toString メソッドが定義されているため, Complex(1.0,2.0) のように表示されている.
また,equals や hashCode メソッドも自動的に定義されているため, オブジェクトの比較も自然に行える.
scala> Complex(1, 2) == Complex(1, 2) res: Boolean = true
hashCode が定義されているので,Map のキーに利用するのも問題ない.
2.1 初期値の指定
以下のように記述することで,引数を省略した場合の初期値を設定できる.
1: case class Complex(re: Double = 0.0, im: Double = 0.0)
虚数部を省略した場合 0.0 が指定され, 実数部と虚数部の双方を省略した場合どちらも 0.0 が指定される.
scala> Complex(1) res: Complex = Complex(1.0,0.0) scala> Complex() res: Complex = Complex(0.0,0.0)
虚数部のみを指定するには,名前付きパラメータを用いる.
scala> Complex(im = 1) res: Complex = Complex(0.0,1.0)
3 メソッドの定義
次に複素数同士の加減乗算などのメソッドを定義する.
3.1 plusメソッドの定義
まず,複素数の加算を行うメソッド plus を定義する.
つまり他の複素数 that を引数として与え,メソッド plus(that) を呼出すと,
this (現在のオブジェクト)と that の和になっている新しい複素数が返るようにする.
Scalaでメソッドを定義するには以下のように記述する.
1: case class Complex(re: Double = 0.0, im: Double = 0.0) { 2: def plus(that: Complex): Complex = /* plus の定義本体 */ 3: }
def はメソッド定義を意味するキーワードで, plus が定義するメソッド名である.
that は引数を表す変数名を表しており,その直後の Complex は引数 that の
データ型を表している (もちろん引数名は that ではなく他の名前を用いても良い).
その後の Complex は plus メソッドが返す値のデータ型を表す.
そして = の後にメソッドの定義本体を記述する.
複素数の和は,メソッド呼出しの対象である複素数オブジェクトの this と,
引数として与えられた複素数オブジェクト that について,
実数部の和と虚数部の和をそれぞれ求め,
それらを実数部と虚数部にした複素数オブジェクトを生成すれば良い.
実数部の和は re + that.re (あるいは this.re + that.re) で求められ,
虚数部の和は im + that.im (あるいは this.im + that.im) で求められる.
したがって plus メソッドは以下のように定義できる.
def plus(that: Complex): Complex = Complex(re + that.re, im + that.im)
なお,結果のデータ型はScala処理系が型推論により自動的に推論するので,
省略しても良い.
したがって plus メソッドを追加した複素数クラスは以下のようになる.
1: case class Complex(re: Double = 0.0, im: Double = 0.0) { 2: def plus(that: Complex) = 3: Complex(re + that.re, im + that.im) 4: }
3.2 実行
上の複素数クラスを記述したファイルを Complex.scala として保存すると,
以下のようにして実行できる.
scala> :load Complex.scala Loading Complex.scala... scala> val z1 = Complex(1, 2) z1: Complex = Complex(1.0,2.0) scala> val z2 = Complex(3, 4) z2: Complex = Complex(3.0,4.0) scala> val z3 = Complex(5, 6) z3: Complex = Complex(5.0,6.0) scala> z1.plus(z2) res: Complex = Complex(4.0,6.0) scala> z1.plus(z2).plus(z3) res: Complex = Complex(9.0,12.0)
Scala ではメソッド呼出しの前のドット (.)や引数のカッコを省略できる.
scala> z1 plus z2 plus z3 res: Complex = Complex(9.0,12.0)
ただし,演算子の優先度などに注意しなければならない.
上の場合,メソッド呼出しは左結合的になる.
すなわち z1 plus z2 plus z3 は (z1 plus z2) plus z3 と同等で
z1 plus (z2 plus z3) とは異なる (答えは同じになる).
3.3 練習問題
- 複素数の減算を計算するメソッド
minusを定義せよ.- (解答例)
-
def minus(that: Complex) = Complex(re - that.re, im - that.im)
- 複素数の乗算を計算するメソッド
timesを定義せよ.- (解答例)
-
def times(that: Complex) = Complex(re*that.re - im*that.im, re*that.im + im*that.re)
- 複素数のマイナスを計算するメソッド
negateを定義せよ.- (解答例)
-
def negate = Complex(- re, - im)
- 上記のメソッドを定義した複素数クラス全体を作成し,実行せよ.
- (解答例)
-
1: case class Complex(re: Double = 0.0, im: Double = 0.0) { 2: def negate = 3: Complex(- re, - im) 4: def plus(that: Complex) = 5: Complex(re + that.re, im + that.im) 6: def minus(that: Complex) = 7: Complex(re - that.re, im - that.im) 8: def times(that: Complex) = 9: Complex(re*that.re - im*that.im, re*that.im + im*that.re) 10: }
z1 plus z2 times z3はどのように実行されるか. すなわち(z1 plus z2) times z3あるいはz1 plus (z2 times z3)のどちらの結果と一致するか.
4 演算子の定義
Scalaでは + や * などの演算子を定義できる.
これらを用いれば,通常の数式と同様の記法および優先度で式を記述可能になる.
たとえば + を以下のように定義する.
1: case class Complex(re: Double = 0.0, im: Double = 0.0) { 2: def plus(that: Complex) = 3: Complex(re + that.re, im + that.im) 4: def + (that: Complex) = plus(that) 5: }
また * も同様に定義すれば,
\(z_1+z_2\times z_3\) を以下のように自然な形で記述できる.
scala> z1 + z2 * z3 res: Complex = Complex(-8.0,40.0) scala> z1 plus (z2 times z3) res: Complex = Complex(-8.0,40.0)
単項の前置演算子 - は以下のように定義する.
def unary_- = negate
\(- z_1\) も以下のように自然な形で記述できる.
scala> - z1 res: Complex = Complex(-1.0,-2.0) scala> z1 negate res: Complex = Complex(-1.0,-2.0)
5 メソッドおよび演算子の多重定義
演算子の定義により自然な記述が可能になったが, 以下のような記述はエラーになる.
scala> z1 plus 1 <console>:11: error: type mismatch; scala> z1 + 1 <console>:11: error: type mismatch;
これはメソッドおよび演算子を 多重定義 (overloading)する方法で 解決できる.
1: case class Complex(re: Double = 0.0, im: Double = 0.0) { 2: def plus(that: Complex) = 3: Complex(re + that.re, im + that.im) 4: def plus(x: Double) = Complex(re + x, im) 5: def + (that: Complex) = plus(that) 6: def + (x: Double) = plus(x) 7: }
上のプログラムで4行目と6行目が追加した部分である. このように変更すれば,以下のように自然に $z1 + 1$を記述できる.
scala> :load Complex.scala scala> val z1 = Complex(1, 2) z1: Complex = Complex(1.0,2.0) scala> z1 + 1 res: Complex = Complex(2.0,2.0)
6 暗黙変換
上の多重定義では,以下のような記述には対応できない.
scala> 1 + z1 <console>:11: error: overloaded method value + with alternatives:
これを解決するにはScalaの 暗黙変換 (implicit conversion)の機能を用いる. このため,以下のようにDoubleをComplexに変換する 関数double2complexを暗黙変換として定義する.
scala> implicit def double2complex(x: Double): Complex = Complex(x) double2complex: (x: Double)Complex
Scala処理系は,型推論で型誤りが生じる場合,
暗黙変換で型誤りが解消できるならばその変換を自動的に適用する.
したがって 1 + z1 の場合 1 をComplexに変換すれば,
型誤りを解消できるため,Scala処理系はそのような変換を暗黙的に行う.
scala> 1 + z1 res: Complex = Complex(2.0,2.0)
上の暗黙変換をファイル中に記述したい場合は, たとえば Complex.scala ファイル中に以下のような記述を追加する.
1: object ComplexConversion { 2: implicit def double2complex(x: Double): Complex = Complex(x) 3: }
以下のようにすれば ComplexxConversion 中のメソッド (関数)がインポートされ, 暗黙変換が利用可能になる.
scala> :load Complex.scala scala> import ComplexConversion._
このように暗黙変換は,非常に便利かつ強力な機能であるが, 多用するとScalaの型推論による型検査の利点が損なわれるため, 限定的に利用することが望ましい.