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


Scalaコップ本の20章に入っていきますよー、20章は抽象メンバーについてやっていきますー。抽象メンバーは抽象メソッドだけでなく抽象フィールドや抽象型なんかも含む普遍性のある存在みたいですな(`・ω・´)

抽象メンバーの弾丸ツアー

とりあえず抽象メンバーのサンプルを見てみますよ(`・ω・´)それにしてもコップ本は弾丸ツアーって表現が好きだなぁ(´・ω・`)

trait Abstract{
  // 抽象型ですな
  type T
  // コレは抽象メソッドで
  def transform(x:T):T
  // こっちは抽象フィールドです
  val initial:T
  var current:T  
}

それでは上記抽象トレイトを具象実装してみますよ

class Concrete extends Abstract {
  // 型を実装してみますよ
  type T = String
  // メソッドの実装です
  def transform(x:String) = x + x
  // フィールドを実装します
  val initial = "hi"
  var current = initial
}

型の実装ってこうやるのか(´・ω・`)とちょっと新しいことを知った気分です。この後でソレゾレの詳細についてやっていきますよー

型メンバー

Scalaの抽象型はtypeキーワードによって定義着を指定せずに宣言された型のことらしいです。そしてなんらかのクラスやトレイトのメンバーとして宣言されるみたいで、このようにメンバーとしての型を型メンバーと呼ぶみたいです。なんだか語感的に稲垣メンバーを思い出してしまうなぁ(´・ω・`)

ちなみに非抽象(具象)型メンバーは、型の新しい名前(別名)を定義するような手段と考えることも出来るみたいです。例えば先に出した例のConcreteクラスにおける型TはStringの別名として扱えたりするみたいですねー

だったらおとなしくStringで定義すりゃいいじゃん(´・ω・`)とか思うのですが、型メンバーを定義する理由としては、実際の名前が長くて使い辛い時のための別名としての宣言と、サブクラスごとに定義が変わるような場合の抽象型としての宣言の2パターンだそうです。

抽象val

抽象valは次のように変数名と型のみの宣言になるっすね

// 抽象val initialを宣言しますよ
val initial:String

んで、こいつをサブクラスで実装するときは値を与えることで具象定義するみたいですね

// 具象定義してやりますぜ(`・ω・´)
val initial = "hi"

抽象valはサブクラスごとに変更できない値を定義するためのもので、次のようなパラメータなしの抽象メソッド宣言ににてますねー

def initial:String

Scala的にval initialとdef initial(パラメータなし)はクライアントコードから見れば同じものにみえるわけですが、valのほうは参照するたびに同じ値を返されてdefの方は変わる可能性があるYO!という違いがあるのです(´・ω・`) なので抽象valをdefでオーバライドすることはできないそうです…まあ逆に抽象defを具象valでオーバーライドするのはOKだったりするらしいですが

// 抽象メンバーを持ったFruitクラスを定義しますよ
abstract class Fruit {
  val v:String
  def m:String
}

// Fruitを継承したAppleクラスを考えます
abstract class Apple extends Fruit{
  val v:String
  // 抽象defをvalでオーバライドするのはOKですね
  val m:String
}

//  Fruitを継承したBadAppleクラスを考えます
abstract class Apple extends Fruit{
  // 抽象valをdefでオーバーライドするのはダメですね
  def v:String
  def m:String
}

// ちなみにvalをdefで無理やりオーバライドすると
// 次のようなエラーが出力されますね
<console>:7: error: error overriding value v in class Fruit of type String;
 method v is not stable
         def v:String
             ^

とりあえずdef→valはOKでval→defはダメYO!って覚えておけばいいですかねc⌒っ゚д゚)っφ メモメモ...それにしてもダメなAppleってナンだろ?Jobsのいなかった暗黒時代のアレかしら?

抽象var

抽象varは抽象valのvarバージョンみたいですね。とりあえず変数名と型のみの定義ですねー

trait AbstractTime {
  // 抽象varを定義しますよ
  var hour:Int
  var minute:Int
}

抽象varを定義した瞬間、抽象var用のsetterとgetterメソッドが定義されるみたいです。でも実際に値を格納するvar自身はサブクラスで具象実装するまで存在しないので、setterやgetterのメソッドはあっても使えない…と

とりあえず、上記AbstractTimeトレイトは次のように展開されるみたいですね

trait AbstractTime {
  // hourのgetterです
  def hour:Int
  // hourのsetterです
  def hour_=(x:Int)
  // minuteのgetterです
  def minute:Int
  // minuteのsetterです
  def minute_=(x:Int)
}

getterやsetterは抽象var定義時に既に用意されるけど変数自体が具象化されていないから使えません(`・ω・´)っていうのはおもしろいですね。

