Open5

Scala学習記録

unouunou

scala メモ

名前渡しパラメーター

名前渡しパラメーターは呼び出しの前に評価されないので副作用を起こしにくい

var assertionEnabled = true

// 呼び出しに()が必要 myAssert(() => 5 > 3)
def myAssert(predicate: () => Boolean) = 
  if (assertionEnabled && !predicate())
    throw new AssertionError

// 名前渡し()不必要 byNameAssert(5 > 3) 
//  assertionEnabledがfalseのときはbyNameAssert(x / 0 == 0) が例外を起こさない
def byNameAssert(predicate: => Boolean) = 
  if (assertionEnabled && !predicate())
    throw new AssertionError

//  assertionEnabledがfalseのときでもbooleanAssert(x / 0 == 0) が例外を起こす
def booleanAssert(predicate: Boolean) = 
  if (assertionEnabled && !predicate)
    throw new AssertionError

メソッドとフィールドのオーバーライド

Scalaはフィールドとメソッドが同じ名前空間に属しているので、フィールドがパラメータ無しメソッドをオーバーライドすることができる。
Scalaの名前空間は以下2つ

  • 値(フィールド、メソッド、パッケージ、シングルトンオブジェクト)
  • 型(クラス、トレイト)

継承と合成

コードの再利用性を考えるなら合成を選んだ方がいい。
(継承はスーパークラスを書き換えるとサブクラスが使用できなくなる可能性がある)
継承関係を持たせる場合is-a関係を持っているか確認するべきである。

ファクトリオブジェクトの利用

ファクトリオブジェクトを定義することによって、階層構造を隠蔽することができ、
ファクトリーメソッドを用いることでオブジェクトの管理を一元化することができる。
また、それによってサブクラスに外部からアクセスする必要がなくなるため、ファクトリオブジェクトの中で、サブクラスをprivate宣言することができる。

ミュータブルオブジェクト

純粋関数型オブジェクトとは、メソッドを呼び出したり、フィールドを間接参照したりした時に必ず同じ値が返されるものである。
varの定義を含んでいるかどうかは必ずしもイコールではない。
Scalaの再代入可能な変数(オブジェクトのメンバーになっているvar)には暗黙のうちにゲッター/セッターメソッドが定義される。
var x のゲッター名はx、セッター名はx_=。

class Time {
  var hour = 12
}

この実装は以下と同じになる。(ローカルフィールドのhは既存の名前と衝突しなければなんでもいい)

class Time {
  private[this] var h = 12
  def hour: Int = h
  def hour_ = (x: Int) = { h = x }
}

このようにゲッターやセッターが生成されるが、直接ゲッターとセッターを定義することもできる。
例えば以下に示すように変数への値の取得・代入に独自の解釈を加えることができる。

class Time {
  private[this] var h = 12
  def hour: Int = h
  def hour_ = (x: Int) = {
    require(0 <= x && x < 24)
    h = x
  }
}

上記の例では無効な値が代入されないように変数を保護したが、変数へのアクセスログをとることもできるし、イベントと統合することで変数の変更のたびに通知を送ることもできる。

対応するフィールドを持たないゲッターやセッターを定義することも可能である。以下は温度を摂氏と華氏の両方を表示・変更できるクラスである。

class Thermometer {
  var celsius = _
  def fahrenheit = celsius * 9 / 5 + 32
  def fahrenheit_ = (f: Float) = {
    celsius = (f - 32) * 5 / 9
  }
  override def toString = s"$fahrenheit F / $celsius C"
}
unouunou

暗黙の型変換とパラメーター

Scalaではimplicitによって、既存の型を拡張したり、明示的にパラメーターを渡さずに関数に値を渡すことができる。

暗黙の型変換

以下のような分数を表すクラスがあったとする

class Rational(val n: Int, val d: Int) {
    def +(that: Rational): Rational = {
      val num = this.n * that.d + this.d * that.n
      val den = this.n * that.d
      new Rational(num, den)
    }

    override def toString: String = s"$n / $d"
  }

+メソッドによって分数同士を足し合わせることはできるが、1や2のような整数とは足し算ができないのでエラーがでる。

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

scala> 1 + oneHalf      // error

そこで、暗黙の型変換を以下のように書くとコンパイラが自動で関数を適用して、型の変換を行ってくれるようになる。

scala> implicit def Int2Rational(n: Int): Rational =  new Rational(n, 1)
warning: ...
def Int2Rational(n: Int): Rational

scala> 1 + oneHalf
val res0: Rational = 3 / 2      // errorが出ない!

もちろん多用は禁物。
Scala 2.1.0から暗黙のクラスが追加されたので、基本的にはそちらを使う。
使い方は def の前の implicit を class の前に動かすだけ。

暗黙のパラメーター

先ほどと違い変換ではなく、
some(a)を some(a)(b)に置き換えて足りないパラメーターを補完する。

例えば、ユーザーの苗字、ミドルネームをカプセル化するLastName、MiddleNameクラスがあったとする。

class LastName(val name: String)
class MiddleName(val name: String)

また、Greeterオブジェクトがあり、その中には2個のパラメータを取るgreetメソッドが含まれており、第2引数にはLastNameとMiddleNameを取り、implicitが付いている。こうすることでパラメーターが暗黙のうちに供給される。

 object Greeter {
    def greet(firstName: String)(implicit lastName: LastName, 
           middleName: MiddleName): Unit = {
      println(s"My name is $firstName ${lastName.name} ${middleName.name}")
    }
  }

