LL脳がscalaの勉強を始めたよ その12


Scalaコップ本の第6章にすすみますよー、第6章では実際に関数型のオブジェクトを構築してみますよー

immutableオブジェクト

関数型ということでこの章ではimmutableなオブジェクトを具体例を利用して実際に作成していきますー

immutableオブジェクトのメリット

コップ本によればimmutableなオブジェクトはmutableオブジェクトに比べて

  • 時間とともに変化するような状態空間を持たないので、動作を推定しやすい
  • コピーを作らずに他のオブジェクトに渡しても安全
  • 複数のスレッドがアクセスしても状態を壊す恐れが無い
  • ハッシュテーブルのキーを安全に作れる

という4つのメリットを挙げられるらしいんだけど、要は「immutableなオブジェクトは外侮から勝手に変更されないから状態が保証できるよね、mutableは無理」って話だと勝手に解釈。

immutableオブジェクトのデメリット

デメリットとしてimmutableなオブジェクトは部分変更ができないので、何かを変更する必要がある場合はオブジェクトグラフを丸々コピーする必要なんかがあるので、性能的なボトルネックになる可能性があるよーとのことデス

まあ前の章にも出ているように、できるだけ変更がかからないように(immutableになるように)設計して、性能的やらでどうしても…という場合のみmutableにするのがScala的ってことデスネ

それではクラスを作ってみましょー

今回例として作成するのはRationalクラスです。Rationalは有理数(整数の分数で表現できる数のことデス)のことで、作成するクラスは有理数(分数:ただし分母は0以外)の加減乗除を実装しますよー

またRationalオブジェクトはimmutableなものとするので、例えば異なるRationalオブジェクト同士の和は新しいRational オブジェクトを作成するような動作になりマス

なんで有理数

ちなみに数学時代以来の有理数という存在ですが、分数を利用した有利数計算は浮動小数点数のように丸めの処理が発生しないので、”有理数で表現できる”数であれば正確に表現することが可能デス。

有理数で表現できない”と強調しているのは表現できない数→無理数の方が有理数に比べて圧倒的に多いからだというチキン防御ですけども何か?

まずは目標を明確化

さて、破綻気味の自然言語でズラズラ書いたところで目標のイメージがつくはずも無いので、Rationalクラスの動作がどういうものになればいいのか!の例をちょろっと

// インスタンスの生成:halfには1/2をもつRationalオブジェクトが格納される
val half = new Rational(1, 2)
// インスタンスの生成:quarterには1/4をもつRationalオブジェクトが格納される
val quarter = new Rational(1, 4)
// 四則演算が可能:13/8をもつRationalオブジェクトが生成される
( half * 3 ) + ( 1 - quarter / 2 )

とりあえずこんな感じのモノを作っていきますよー

Rationalの構築

まずはクラスの基本定義をやりますよー、整数による分数で表現できるのが有理数なので引数として分子、分母の2つの整数をとります

// nが分子でdが分母です
class Rational(n:Int, d:Int) 

このコードで生成されるのは2個のパラメータをとる基本コンストラクタ(コンパイラが自動生成するものかしら?)が生成されるそうです。それと上では引数と適当に呼んだものですが、正確にはクラスパラメータと呼ぶらしいデス。まあ、クラスの引数だから意味的には一緒かと。

Javaなんかだとクラスパラメータは別途記述したコンストラクタの中で受け付けたりする(PHPもそうだった気がする)んですが、Scalaだとそこら辺はすっ飛ばして簡潔に書けるのでかなり楽ちんですねぇ

それじゃ、こんな感じの基本クラスを実際に動かして試してみますよー

// Rationalクラスの作成
scala> class Rational(n:Int, d:Int) {
     |   println("Created " + n + "/" +d)
     | }
defined class Rational

// インスタンスの生成
scala> new Rational(1, 2)
Created 1/2
res0: Rational = Rational@4d40df
クラス本体に書かれた処理

さりげなく書いておいたRationalクラス内のprintlnがインスタンス生成時に実行されているんですが、Scalaコンパイラはクラス本体に書かれたコード全体を基本コンストラクタの処理としてコンパイルするっぽいですね。

うん、慣れちゃえばコード量が減るのでかなり楽チンそうだ(`・ω・´)

toStringメソッドをオーバーライド

上でインスタンス生成した際に出力された「res0: Rational = Rational@4d40df」という行はRationalオブジェクトのtoStringメソッドが呼び出しているらしく、標準ではjava.lang.Objectで定義されたtoStingが呼び出されて「クラス名@16進数(内部的なインスタンス識別子かしら?)」の形式になるみたい。

でも、こんなのが出力されてもイマイチ活用方法が思い浮かばないので、Rationalインスタンス中身が確認できるように出力内容を変更してみますー

// toStringメソッドをオーバーライドしたクラスを定義しますよー
scala> class Rational(n:Int, d:Int) {
     |   override def toString = n  + "/" + d
     | }
defined class Rational
// インスタンス生成したら値がきちんと出力されましたよー
scala> new Rational(1, 2)                    
res1: Rational = 1/2

オブジェクトのtoStringメソッドはデバッグ出力文やログメッセージによく使うそうなので、欲しい情報を詰め込んでおくと素敵な感じ

事前条件のチェック

とりあえず基本的なクラスを作成できるようになったものの、有理数の分母は0になってはいけないので事前チェックの処理を追加しますー

具体的にはクラスパラメータdは0をとってはいけないので、dに0が渡された場合はエラーとして処理してオブジェクトの構築をしないようにします。

// requireを定義して条件に満たない場合は
// IllegalArgumentExceptionを投げるようにします
scala> class Rational(n:Int, d:Int) {
     |     require(d != 0)
     |     override def toString = n  + "/" + d
     | }
defined class Rational
// 分母に0を指定したらエラーが発生しますよー
scala> new Rational(1, 0)                      
java.lang.IllegalArgumentException: requirement failed
	at scala.Predef$.require(Predef.scala:107)
	at Rational.<init>(<console>:5)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMetho...
// 0以外なら普通に処理されますよー
scala> new Rational(1, 2)                      
res3: Rational = 1/2

今回はここまでー

とりあえずクラスの定義まで終わったので残りは次回にー、次は具体的な演算の追加ですな