抽象valの初期化

抽象valを上手く使うことでスーパークラスのクラスパラメータと似た様な役割を果たすことができるみたいです。特にコンストラクタを持たないのでパラメータを渡すことのできないトレイトではこの役割が超重要になるみたいですね(`・ω・´)なのでトレイトにパラメータを与えたい場合などはこの動作を使うのがいいみたいです。

例えば6章で扱った有理数クラスRationalをトレイトに再構成する場合にはクラスパラメータを取れないので抽象valを使うようにしてみるのです

// 抽象valで分母と分子の値を定義してやります
trait RationalTrait {
  val numerArg:Int
  val denomArg:Int
}

そのうえで具象インスタンスを実装する場合には次のようにすればOKとのことです

// 具象valを定義することで具象インスタンスを定義しますね
new RationalTrait{
  val numerArg = 1
  val denomArg = 2
}

…つーか、こうすればトレイトのインスタンス作れるのね(´・ω・`)知らんかった。すっかりできないものだと勘違いしてました…まあ、実際の上記動作はトレイトをミックスインした匿名クラスを生成したうえでそのインスタンスの生成を行っているみたいなので、実際のところはトレイトのインスタンスでは無いらしいんですが(´・ω・`)

クラスインスタンスとトレイトインスタンスの違い

トレイトでも擬似的にインスタンスを生成することが出来るようなんですが、次のようにパラメータとして式がが渡された場合の評価タイミングが変わるそうです。

例えば次のようなクラスインスタンス生成では、クラスの初期化前に式の評価が行われますねー

// 初期化前に式の評価が行われるので
// クラスの初期化に式を使えますネ(´・ω・`)
new Rational(expr1, expr2)

それに対して次のようなトレイトインスタンスの生成では匿名クラスの初期化後に式の評価が行われるので、初期化時に該当の式を利用できないという問題が発生するみたいです。

new RationalTrait {
 // (匿名)クラス初期化後に式の評価が行われるので
 // 初期化処理に式の内容を利用することができないみたいです
 // 正確にはデフォルト値(今回だと0)が返ってくるみたいですが(´・ω・`)
  val numerArg = expr1
  val denomArg = expr2
}