そして、必要とされる型(今回はLastNameとMiddleName)の変数を定義する。
HogeNamesオブジェクトに以下のようなコードを入れる

object HogeNames {
    implicit val sName: LastName = new LastName("fuga")
    implicit val mName: MiddleName = new MiddleName("piyo")
  }

val自体にimplicitをつける。こうすることで、コンパイラがこの値を使えるようになる。

あとは、implicitで定義した変数が同じスコープ内にあれば良いのでimportして関数を呼び出せばあとはコンパイラーがうまいことしてくれる。

scala> import HogeNames._
import HogeNames._

scala> Greeter.greet("hoge")
My name is hoge fuga piyo

このように明示的にパラメータを渡さなくても、自動で適用される。

まとめ

Scalaのコンパイラーは賢い、えらい

unouunou

抽象メンバー

trait RationalTrait {
  val numerArg: Int
  val denomArg: Int
}

というトレイトがあったとすると、

new RationalTrait{ expr1, expr2 }

と呼び出すと、このトレイトをミックスインし、本体で定義されている「無名クラス」のインスタンス
を生成することができる。

new Rational(expr1, expr2)

と似ているが、式が初期化される順序に違いがある。
Rationalクラスの場合はexpr1とexpr2の2つの値が評価されたあとにクラスが作成されるが、
無名クラスの場合はクラスが作られた後、2つの値が評価される。(評価されるまではデフォルト値、Intの場合は0を保持している)
なので、以下のような場合はエラーになる

trait RationalTrait {
  val numerArg: Int
  val denomArg: Int
  require(denomArg != 0)
}

// REPL
scala> new RationalTrait {
   val numArg: Int = 1
   val denArg: Int = 2
 }

// エラー
java.lang.IllegalArgumentException: requirement failed

対処法

事前初期化済みフィールドと遅延評価の2種類の解決法がある。

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

スーパークラスが呼び出される前にサブクラスのフィールドを初期化できるようにするもの。
中括弧で囲まれたフィールド定義をスーパークラスのコンストラクタより前に書けば良い

// エラーが出ない
new {
   val numArg: Int = 1
   val denArg: Int = 2
 } with RationalTrait

// オブジェクト定義に含まれる事前初期化済みフィールド
object OneHalf extends  {
   val numArg: Int = 1
   val denArg: Int = 2
 } with RationalTrait

// クラス定義に含まれる事前初期化済みフィールド
class Rational(n: Int, d: Int) extends  {
   val numArg = n
   val denArg = d
 } with RationalTrait

事前初期化済みフィールドの中でthisを使うと、構築中のオブジェクト自体ではなく、構築中のオブジェクトやクラスを含んでいるオブジェクトになる。

object Example {
  class Rational ( ... ) { ... } ...
}

3つ目の例が上記のようになっていた場合、事前初期化済みフィールドの中で呼び出したthisはExampleを参照する。

2. 遅延評価

val定義の前にlazy修飾子をつけると、valが初めて使われるまで評価をしない。また、再度評価されず、一度目の評価で代入された値が返される。
以下のようにRationalTraitの定義を変更する。(lazyは抽象メンバーには使えない)

trait RationalTrait {
    val numArg: Int
    val denArg: Int
    lazy val num: Int = numArg / g
    lazy val den: Int = denArg / g
    private lazy val g: Int = {
      require(denArg!=0)
      gcd(numArg, denArg)
    }
    override def toString: String = "$num / $den"
    
    @tailrec
    private def gcd(a: Int, b: Int): Int = 
      if (b == 0) a else gcd(b, a%b)
  }

そうすると、以下のような順序で評価がされる。
①RationalTraitの新しいインスタンスが生成される(フィールドはデフォルト値だが、requireの位置がlazyで宣言したフィールドの中に変更されているため、エラーが出ない)

②newによって無名クラスが生成され、コンストラクターが実行される。numArgとdenArgが初期化される。

③toStringメソッドによってnumにアクセスされ、評価される。

④num初期化子によってgにアクセスされる(この時既にdenArgは初期化されており、値が入っているのでエラーが出ない。)

⑤toStringメソッドによってdenにアクセスされ、評価される。また、④と同じようにgへのアクセスがあるが既に評価されているので改めて評価されず、値だけが返される。

⑥最後に、結果値の文字列が構築され表示される

遅延評価のもう一つのメリットはテキスト上での順序が実行順序に影響を与えないことだが、
このメリットが得られるのは遅延評価valの初期化が副作用を起こさない場合だけである。
そのため、遅延評価valは命令型で書かれているコードには適さず、関数型のコードに適している。

抽象型

FoodクラスとeatメソッドをもつAnimalクラスがあるとする。

class Food
abstract class Animal {
  def eat(food: Food)
}

次に、これら2つのクラスを特化させるためにGrassを食べるCowクラスを作る。

class Grass extends Food
class Cow extends Animal {
  override def eat(food: Grass) = {}
}

しかし、これはコンパイラを通らない。
上記の定義だと厳密にパラメータが一致する必要があるので以下のように抽象型を使い
それぞれの動物にあった餌を食べられるようにすればよい。

class Food
abstract class Animal {
  type SuitableFood <: Food
  def eat(food: SuitableFood)
}

class Grass extends Food
class Cow extends Animal {
  type SuitableFood = Grass
  override def eat(food: Gras) = {}
}