複素数クラスを実装することによりScalaのコンセプトを説明する: Complex Numbers in Scalaを読んで

Complex Numbers in Scala | Stoyan Rachev's Blog
Java Code Geeksの記事 CC-BY-SA 3.0

複素数クラスの実装を通じてScalaの重要なコンセプトを説明している。説明する主な機能は以下のとおり。

  • メソッドのオーバーライド
  • コンストラクタと演算子オーバーロード
  • 暗黙の型変換
  • アクセス修飾子
  • 単項演算子
  • コンパニオンオブジェクト
  • トレイト
  • case classとパターンマッチング

逐一流れをなぞってるけど翻訳ではなく読書ノートという位置づけで一つよろしくお願いします(逃げ)。あと日本語割と砕けてる。

開始

最初の定義はすごくシンプルに。

class Complex(val re: Double, val im: Double)

これで全部。2つのDouble型を持つだけ。Scalaにおいてはこれらの値はpublic(デフォルト)で変更不可能(valキーワードによる)である。
インスタンスを作るとこうなる。

scala> val x = new Complex(1, 2)
x: Complex = Complex@1e2350a

(原文ではこれだけでもJavaより簡潔ですよねムフフみたいなことを言ってるけどスルー)

メソッドのオーバーライド

インスタンスの表現がわかりづらいので、複素数を表現する a+b*i みたいな表示になるようにしましょう。

x: Complex = Complex@1e2350a

これはtoStringメソッドをオーバライドすればいける。どこからオーバーライドしているのかって? Scalaでは、すべてのクラスはAnyクラスから派生しているのだ。

class Complex(val re: Double, val im: Double) {
  override def toString =
    re + (if (im < 0) "-" + -im else "+" + im) + "*i"
}

overrideキーワードは必須だ*1。これを省いた場合、うっかりオーバーライドしてしまう事故を防ぐためにScalaではコンパイルエラーを吐くようになっている。
じゃあインスタンスを作ってみましょう。

scala> val x = new Complex(1, 2)
x: Complex = 1.0+2.0*i

メソッド、演算子メソッド

複素数に加算を定義してみましょう。とりあえずJavaっぽくaddメソッドを追加してみましょうか。

class Complex(val re: Double, val im: Double) {
  def add(c: Complex) = new Complex(re + c.re, im + c.im)
  ...
}

こいつはJavaのメソッド呼び出しと同じようにできる。

scala> val x = new Complex(1, 2)
x: Complex = 1.0+2.0*i

scala> val y = new Complex(3, 4)
y: Complex = 3.0+4.0*i

scala> x.add(y)
res0: Complex = 4.0+6.0*i

ところでScalaではね、副作用のないメソッドは演算子の表記みたいにして*2呼び出すことができるんだよ!

scala> x add y
res1: Complex = 4.0+6.0*i

さらにさらに、実は演算子+もオーバライドすることができるんです。addなんていらなかったんだ! そう、Scalaならね!

class Complex(val re: Double, val im: Double) {
  def +(c: Complex) = new Complex(re + c.re, im + c.im)
  ...
}

よってxとyの和はシンプルに計算できるようになった。

scala> x + y
res2: Complex = 4.0+6.0*i

これを見て「あっ、C++演算子オーバーロードだ、殺せ!」となったキミ、ちょっと待ってほしい。実はね、Scalaでは演算子に見えるものでもすべてメソッドなんだよ! これにより、伝統的なオーバーロード手法よりも一貫的で簡明な演算子記法の取り扱いが可能になっているから安心してほしい。

メソッドとコンストラクタをオーバーロードする

複素数クラスに第2引数(虚部)を与えなかった場合、それは虚部を持たない実数として解釈できるよね。あと実数は複素数に含まれるのだからこの2つの数の間の演算も定義したい。
Scalaではもちろんコンストラクタとメソッドをオーバーロードすることができる。第2引数を省略した場合、すなわちim=0のコンストラクタと、+メソッドをDouble型を受け入れるようにオーバーロードしてみよう。

class Complex(val re: Double, val im: Double) {
  def this(re: Double) = this(re, 0)
  ...
  def +(d: Double) = new Complex(re + d, im)
  ...
}

これで実部の計算についても仕様をカバーしたComplexクラスができた。

scala> val y = new Complex(2)
y: Complex = 2.0+0.0*i

scala> y + 2
res3: Complex = 4.0+0.0*i

オーバーロードの記述はJavaや他の言語と似通っているが、コンストラクタのオーバーロードについてはScalaではさらに強い制約が課されていることに気をつけよう。つまりすべてのオーバーロードされたコンストラクタは最終的にデフォルトコンストラクタ(クラスの頭で定義されたやつ)を呼び出す必要があるのだ。またデフォルトコンストラクタはスーパークラスのコンストラクタしか呼び出すことができない。

暗黙の型変換