上記トレイトでは初期化時にパラメータ(風)が使えないという問題は出ないのですが、こんな感じの定義だったりすると問題が発生するので気をつけましょーね(´・ω・`)とのことです

trait RationalTrait {
  val numerArg:Int
  val denomArg:Int
  // トレイトの初期化時に抽象valを利用する定義です
  // この例では抽象valnumerArgとdenomArgにデフォルト値0が適用されます
  require(denomArg != 0)
  private val g = gcd(numerArg, denomArg)
  val numer = numerArg / g
  val denom = denomArg /g
  private def gcd(a:Int, b:Int):Int =  if(b == 0) a else gcd(b, a % b)
  override def toString = numer + "/" + denom
}

実際にはこんな風なエラーが発生しますねー

scala> val x = 2
x: Int = 2

scala> new RationalTrait {
     |   val numerArg = 1 * x
     |   val denomArg = 2 * x
     | }
java.lang.IllegalArgumentException: requirement failed
	at scala.Predef$.require(Predef.scala:107)
	at RationalTrait$class.$init$(<console>:8)
	at $anon$1.<init>(<console>:7)
	at .<init>(<console>:7)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0...
scala> 

これはdenomArgの値がトレイト初期化時にデフォルト値0になってしまったtameni
require呼出で例外が投げられたためのエラーでした。抽象valの実装は評価タイミングでクラスパラメータと異なりますYO!っていうサンプルでしたー

ちなみにScalaには上記のような問題に対処する方法がとして事前初期化済みフィールドと遅延評価valの2つが用意されているとのことです。

事前初期化済みフィールド

スーパークラスが呼び出される前にサブクラスのフィールドを初期化する方法として事前初期化済みフィールドを定義する方法があります。これはフィールド定義をスーパークラスコンストラクター呼出よりも前に書いてしまえば良いらしいです…と言われてもよくわからないのでサンプル書いてみますね(´・ω・`)

// 無名クラスで使ってみますよ
new {
  val numerArg = 1 * x
  val denomArg = 2 * x
} with RationalTrait // extendsなしwithでスーパートレイト呼出です

上の例では無名クラスでの呼出でしたが、事前初期化済みフィールドはオブジェクトや名前付きサブクラスでも使えるみたいです。

例えばオブジェクトでもこんなふうに使えますねー

object twoThirds extends {
  val numerArg = 2
  val denomArg = 3
} with RationalTrait

名前付きのサブクラスでも無問題です(`・ω・´)

// トレイトをミックスインする形で書きますね
// 実際にはトレイとをミックスインする
// 事前初期化済みフィールドを持つ匿名クラスの継承って感じでしょうか…
class RationalClass(n:Int, d:Int) extends {
  val numerArg = n
  val denomArg = d
} with RationalTrait {
  // 有理数の加法メソッドですな
  dev + (that:RationalClass) = new RationalClass(
    numer * that.denom + that.numer + denom,
    denom * that.denom
  )
}

うん、これができるようになると抽象フィールドをトレイトで精一杯利用出来るような気がするのでかなり便利になりそうですねー、やっぱりトレイト面白いっす(`・ω・´)

ちなみに初期化順の都合上、事前初期化済みフィールドが初期化中にでthis参照を使うと、参照先は構築中のオブジェクトやクラスを生成しているオブジェクト(インタープリタで生成)になるみたいですねー。ちょっと試してみるとこんな感じになるそうですねー

scala> new {
     |   val numerArg = 1
     |   val denomArg = this.numerArg * 2
     | } with RationalTrait
// thisで参照する先(自動生成された$iw)にnumerArgがないので
// 存在しないよエラーが発生しますね
<console>:8: error: value numerArg is not a member of object $iw
         val denomArg = this.numerArg * 2
                             ^

遅延評価val

事前初期化済みフィールドを使うと初期化の順序を入れ替えることができるのでクラスコンストラクター引数のような初期化動作を擬似的に行うことができるのですが、初期化の順序をもう少し細かく制御したい場合はval定義を遅延評価させることでやれるみたいですねー

では実際に遅延評価をやってみますよー、とりあえずこんなオブジェクトを考えてみます

object Demo {
  val x = {
    // 初期化時にメッセージを表示します
    println("initializing x")
    // 値を設定しますね
    "done"
  }
}

//とりあえず実行してみます
scala> Demo
// xが初期化されました
initializing x
res3: Demo.type = Demo$@c18e99

scala> Demo.x
res4: java.lang.String = done

それではlazy修飾子をつけた修正を行って遅延評価をやってみますよー

object Demo {
  // lazy修飾子を付与して遅延評価します 
  lazy val x = {
    // 初期化時にメッセージを表示します
    println("initializing x")
    // 値を設定しますね
    "done"
  }
}

