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


Scalaコップ本の6章を進めていきますよー、今回は補助コンストラクタと非公開メソッド・フィールドですよー

今回はいつもより多めに脱線しておりますー

前回までの復習ー

前回までに作成した有理数演算クラスRationalはこんな感じですな

class Rational(n:Int, d:Int) {
    // 分母が0で無いことをチェック
    require(d != 0)
    // 外部からアクセス可能なフィールドを追加
    val numer: Int =n
    val denom: Int = d
    // クラスのtoString表現を見やすく上書き
    override def toString = numer + "/" + denom
    // 2つのRationalオブジェクトを加算するメソッド
    def add(other: Rational):Rational = {
        new Rational(
            numer * other.denom + other.numer * denom,
            denom * other.denom
        )
    }
    // 2つのRationalオブジェクトを比較してレシーバのほうが小さいか判定するメソッド
    def lessThan(other:Rational) = {
        this.numer * other.denom < other.numer * this.denom
    } 
    // 2つのRationalオブジェクトの大きい方を返すメソッド
    def max(other: Rational) = {
        if(this.lessThan(other)) other else this
    }
}

今回もこのクラスにいろいろと付け加えていきますよー

補助コンストラク

クラスにコンストラクタを複数与えたい場合に使うのが補助コンストラクタです(`・ω・´)とのことですが、ようするにクラスパラメータの個数や型を変えたオブジェクトの生成ができるようにしましょー、ってことだと解釈

例えば前回までに作ったクラスだとクラスパラメータとして必ず分子、分母の整数値を与えなければならなかったけど、有理数(4/1: 整数の4)だったらクラスパラメータは4だけでいいじゃん(´Д`)ってやりたいですよね…そんなときは補助コンストラクタです(`・ω・´)

んじゃ、実際に例を使って動かしてみますよー

例えばこれまで作ったRationalクラスを簡易化したのがコレです

class Rational(n:Int, d:Int) {
    override def toString = n + "/" + d
}

こいつに補助コンストラクタを定義します

class Rational(n:Int, d:Int) {
    // メソッドthisによって補助コンストラクトを定義します
    def this(n:Int) = this(n, 1)
    override def toString = n + "/" + d
}

補助コンストラクタを定義したことでこんな風にオブジェクトの生成ができます

// 分子のみ(整数)パターン
scala> val r1 = new Rational(3)
r1: Rational = 3/1
// 分子と分母パターン
scala> val r2 = new Rational(3/1)
r2: Rational = 3/1
複数定義できますのん?

複数の補助コンストラクタを定義してみますよー

class Rational(n:Int, d:Int) {
    def this(n:Int) = this(n, 1)
    // クラスパラメータが文字列型の補助コンストラクタを追加
    def this(s:String) = this(2,3)
    // クラスパラメータない補助コンストラクタを追加
    def this() = this(1,1)
    override def toString = n + "/" + d
}

まあ、なんのタメの静的型付けのか!というツッコミが聞こえてきそうですが…とりあえず置いといて実行して見ますよー

// 文字列で呼び出してもOK
scala> val r1 = new Rational("hoge") 
r1: Rational = 2/3
// クラスパラメータなしでもOK
scala> val r2 = new Rational()      
r2: Rational = 1/1
補助コンストラクタに記述できる処理は?

上の例の補助コンストラクタは基本コンストラクタを呼び出しているだけなので、ちょっぴりちがう処理をさせたくなったりするのです

まず、別の補助コンストラクタを呼び出すのは大丈夫そう

class Rational(n:Int, d:Int) {
    def this(n:Int) = this(n, 1)
    // 別の補助コンストラクタを呼び出す
    def this(s:String) = this(2)
    override def toString = n + "/" + d
}

// 実行結果:クラスパラメータが一つのものが呼び出されました
scala> val r1 = new Rational("hoge")
r1: Rational = 2/1

次にコンストラクタに関わらない処理を書いてみます。。。まあ当然だめですけども

class Rational(n:Int, d:Int) {
    def this(n:Int) = this(n, 1)
    // 別処理します
    def this(s:String) = println("hoge")
    override def toString = n + "/" + d
}

// コンパイラ様に怒られます
<console>:4: error: 'this' expected but identifier found.
           def this(s:String) = println("hoge")

んじゃ、別の補助コンストラクタを呼び出してから別処理してみますと…成功するのでした

class Rational(n:Int, d:Int) {
    def this(n:Int) = this(n, 1)
    // 別の補助コンストラクタを呼び出して、別処理
    def this(s:String) = this(4);println("hoge")
    override def toString = n + "/" + d
}

// こちらは無事に行われましたー
scala> val r1 = new Rational("hoge")                   
hoge
r1: Rational = 4/1

ちなみにコンストラクタ呼び出しと別処理(println)を逆転するとコンパイラに怒られますー

ざざっと自分なりのまとめ
  • 補助コンストラクタは複数定義可能
  • 補助コンストラクタで必要とするクラスパラメータは自由自在
  • 補助コンストラクタは最初の式(処理の最初)に他のコンストラクタを呼び出す必要がある

…ということなので、クラスパラメータのを編集するような前処理には使えなさそうだなぁ…でも、以下の2通りならクラスパラメータを無理やり前処理できなくも無いのか…

  • 基本コンストラクタのクラスパラメータを無くして、各補助コンストラクタ内でval(もしくはvar)フィールドを定義
  • フィールドをvar型とかで定義して、コンストラクタを実行後に上書き

とりあえず、クラスパラメータにパターンを持たせてオブジェクト生成の基本処理をゴニョゴニョしたいときに使ってみようかなーっと、C++とかPL/SQLなんかで似たようなことやったような気がするなぁ…

おまけ

スーパークラスのコンストラクタを呼び出せるのは基本コンストラクタのみデス

また、補助コンストラクタは他の(基本、補助どちらかの)コンストラクタを呼び出さなければならない…ということは最終的には基本コンストラクタが呼び出されるわけなので、呼び出し順をちゃんと考慮さえすれば基本コンストラクタでスーパークラスが呼び出せれば問題ないような気がするのですよ

ちなみに補助コンストラクタ同士で完結した呼び出しを定義すると当たり前のように怒られますた

class Rational(n:Int, d:Int) {
    // 補助1: 補助コンストラクタ2を呼び出し
    def this(n:Int) = this("hoge")
    // 補助2: 補助コンストラクタ1を呼び出し
    def this(s:String) = this(4)
    override def toString = n + "/" + d
}

//まあ、怒られるわな
<console>:6: error: called constructor's definition must precede calling constructor's definition
           def this(n:Int) = this("hoge")

非公開フィールドとメソッド

外部に非公開なものはprivate宣言をすればOKってことで、privete宣言を使って基本コンストラクタに正規化処理を付け加えますよー、ちなみにここでいう正規化は約分のことですね

正規化処理の概要

ここまで作ってきたクラスでは(2/4)をそのまま約分せずに使ってますが、こいつを(1/2)として扱わないのはあまり格好がよろしくないので正規化(約分)の処理を追加しますー

有理数の約分をするには分母と分子を各々の最大公約数で割ればいいので、最大公約数を求めるメソッドとその結果を格納するフィールドを定義しますデス

んじゃ、実装しますー

上で出てきた処理の概要を(簡易的にした)Rationalクラスに実装してみます。なお、この最大公約数に関する処理もろもろはRationalクラスを扱う際には外部からアクセスする必要のないものなので非公開にしてしまいます。

class Rational(n:Int, d:Int) {
    // 最大公約数をもとめます: absは絶対値を求めるメソッド
    private val g = gcd(n.abs, d.abs)
    // 最大公約数で割った値を分子分母として格納
    val numer = n / g
    val denom = d / g
    override def toString = numer + "/" + denom
    // 最大公約数を計算するメソッド
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
}

正規化出来るか試してみますよ

scala> val r1 = new Rational(2, 4)
r1: Rational = 1/2

無事できてますねー

非公開っぷりをためしてみる

まあ念のためという名の好奇心で試してみますよー

scala> val r1 = new Rational(2,4)
r1: Rational = 1/2
// フィールドgに拒否られました
scala> r1.g
<console>:7: error: value g cannot be accessed in Rational
       r1.g
          ^
//  メソッドgcdにも怒られまった
scala> r1.gcd(2,4)
<console>:7: error: method gcd cannot be accessed in Rational
       r1.gcd(2,4)

動作的にOKですね

メソッドgcdについて脱線しまくってみる

最大公約数を求める非公開メソッドgcdは再帰呼び出しでループ処理を行ってますねー再帰ってのが関数型っぽいですね(偏見?)

この再帰を使った最大公約数を求めるアルゴリズムwikipedia:ユークリッドの互除法をつかったものなんですが、ここにある適当な説明よりもググったほうが良いと思われます。


・すっかり忘れているので自己満足的なメモ

ユークリッド互除法は2つの整数A, Bの最大公約数と、B, (A % B)の最大公約数が等しい性質を利用するアルゴリズムで必ずA > A % B > (A % B) % Bになるので

gcd(A, B) = gcd(B, (A % B)) =  gcd((A % B), B % (A % B) ) ... = gcd(C, 0) 

となることから最大公約数Cを求めることができる( gcdは最大公約数を求める演算)


2つの整数のA, Bの最大公約数と、B, (A % B)の最大公約数が等しい証明っぽいものは次

AとBの最大公約数を求める場合

A÷Bの商がrで余りがqならば A = B * r + qで表現できる ...(1)

ここでAとBの最大公約数をGとするとA = G * a, B=G * b (a,bは互いに素)...(2)

(2)を(1)式に代入すると G * a = G * b * r + qになる ...(3)

(3)を整理してGでまとめるとG(a - b * r) = qよりGはqの約数となる...(4)

ここでq = G  * q'として(4)式に代入するとG(a - b * r)= G * q'...(5)

(5)式を整理するとa = b * r + q'  になる...(6)


ここでbとq'に公約数gが存在すると仮定してb = g * b', q' =g * q''とすると

a = g * b' * r + g * q''より a = g ( b' * r + q '')となり、aとgに公約数gが存在する事になってしまうため、aとbが互いに素であるという仮定に矛盾する

従ってbとq'は互いに素であり、もともとB = G * b, q = G * q'と定義してあったのでGはBとqの最大公約数であると言える


…すっかり錆び付いているのでツッコミ希望デス。あとgcd(C, 0) とかって正しいのかしら?

いじょー

前回までに育てたクラスに補助コンストラクタと正規化を追加するとこんな感じですねー

class Rational(n:Int, d:Int) {
    // 分母が0で無いことをチェック
    require(d != 0)
   // 分子分母の最大公約数を取得
    private val g = gcd(n.abs, d.abs)
    // 正規化した分子分母をフィールドとして格納
    val numer = n / g
    val denom = d / g
    // 分子のみをクラスパラメータとする補助コンストラクタを設定
    def this(n:Int) = this(n, 1)
    // クラスのtoString表現を見やすく上書き
    override def toString = numer + "/" + denom
    // 2つのRationalオブジェクトを加算するメソッド
    def add(other: Rational):Rational = {
        new Rational(
            numer * other.denom + other.numer * denom,
            denom * other.denom
        )
    }
    // 2つのRationalオブジェクトを比較してレシーバのほうが小さいか判定するメソッド
    def lessThan(other:Rational) = {
        this.numer * other.denom < other.numer * this.denom
    } 
    // 2つのRationalオブジェクトの大きい方を返すメソッド
    def max(other: Rational) = {
        if(this.lessThan(other)) other else this
    }
    // 最大公約数を計算するメソッド
    private def gcd(a:Int, b:Int):Int = {
        if(b == 0) a else gcd(b, a % b)
    }
}

日本戦だのなんだのと週末に時間が取れなかったので、今回はこのへんでー

次回は9節の内容で演算子を定義しますよー