ところで、上のコードではy + 2する代わりに2 + yをしようとするとエラーが出る。残念ながらScalaの単純な数値型の+メソッドはComplex型を受け入れてくれないからだ。これを解決するためにDoubleからComplexへの暗黙の型変換を定義することができる。

implicit def fromDouble(d: Double) = new Complex(d)

これでDoubleにComplexを足すことができるようになった。

scala> 2 + y
res4: Complex = 4.0+0.0*i

ところで、implicitを用いればこれ以上 + をオーバーロードせずとも加算が定義できるのである。
やった、暗黙の型変換最高や! これさえあれば演算子オーバーロードなんて必要なかったんや!
じっさい、Why Method Overloading Sucks in Scalaで説明されているように、演算子オーバーロードより暗黙の型変換を好む深遠な理由があるのだ。Complexクラスの最終版では、単純な型クラスからの暗黙の型変換が加えられている。

アクセス修飾子

privateとprotectedは普通に使える。
ここでは複素数の絶対値を計算するmodulusをprivateで定義してみよう。

import scala.math.{sqrt, pow}

class Complex(val re: Double, val im: Double) {
  private val modulus = sqrt(pow(re, 2) + pow(im, 2))
  ...
}

単項演算子

ではこのComplexクラスの利用者にインスタンスの絶対値を取れるようにするにはどうしたらよいのだろうか? 絶対値の取得はとても一般的な演算なので、短いナイスな演算子で呼び出せるようにしておきたいですね! というわけで、ここでは単項演算子で呼び出せたほうがいいだろう。ありがたいことに、Scalaではこのような演算子を定義する楽な方法が存在する。

class Complex(val re: Double, val im: Double) {
  private val modulus = sqrt(pow(re, 2) + pow(im, 2))
  ...
  def unary_! = modulus
  ...
}

すなわちunary_を頭につけたメソッドは単項演算子から呼び出せるのである。

scala> val y = new Complex(3, 4)
y: Complex = 3.0+4.0*i

scala> !y
res5: Double = 5.0

コンパニオンオブジェクト

Scalaでは、objectキーワードによって、シングルトンクラスとシングルトンインスタンスを同時に宣言することができる。また、objectで宣言したシングルトンと同名のクラスが同一ファイル内で宣言されている場合、そのシングルトンはクラスのコンパニオンオブジェクトとなる。コンパニオンオブジェクトはそのクラスと特別な関係を取り持ち、クラスのプライベートなフィールドやメソッドにアクセスできるようになる。

Scalaにはstaticキーワードが存在しない。それはScalaの開発者がstaticは真のオブジェクト指向にはそぐわないものだと考えたからだ。よって、コンパニオンオブジェクトは他の言語で言うところのstaticなメンバを置いておくところである。例えばファクトリメソッド、そして暗黙の型変換などがここに書き込まれまれる。Complexクラスのコンパニオンオブジェクトを定義してみよう。

object Complex {
  val i = new Complex(0, 1)
  def apply(re: Double, im: Double) = new Complex(re, im)
  def apply(re: Double) = new Complex(re)
  implicit def fromDouble(d: Double) = new Complex(d)
}

このコンパニオンオブジェクトは以下のメンバを持つ。

  • 定数i。これは虚数単位。
  • 2つのapplyメソッド。これはComplexインスタンスを生成するファクトリメソッドで、newする手間を省くものである。
  • 暗黙の型変換fromDoubleは上で紹介したとおり。

コンパニオンオブジェクトのお陰で、オブジェクト指向であることを損なわずにスッキリ書けるようになりましたね!

scala> 2 + i + Complex(1, 2)
res6: Complex = 3.0+3.0*i

トレイト(Traits)

数学的には複素数は比較することができない。とは言っても、実用上絶対値によって順序関係を導入しておけば便利であろう。数値型の比較には<, <=, >, >=という4つの演算子を用いることができる。

一つの方法として、それぞれ4つのメソッドについて演算を定義することが考えられるだろう。ところで、Scalaにおいては<, <=, >, >=を一度に定義するある種の鋳型(boilerplate)が存在し、これをトレイトと呼んでいる。

トレイトは、オブジェクトの型をサポートするメソッドのシグネチャによって規定するという点で、Javaのinterfaceに似ている。しかし部分的な実装を許しているという点で異なっている。これはJava 8のdefault methodという機能に似通っている。Scalaにおいてはクラスは拡張することができ、あるいはmixin class compositionによって複数のトレイトをmix-inすることができる。

ここでは、OrderedトレイトをComplexクラスにmix-inしている。このトレイトは4つの比較演算子(<, <=, >, >=)の実装を提供しており、その全てがcompareメソッドを呼び出すようになっている。つまりすべての比較演算がcompareに集約されており、我々はcompareの実装さえ与えてやればよい。