// さて実行してみますかねー
// オブジェクト初期化時にvalの初期化されませんでした
scala> Demo
res5: Demo.type = Demo$@17e4cec

// valの呼び出し時に初期化されました
scala> Demo.x
initializing x
res6: java.lang.String = done

// 初期化は済んでいるのでもう呼び出されません(´・ω・`)
scala> Demo.x
res7: java.lang.String = done

遅延評価を利用すると、そのvalが初めて呼ばれるときに初期化されるように調整することが出来るみたいですね。ちなみにオブジェクト自体も初めて使われるときに初期化されるものなので、オブジェクトの内容を記述する無名クラスを使った遅延評価val呼び出しの略記法と解釈することが出来るみたいです。

scala> object Demo {
     |   val x = {
     |     // 初期化時にメッセージを表示します
     |     println("initializing x")
     |     // 値を設定しますね
     |     "done"
     |   }
     | }
defined module Demo

// オブジェクトの 他の要素を呼び出さなければ
// 目的のvalの呼び出し時に初期化されるのと同義になるデス
scala> Demo.x
initializing x
res9: java.lang.String = done

scala> Demo.x
res10: java.lang.String = done

せっかくなので遅延評価valを利用してRationalTraitを書き直してみますかねー

trait LazyRationalTrait{
  val numerArg:Int
  val denomArg:Int
  // 抽象valにかかわるので遅延評価しますよ
  lazy val numer = numerArg / g
  lazy val denom = denomArg / g
  override def toString = numer + "/" + denom
  // 抽象valにかかわるので遅延評価しますよ
  private lazy val g = {
    // 遅延評価のタイミングでrequireが実行されるように変更です
    require(denomArg != 0)
    gcd(numerArg, denomArg)
  }
  private def gcd(a:Int, b:int):Int = {
    if(b == 0) a else gcd(b, a % b)
  }
}

上記コードでは抽象valにかかわる箇所にlazyを付与して評価タイミングを遅延させてマス。ついでに前回例外の原因になったrequireを遅延評価される非公開フィールド内に移動されたので、初期化タイミングのズレには対応できるはずです。


そんなわけで試してみますかね

scala> val x = 2
x: Int = 2

scala> new LazyRationalTrait {
     |   val numerArg = 1 * x
     |   val denomArg = 2 * x
     | }
res11: java.lang.Object with LazyRationalTrait = 1/2

おおー、できた(`・ω・´)。なんだか遅延評価で制御をするとうまい具合やれる上にコードにlazyとか入るのがかっこよさげですな(´・ω・`)

ちなみに上記処理の際の初期化手順は次のような流れになるみたいです

  • LazyRationalTraitの新しいインスタンスが生成
    • LazyRationalTaitの初期化コード(何も無い)が実行
    • LazyRationalTaitのフィールドは未初期化のまま
  • new式によって定義された無名サブクラスの基本コンストラクターが実行
    • numerArgが2, denomArgが4で初期化される
  • numerフィールドがLazyRationalTraitのtoStringメソッドによって実行
    • numerの値が評価される
    • numerの初期化子が非公開フィールドgにアクセスしgが評価
    • gの評価時にnumerArgとdenomArgにアクセスが発生
  • toStringがdenomにアクセスするのでdenomが評価される
    • denomの評価でdenomArgとgにアクセスが発生(gは既に初期化済)
  • 結果値1 /2が構築、表示

ちなみにlazyによる遅延評価は副作用がある場合や副作用に依存する場合は危険らしいです。副作用がある場合は初期化順序がかなり重要視されるのでlazyによる実行順序の変化をトレースするのが超大変になるからみたいですねー、なのでlazy利用は関数型に向いているそうです(´・ω・`)Haskell様なんかが遅延評価を得意とするみたいです

いじょー

とりあえず今回はこのへんでー、次回は抽象型なんかをやりますよー