class Complex(val re: Double, val im: Double) 
  extends Ordered[Complex] {
  ...
  def compare(that: Complex) = !this compare !that
  ...
}

これで複素数の比較演算ができるようになった。

scala> Complex(1, 2) > Complex(3, 4)
res7: Boolean = false

scala> Complex(1, 2) < Complex(3, 4)
res8: Boolean = true

Case Classとパターンマッチング

ところでまだ思ったように動作しない演算子が残っている。等価演算子==である。

scala> Complex(1, 2) == Complex(1, 2)
res8: Boolean = false

こうなるのは==がデフォルトではequalsメソッドを呼び出して、参照の等価性を調べているからである。一つの方法はequalsをオーバーライドすることである。ただしこの場合hashCodeもオーバーライドしなければならない。実に面倒ですね!

なんと、Scalaではcase classを用いることによってこのような面倒を回避するためにことができるのです。case classはクラス宣言の頭にcaseキーワードを足すことで定義できる。これによって以下のような便利機能が自動的に追加されるんだ。

  • 適切なequalsとhashCodeの実装*3
  • applyファクトリメソッド付きのコンパニオンオブジェクト
  • クラス変数は暗黙的にvalで宣言される

書き方。

case class Complex(re: Double, im: Double) 
  ...
}

これで==は期待通りの動作をするようになる。

scala> i == Complex(0, 1)
res9: Boolean = true

ところで、case classの最も重要な機能はそれがパターンマッチの中で利用できるということだ。このことを知るために、以下のtoStringの実装を見てみよう。

  override def toString =
    this match{
      case Complex.i      => "i"
      case Complex(re, 0) => re.toString
      case Complex(0, im) => im.toString + "*i"
      case _              => asString
    }
  private def asString =
    re + (if (im < 0) "-" + -im else "+" + im) + "*i"

このコードはそれぞれのパターン(虚数単位i, 実数, 純虚数, そして複素数)に対してthisがマッチするようになっている。パターンマッチングはVisitorパターンの代替と考えられるが、短くて理解しやすく、複雑なオブジェクト階層に沿って処理を行う場合にその恩恵は計り知れないものになる。

仕上げ

import scala.math._

case class Complex(re: Double, im: Double) extends Ordered[Complex] {
  private val modulus = sqrt(pow(re, 2) + pow(im, 2))

  // コンストラクタ
  def this(re: Double) = this(re, 0)

  // 単項演算子
  def unary_+ = this
  def unary_- = new Complex(-re, -im)
  def unary_~ = new Complex(re, -im) // 複素共役
  def unary_! = modulus

  // 比較
  def compare(that: Complex) = !this compare !that

  // 算術演算
  def +(c: Complex) = new Complex(re + c.re, im + c.im)
  def -(c: Complex) = this + -c
  def *(c: Complex) = 
    new Complex(re * c.re - im * c.im, im * c.re + re * c.im)
  def /(c: Complex) = {
    require(c.re != 0 || c.im != 0)
    val d = pow(c.re, 2) + pow(c.im, 2)
    new Complex((re * c.re + im * c.im) / d, (im * c.re - re * c.im) / d)
  }

  // 文字列表現
  override def toString() = 
    this match {
      case Complex.i => "i"
      case Complex(re, 0) => re.toString
      case Complex(0, im) => im.toString + "*i"
      case _ => asString 
    } 
  private def asString = 
    re + (if (im < 0) "-" + -im else "+" + im) + "*i"  
}

object Complex {
  // 虚数単位
  val i = new Complex(0, 1)

  // ファクトリメソッド
  def apply(re: Double) = new Complex(re)

  // 暗黙の変換
  implicit def fromDouble(d: Double) = new Complex(d)
  implicit def fromFloat(f: Float) = new Complex(f)
  implicit def fromLong(l: Long) = new Complex(l)
  implicit def fromInt(i: Int) = new Complex(i)
  implicit def fromShort(s: Short) = new Complex(s)
}

import Complex._

ところでこれまでのコードをScalaインタプリタ内で実行してみるさいは、:pasteを使うと便利ですよ!

この記事に対するreactionという人のコメント

Complexクラスの最終版コードについて2つ。

  1. 暗黙の型変換について無駄が多い。*4
  2. Complexのコンストラクタについては、オーバーロードしなくてもデフォルト引数を渡してあげてあげればいいんじゃないかな!
case class Complex(re: Double, im: Double = 0.0)

*1:読注:実装を持つクラス(具象クラス)から継承してオーバーライドする場合。抽象クラスを継承した場合は不要だし書いてはいけない。

*2:読注:ピリオド記号.とカッコを省いて、すなわち中置記法として

*3:読注: 適切(adequate)ってどういうこと、と調べたらequalsはクラスの各フィールドの値を比較するようになるそうです。

*4:読注:具体的な指摘されているんですがまだ理解できていないので後で勉強します…