Scalaスケーラブルプログラミング第3版 読書メモ

Mar 9, 2017 ( Feb 11, 2022 更新 )

やりたいこと

Gatling DSLの拡張

やりたいことを達成するために理解すべきこと

  • 関数・クラス定義の基礎的な部分
    • 第2章
    • 第3章
    • 第4章
    • 第5章
    • 第10章
    • 第11章
  • trait
    • 第12章
  • implicit
    • 第21章

  • implicit classと普通のclassの違い *

第1章

  • Scalaは純粋なオブジェクト指向言語

    • すべてはオブジェクトであり型を持っている
  • 暗黙の型変換

    • ScalaはJavaの型を再利用している
    • String.toIntというメソッドはJavaにはない。このコードが現れるとScalaのコンパイラは暗黙的にScalaのStringOps型に変換する。結果として、String.toIntが使えるようになる。
  • 述語関数(predice)

    • 結果型がBooleanとなる関数リテラルは、述語関数と呼ばれる
    • _.isUpperは、アンダースコアによって表される引数をとり、それが大文字化どうかを検証する。戻り値はBoolean

第2章

関数

関数の引数の型指定は必須

scala> def max(x: Int, y: Int): Int = { if (x > y) x else y }
max: (x: Int, y: Int)Int

scala> max(1, 2)
res6: Int = 2

結果型の指定はOptional。再帰関数の場合には型定義が必要。

scala> def max2(x: Int, y: Int) = { if (x > y) x else y }
max2: (x: Int, y: Int)Int

scala> max2(10,2)
res8: Int = 10
scala> def fib(x: Int) = { if (x == 0 || x == 1) x else fib(x - 2) + fib(x - 1) }
<console>:11: error: recursive method fib needs result type
       def fib(x: Int) = { if (x == 0 || x == 1) x else fib(x - 2) + fib(x - 1) }
                                                        ^

scala> def fib(x: Int): Int = { if (x == 0 || x == 1) x else fib(x - 2) + fib(x - 1) }
fib: (x: Int)Int

scala> fib(10)
res9: Int = 55

Unit型

Unit型はJavaのvoidと似ており、意味のない値を返すことを示す型。 結果型がUnit型ということは、副作用のために実行されるということ。

scala> def greet() = println("hello")
greet: ()Unit

関数リテラル

下記のようにforeachに関数リテラルを渡して繰り返し処理ができる。 後述するが、関数リテラルの書き方とLambdaの書き方は異なる。 関数リテラルの引数は型推論されるが、Lambdaの引数は型推論されない。(通常の関数定義と同じ)

args.foreach(arg => println(arg))
Lambda

通常の関数定義と同じく引数の型定義は必須になっている。

scala> val max3 = (x: Int, y: Int) => if (x > y) x else y
max3: (Int, Int) => Int = $$Lambda$1231/2005488238@5fafd099

scala> val max4 = x, y => if (x > y) x else y
<console>:1: error: ';' expected but ',' found.
val max4 = x, y => if (x > y) x else y
            ^

scala> val double = x => x * 2
<console>:11: error: missing parameter type
       val double = x => x * 2

第3章 Scalaプログラミングの次の一歩

配列

メソッド

for (i <- 0 to 2)
  print(greetString(i))

0 to 2(0).to(2)と同等。

  • あるオブジェクトのメンバメソッドを呼び出すとき
  • そのメンバメソッドの引数が1つのとき

の場合にのみ、ドットと括弧を省略することができる。

scala> (0).to(2)
res21: scala.collection.immutable.Range.Inclusive = Range 0 to 2

Scalaではすべての演算がメソッド呼び出しとなる。 そのため、+演算子は以下と同義である。

scala> (1).+(2)
res25: Int = 3

オブジェクトのメソッド呼び出しの基本原則

  • 括弧内に何かの引数を納めてオブジェクトに適用すると、それはapplyメソッド呼び出しに変換される。
  • 括弧で囲まれた1個以上の引数を伴う変数への代入は、コンパイラはupdateメソッド呼び出しに変換する。

scala> val myMessages: Array[String] = new Array[String](2)
myMessages: Array[String] = Array(null, null)

scala> myMessages.update(0, "hello")

scala> myMessages.update(1, "world")

scala> myMessages
res31: Array[String] = Array(hello, world)

scala> myMessages(1) = "John"

scala> myMessages
res33: Array[String] = Array(hello, John)

配列を扱うための簡潔なインターフェース

scala> val myShortMessages = Array("hey", "hoo")
myShortMessages: Array[String] = Array(hey, hoo)

scala> val myShortMessages2 = Array.apply("hey", "hoo")
myShortMessages2: Array[String] = Array(hey, hoo)

※この例でのArrayはcompanion objectで定義されている。companion objectについては4章で説明がある。 Javaプログラマであれば、Arrayのstaticメソッドを呼び出しているのだと考えるとわかりやすい。

List

ScalaにはListという型がある。Listは常にimmutableである。

scala> val oneTwo = List(1,2)
oneTwo: List[Int] = List(1, 2)

scala> val threeFour = List(3,4)
threeFour: List[Int] = List(3, 4)

scala> val oneToFour = oneT
oneToFour   oneTwo

scala> val oneToFour = oneTwo ::: threeFour
oneToFour: List[Int] = List(1, 2, 3, 4)

scala> val twoThree = List(2,3)
twoThree: List[Int] = List(2, 3)

scala> val oneToThree = 1 :: twoThree
oneToThree: List[Int] = List(1, 2, 3)

メソッドを演算子的な記法で使うとき((0).+(1)0 + 1と同義である)、そのメソッド名の末尾がコロンの場合は右非演算子で定義される。 上記のコード例だと、1 :: twoThreetwoThree.::(1)と同義となる。

空のListはNilという型で表すことができる。 メソッドの演算子的な表記方法でListを作成する場合は、以下のように記述することができる。

scala> Nil
res0: scala.collection.immutable.Nil.type = List()

scala> val oneTwoThree = 1 :: 2 :: 3 :: Nil
oneTwoThree: List[Int] = List(1, 2, 3)

Tuple

Tupleには異なる型を含めることができる。

Tupleの要素にアクセスするにはTuple._1のようにアンダースコアと数字を指定する。

scala> val pair = (1, "hey")
pair: (Int, String) = (1,hey)

scala> pair._1
res1: Int = 1

scala> pair._2
res2: String = hey

collection

Set

Scalaにはimmutableなcollectionとmutableなcollectionの両方が定義されている。

mutableなcollectionを使うには、明示的にimportする必要がある。

varで定義したimmutable.Setは+=でオブジェクトを追加すると新しいSetを生成して返していることがわかる。

scala> var mySet = Set(1,2)
mySet: scala.collection.immutable.Set[Int] = Set(1, 2)

scala> mySet += 3

scala> mySet
res1: scala.collection.immutable.Set[Int] = Set(1, 2, 3)

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val MyMutableSet = mutable.Set(1,2)
MyMutableSet: scala.collection.mutable.Set[Int] = Set(1, 2)

scala> MyMutableSet += 3
res2: MyMutableSet.type = Set(1, 2, 3)

scala> java.lang.System.identityHashCode(mySet)
res3: Int = 477800054

scala> mySet += 4

scala> mySet
res5: scala.collection.immutable.Set[Int] = Set(1, 2, 3, 4)

scala> java.lang.System.identityHashCode(mySet)
res6: Int = 2131627443

scala> java.lang.System.identityHashCode(MyMutableSet)
   val MyMutableSet: scala.collection.mutable.Set[Int]
scala> java.lang.System.identityHashCode(MyMutableSet)
res7: Int = 1488803904

scala> MyMutableSet += 4
res8: MyMutableSet.type = Set(1, 2, 3, 4)

scala> MyMutableSet
res9: scala.collection.mutable.Set[Int] = Set(1, 2, 3, 4)

scala> java.lang.System.identityHashCode(MyMutableSet)
res10: Int = 1488803904

Map

->はScalaのすべてのオブジェクトが持つメソッドで、キーと値を返す2要素のTupleを返す。

scala> (0).->("one")
res11: (Int, String) = (0,one)

scala> import scala.collection.mutable
import scala.collection.mutable

scala> val myMap = mutable.Map[Int, String]()
myMap: scala.collection.mutable.Map[Int,String] = Map()

scala> myMap += (0 -> "one")
res12: myMap.type = Map(0 -> one)

scala> myMap
res13: scala.collection.mutable.Map[Int,String] = Map(0 -> one)

immutable.Mapの場合は、以下のように初期化する。

scala> val immutableMap = Map(0 -> "zero", 1 -> "one")
immutableMap: scala.collection.immutable.Map[Int,String] = Map(0 -> zero, 1 -> one)

第4章 クラスとオブジェクト

  • Scalaでは、明示的にアクセス修飾子をつけない場合はpublicになる。
  • Scalaのメソッドパラメータはvalでimmutable。
  • 明示的なreturnがない場合は、メソッドの最後のexpressionのreturn値を返す。
  • Scalaはメソッドの結果型を推論してくれるが、publicなメソッドの場合はそのメソッドの利用者がわかりやすいように結果型を記述すべきである。
  • 1行に複数の文を書く場合はセミコロンが必要である。
scala> val a = 1 println(a)
<console>:13: error: value println is not a member of Int
       val a = 1 println(a)
                 ^

scala> val a = 1; println(a)
1
a: Int = 1

以下のような場合は、x, +yの2つの文として解釈されてしまう。

x
+y

括弧で囲むことで回避できるが、+のような演算子は文の末尾に記述することがScalaの慣習となっている。

(x
+ y)
x +
y

シングルトンオブジェクト

Scalaはクラスにstatic memberを持たせない。 その代わり、singleton objectsという概念がある。このsingleton objectsにはvalを定義することができる。

singleton objectの定義には、classの代わりにobjectというキーワードを用いる。

singleton objectと同じ名前のclassがあるとき、このsingleton objectはclassのcompanion objectと呼ばれる。 また、classのほうはcompanion objectのcompanion classである。 この場合、classobjectは同じファイル内に定義されなければならない。

Javaユーザによっては、signleton objectsはstaticなものの置き場と考えることもできるかもしれない。 companion class/singleton objectの違いは以下である

  • class
    • valを定義できない
  • object
    • newでインスタンス化できない
      • ※ただし、Scalaの実行環境上で、コードで初めてアクセスされたときにシングルトンとしてインスタンス化される。object名でアクセス可能。

objectは型定義ではないが、super classを継承できるし、traitをミックスインできる。 例は13章で解説する。

companion classと同じ名前を共有しないsingleton objectsは、standalone objectsと呼ばれる。 standalone objectsは、ユーティリティのために使ったり、Scalaアプリケーションのエントリーポイントの定義のためなどに使われる。

standalone objectsを利用したScalaアプリケーションの起動

Scalaプログラムを実行するには、mainメソッドを持つstandalone singleton objectの名前を指定しなければいけない。

下記のScalaアプリケーションを起動するコードは、

object Main {
  def main(args: Array[String]) = {
    for(arg <- args) {
      println(arg)
    }
  }
}

下記のApp traitを継承したものと同じ挙動となる。 上記のstandalone singleton objectと同様にargsにもアクセスできる。

object MainAsApp extends App {
  for(arg <- args) {
    println(arg)
  }
}
  • Scalaではファイル名とobject, classの名前を一致させる必要はない。しかし、わかりやすさのためにJava同様に同じ名前にしておくのが慣習となっている。

第10章 合成と継承

実装を持たないclassをabstract classとして定義することができる。 Javaと同様に、absctract classのnewは行うことはできない。

  • レシーバーオブジェクト

Scalaでは、パラメータをとらず副作用を起こさないメソッドは()なしで定義するほうがよいとされている。

下記の副作用のないパラメータのないメソッドlengthは、

def length: Int = 10

下記のval lengthと同等であるからである。

val length = 10

println()のような副作用のあるメソッドは、フィールドアクセスなのかメソッド呼び出しなのか紛らわしくしないために括弧をつけることを推奨している。

継承

extendsは

  • public であるメンバを継承する
  • サブクラスで実装したパラメータや同じ名前のメンバはオーバーライドされる
  • フィールドがメソッドをオーバーライドすることもできるし、その逆もできる。
    • Scalaではフィールドとメソッドは同じ名前空間に存在する
    • そのため、フィールドとメソッドで同じ名前を使うことはできない
abstract class Element {
  def contents: Array[String]
  def height: Int = contents.length
  def width: Int = if (height == 0) 0 else contents(0).length
}

extendsは以下のように書ける。

class ArrayElement(conts: Array[String]) extends Element {
  override def contents: Array[String] = conts
}

上記のコードは、単にcontsパラメータをcontentsメソッドで返すだけとなっている。 このような場合は、以下のようにパラメータフィールドとしてElementのcontentsメソッドをオーバーライドできる。

パラメータフィールドとは、パラメータとして定義した変数をそのまま同名のフィールドに定義できる機能である。

class ArrayElement(val contents: Array[String]) extends Element {}

パラメータフィールドの定義にはvarも使うことが可能である。 また、パラメータフィールドには修飾子private,protected,overrideをつけることが可能。

また、overrideは抽象クラスしかない場合はつけてもつけなくてもよいが、 すでに実装があるメソッドをoverrideする場合は、override修飾子をつけないとコンパイルエラーになる。

final

継承を禁止したいときはfinal修飾子を使うことができる。 継承を禁止するclass,メソッドに付与することができ、final classやメンバをoverrideするとコンパイルエラーとなる。

スーパーコンストラクタの呼び出し

スーパークラスのコンストラクタは以下のように記述することができる。

class ArrayElement(val contents: Array[String]) extends Element {}

class LineElement(s: String) extends ArrayElement(Array(s)) {
  override def width: Int = s.length
  override def height: Int = 1
}

super classのパラメータとして、child classのパラメータを使うことができる。

多相性と動的束縛

Javaとは異なり、Scalaではoverrideしたメソッドは、たとえ親クラスとして定義されていても実態が子クラスであれば、小クラスのメソッドとなる。

abstract class Element {
  def name = "Element"
}

class ArrayElement extends Element {
  override def name = "ArrayElement"
}

class LineElement extends ArrayElement {
  override def name = "LineElement"
}

class UniformElement extends Element {}
def callName(e: Element): Unit = {
  println(e.name)
}

上記のクラス、メソッドを使った場合の結果は以下となる。

scala> callName(new ArrayElement)
ArrayElement

scala> callName(new UniformElement)
Element

ファクトリーobject

以下のように、objectとclassを使ってfactory objectの定義ができる。

abstract class Element {
  def contents: Array[String]
}

object Element {
  private class ArrayElement(val contents: Array[String]) extends Element {}

  def elem(contents: Array[String]): Element = new ArrayElement(contents)
}
scala> Element.elem(Array("abc"))
res21: Element = Element$ArrayElement@5c9caa3b

第11章 Scalaの階層構造

継承関係

ScalaのすべてのオブジェクトはAny型となる。 Anyを継承する型としてAnyValAnyRefがある。

Anyには

  • equals
  • ==
  • !=
  • hashCode
  • toString

という基本メソッドが定義されている。

AnyValはScalaの基本型クラスの親クラスである。 Intなどの既存の型クラスはfinalかつabstractなのでnewできない。

AnyValの子クラスUnitは、()という単一のインスタンス値を持っている。これはJavaのvoidに対応している。

Scalaの値クラスは、自身の型クラスに定義していないメソッドを呼び出されたときに互いに暗黙の型変換をしてメソッド呼び出しに対応することがある。 これは別の章で解説する。

AnyRefは、Scalaの参照クラスとされる。 JavaのObjectに対応しており、ScalaのクラスやJavaからimportしてきたクラスはすべてAnyRefを継承している。

Javaのプリミティブ型の取り扱い

Javaでは==は参照等価を検証するキーワードである。 例として、Integerクラスを2つ生成して==で検証すると、これらは別のインスタンスのためfalseが返る。

ScalaのAnyRefクラスでは、参照等価の検証にはeq, neを使う。 ==は値の同一性(1というIntegerは1というIntegerと同一である)の検証に使う。 そのため、JavaのIntegerをScalaのIntegerとして==で検証するとtrueとなる。

scala> def isEqual(x: Integer, y: Integer) = x == y
isEqual: (x: Integer, y: Integer)Boolean

scala> isEqual(new Integer(1), new Integer(1))
res0: Boolean = true

scala> isEqual(1, 1)
res7: Boolean = true
scala> def isEq(x: AnyRef, y: AnyRef) = x eq y
isEq: (x: AnyRef, y: AnyRef)Boolean

scala> isEq(new Integer(1), new Integer(1))
res5: Boolean = false

最下位(bottom)のNull, Nothing

NullAnyRefを継承している。 AnyVal型の変数にnullを代入することはできない。

NothingはすべてのScalaのクラスを継承したクラスである。

何も返さないメソッド定義に使うことができる。例えば、例外を送出するメソッド定義に使うことができる。

下記の例では、divideメソッドはIntを返す定義となっているが、Nothingはすべてのクラスのbottomのクラスなので divideの戻り型がIntである、という条件を満たしている。

def error(message: String): Nothing = throw new RuntimeException(message)

def divide(x: Int , y: Int): Int = {
  if (y != 0) x / y
  else error("can not divide by zero")
}

以下のように直接throwしても、結局Nothingはすべてのクラスのbottomクラスなので戻り値Int型の条件を満たしていることになる。

def divide(x: Int , y: Int): Int = {
  if (y != 0) x / y
  else throw new RuntimeException("boom")
}

同じ型の羅列を割ける

ScalaのAnyValを使って、独自の値クラスを作成しておくことにより、単純ミスを防ぐことができる。

例えば、同じ型でいくつかの引数を取る場合、順番を間違えると気づきにくいが

scala> def getFullName(givenName: String, familyName: String): String = s"$givenName $familyName"
getFullName: (givenName: String, familyName: String)String

scala> getFullName("Smith", "John")
res8: String = Smith John

下記のようにAnyValを定義することによって単純なミスを防ぐことができる。

scala> :paste
// Entering paste mode (ctrl-D to finish)

  class GivenName(val value: String) extends AnyVal
  class FamilyName(val value: String) extends AnyVal

//  def getFullName(givenName: String, familyName: String): String = s"$givenName $familyName"
  def getFullName(givenName: GivenName, familyName: FamilyName): String = s"${givenName.value} ${familyName.value}"

// Exiting paste mode, now interpreting.

defined class GivenName
defined class FamilyName
getFullName: (givenName: GivenName, familyName: FamilyName)String

scala> getFullName(new GivenName("John"), new FamilyName("Smith"))
res5: String = John Smith

第12章 Trait

traitの仕組み

Scalaのclassとtraitの前提は以下である。

  • classは1つのsuper classしか継承できない
  • traitはメソッドとフィールドの定義をカプセル化したもの
    • classにmix-inすることができる

下記のように、traitもclassと同じようにoverrideされる使い方ができる。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Animal(val name: String = "Animal") {

  def getEatingMessage(): String = "I ate."
}

trait hasLegs {

  def getWalkingMessage(): String = "I walked."
}

class Flog(override val name: String = "Flog") extends Animal with hasLegs {

  override def getEatingMessage(): String = s"$name ate something."
  override def getWalkingMessage(): String = s"$name walked."
}

// Exiting paste mode, now interpreting.

defined class Animal
defined trait hasLegs
defined class Flog

scala> val j = new Flog("John")
j: Flog = Flog@96075c0

scala> j.getWalkingMessage()
res8: String = John walked.

scala> j.getEatingMessage()
res9: String = John ate something.

traitはextendsキーワードでもmix-inできるが、super classをextendsしている場合はwithを使う。

class Flog(override val name: String = "Flog") extends Animal with hasLegs with hasHands {}

traitではフィールドを持つことができ、そのフィールドをmix-in先のクラスで変更することができる。 traitで状態を定義することができる。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class Animal(val name: String = "Animal") {

  def getEatingMessage(): String = "I ate."
}

trait hasLegs {

  var hasLegs: Boolean = true

  def getWalkingMessage(): String = "I walked."

  def getHasLegsMessage(): String = if (hasLegs) "I have legs." else "I have no legs."
}

class Flog(override val name: String = "Flog") extends Animal with hasLegs {

  override def getEatingMessage(): String = s"$name ate something."
  override def getWalkingMessage(): String = s"$name walked."
}

// Exiting paste mode, now interpreting.

defined class Animal
defined trait hasLegs
defined class Flog

scala> val alice = new Flog("Alice")
alice: Flog = Flog@1185b0b7

scala> alice.getHasLegsMessage()
res10: String = I have legs.

scala> alice.hasLegs = false
alice.hasLegs: Boolean = false

scala> alice.getHasLegsMessage()
res11: String = I have no legs.

以下のようにして、traitをmix-inした子クラス内でtraitのフィールドへアクセスすることもできる。

override def getHasLegsMessage(): String = if (hasLegs) s"$name have legs." else s"$name have no legs."

オブジェクトの比較にtraitを使う

独自のクラスを定義するときに、>などの比較メソッドを自分で定義するのは手間である。

Orderdp[Type] traitをmix-inしてcompare(that: Type)を実装することにより、>などの比較演算子を楽に実装できる。

ScalaのOrderd traitは以下のようなコードになっている。

trait Ordered[A] extends Any with java.lang.Comparable[A] {

  def compare(that: A): Int

  /** Returns true if `this` is less than `that`
    */
  def <  (that: A): Boolean = (this compare that) <  0

  /** Returns true if `this` is greater than `that`.
    */
  def >  (that: A): Boolean = (this compare that) >  0

  /** Returns true if `this` is less than or equal to `that`.
    */
  def <= (that: A): Boolean = (this compare that) <= 0

  /** Returns true if `this` is greater than or equal to `that`.
    */
  def >= (that: A): Boolean = (this compare that) >= 0

  /** Result of comparing `this` with operand `that`.
    */
  def compareTo(that: A): Int = compare(that)
  }

その他ののtraitの使い方

extendsキーワードを使って、traitをmix-inできるクラスを絞ることができる。 traitがextendsしている場合は、extendsで指定されたclassを継承しているclassしかそのtraitをmix-inできない。

また、new MyNumber with Doublingのようにnewする際にtraitを適用できる。

withを使ったtraitは、右から順に適用される。 以下はtrait内でsuperを使った例である。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class MyNumber(var num: Integer) {
  def calc() = println(s"MyNumber, $num")
}

trait Doubling extends MyNumber {
  override def calc = {
    super.calc()
    num = num * 2
    println(s"Doubling, $num")
  }
}

trait Minus extends MyNumber with Doubling {
  override def calc = {
    super.calc()
    num = num - 1
    println(s"Minus, $num")
  }
}

// Exiting paste mode, now interpreting.

defined class MyNumber
defined trait Doubling
defined trait Minus

scala> val n = new MyNumber(10) with Doubling with Minus
n: MyNumber with Doubling with Minus = $anon$1@2d64c100

scala> n.calc()
MyNumber, 10
Doubling, 20
Minus, 19

superを呼び出すメソッドは、super classやmix-inされたtraitの振る舞いを変更する。

traitをすべきか、せざるべきか

  • 振る舞いが再利用されない場合
    • 具象クラスにする。結局のところ再利用されない。
  • 複数の無関係なクラスで再利用される可能性がある
    • traitを使う
  • Javaのコードで継承させたいとき
    • abstractを使う
    • Javaにはtraitがないため、「変なことになりがち」だという
      • ただし、抽象メンバだけのtraitはJavaではInterfaceとして扱われるので、抽象メンバだけの場合はtraitでよい
  • コンパイル後のコードをライブラリとして配布したい場合
    • traitを使うと、traitがメンバーを獲得したり失ったりするとライブラリを利用する側が再コンパイルする必要が出てきてしまう
    • ※具体的にどうabstractと違いがあるのかわからなかった

とりあえずtraitを使い、あとから問題が出てきた場合に変更すればよいとされている。

第19章 型のパラメーター化

下限境界

下記の例では、型パラメータUは、型パラメータTのsuper-typeでなければいけない。

class Queue[+T] (private val leading: List[T], private val trainling: List[T] {
  def enqueue[U >: T](x: U) = new Queue[U](leading, x :: training)
})

例えば、Fruitというクラスがあり、そのサブクラスとしてAppleとOrangeがあったとする。 trainlingにはAppleがいるが、Queue.enqueue(Orange)すると、Queue.trailingにOrangeを加えて new Queue[Fruit]としてQueueを返すことができる。 AppleもOrangeもFruitのsub-typeなので門ヂアはないえ。

上限境界

以下のコードの場合は、型パラメータBは、StructureBuilder[B]の上限境界である。 これは、BStructureBuilderのsub-typeでなければいけないという定義である。 下のコードではtraitを使っているので、BStructurebuilderをmixinした型でなければいけない。

trait structureBuilder[B <: structureBuilder[B]]

第21章 暗黙の型変換

自分のコードと他人のライブラリ間での問題点

  • 自分のコードは自由に拡張できる
  • 他人のライブラリは、自由に拡張できない。与えられたものをそのまま使わなければいけない
    • しかしながら、他人のライブラリも拡張していきたい場面は多い

Scalaでは、この問題に「暗黙の型変換(Implicit Conversions)」と「暗黙のパラメータ(Implicit Parameters)」を使って対応する。

暗黙の型変換

ある動作を、その動作を実行するクラスとは別で定義したいとする。 Java8以前的な記述をする場合、以下のようにクラスにクラスを渡すような書き方ができる。

scala> :paste
// Entering paste mode (ctrl-D to finish)

abstract class Action {
  def call(): Unit
}

class ActionLauncher(var action: Action = null) {

  def addAction(action: Action): Unit = this.action = action
  def callAction(): Unit = this.action.call()
}

class HelloAction extends Action {
  def call(): Unit = println("hello!")
}

// Exiting paste mode, now interpreting.

defined class Action
defined class ActionLauncher
defined class HelloAction

scala> val launcher = new ActionLauncher
launcher: ActionLauncher = ActionLauncher@6ee8dcd3

scala> launcher.addAction(new HelloAction)

scala> launcher.callAction()
hello!

しかしこれは冗長である。

以下のように定義できると簡潔でわかりやすい。

scala> :paste
// Entering paste mode (ctrl-D to finish)

class ActionLauncher(var action: () => Unit) {

  def addAction(action: () => Unit): Unit = this.action = action
  def callAction(): Unit = this.action()
}

// Exiting paste mode, now interpreting.

defined class ActionLauncher

scala> val l = new ActionLauncher(() => println("hello"))
l: ActionLauncher = ActionLauncher@2149594a

scala> l.callAction()
hello

しかしこのActionLauncherが外部ライブラリであれば、このActionクラスを使ったインターフェースに従うしかない. そこで、変換メソッドを利用してlauncherを簡単に生成できるようにした。

scala> :paste
// Entering paste mode (ctrl-D to finish)

abstract class Action {
  def call(): Unit
}

class ActionLauncher(var action: Action = null) {

  def addAction(action: Action): Unit = this.action = action
  def callAction(): Unit = this.action.call()
}

object Util {
  def fun2act(act: () => Unit): Action  = new Action {
    override def call() = act()
  }
}


// Exiting paste mode, now interpreting.

defined class Action
defined class ActionLauncher
defined object Util

scala> val l = new ActionLauncher(Util.fun2act(() => println("hello")))
l: ActionLauncher = ActionLauncher@64dfb31d

scala> l.callAction()
hello

こういった場合、ScalaではImplicit Conversionsを利用することができる。

さきほどの変換メソッドに、implicit修飾子をつける。 こうすることで、fun2act()は型変換メソッドとして定義される。

下記のコードの場合、Scalaのコンパイラはnew ActionLauncher(() => Unit)しようとする。 しかしActionLauncherの引数はActionである。 この場合、Scalaは() => UnitからActionに変換するimplicitメソッドを探し、あればそれを適用する。

scala> :paste
// Entering paste mode (ctrl-D to finish)

import scala.language.implicitConversions

abstract class Action {
  def call(): Unit
}

class ActionLauncher(var action: Action = null) {

  def addAction(action: Action): Unit = this.action = action
  def callAction(): Unit = this.action.call()
}

implicit def fun2act(act: () => Unit): Action  = new Action {
  override def call() = act()
}

// Exiting paste mode, now interpreting.

import scala.language.implicitConversions
defined class Action
defined class ActionLauncher
fun2act: (act: () => Unit)Action

scala> val l = new ActionLauncher(() => println("good morning!"))
l: ActionLauncher = ActionLauncher@1de6932a

scala> l.callAction()
good morning!

implicit適用ルール

implicit定義が読まれる先

implicitは、スコープ内にあるか、変換するソース型やターゲット型として対応づけられていなければならない。

そこで、implicitを使いたい場合は、implicit定義があるファイルをimportする必要がある。

※ 本にはPreambleという、implicit定義を書いたものを慣習的にライブラリは用意しているのでそれをimportすればよいと書いてあるが、GitHubを軽くみたところそれをやっている人を見つけられなかった…。

また、「型変換の対象になる型」と「要求されている変換後の型」のcompanion object内に定義されているimplicitは型変換に適用される。 Scala自体もこの方法でimplicit定義を書いている模様。

適用ルール

  • 暗黙の型変換は1度しか適用されない
    • スコープのなかに複数使える型変換定義が合った場合は、型変換を行わない。
  • 書かれた定義でコンパイルできるのであれば、暗黙の型変換は使われない
    • つまり、冗長に明示的に型を記述することもできるし、暗黙の型変換を使いまくって型定義を見えないところに書くというスタイルもとれる

暗黙の型変換が適用される場所は以下の3つである。

  • 要求された型への変換
    • 引数に会う型がない場合はimplicitを探して適用する
  • レシーバーの変換
    • 前述したのは引数に会う型がない場合だが、something.do()などと書いてsomethingのクラスにdo()がない場合は、somethingからdo()を持つクラスに変換するimplicitを探して適用する
    • 暗黙のクラス変換(implicit class)がややこしいので後でまとめなおす
  • 暗黙のパラメータ
    • call(a, b, c)の定義に対してcall(a)しかなかった場合、コンパイラはsomecall(a)(b, c)に置き換える。
      • カリー化?というらしいので後で調べる

暗黙のパラメータ

以下のようにb,cをimplicitとして定義する場合には、必要とされる型の変数を定義する必要がある。

def call(a: AParam)(implict b: BParam, c: CParam): Unit = {}

object Params {
  implicit val b = new BParam
  implicit val c = new CParam
}

暗黙のパラメータ型変換に使う型は、型名を独自に用意したほうがよい。 なぜなら、型を汎用型にすればするほどScalaのコードのいろいろな場所で暗黙の型変換が行われることとなり混乱を起こす。

コンテキスト境界を使う

implicitで型定義はしたいが、型の変数自体は使わないときに簡潔に書くことができる。

def call[T](messages: List[T])(implicit something: Something[T]): T = ...)
def call[T: Something](message: List[T]): T = ...)
  • TODO: あとで型パラメータについても調べてから再度読む

第23章 for式の再説

for式は以下の形式で定義することができる。

for ( /* seq(generator | definition | filter) */ ) yield <expression>

// 中括弧を使って複数行で書いてもOK
for {
  x <- 1 until 11, // generator
  name = "John", // definition
  if (x % 2 == 0) // filter
} yield (x, name)

generatorで繰り返し処理のgeneratorを宣言する。<-でgenerateした値は、for〜yieldのスコープ内に定義できる。

yieldで、forで定義したgenerator、definitaion(のうち、filterに当てはまるもの)を変数として値を生成して返すことができる。

scala> for (x <- 1 until 11; name = "John"; if (x % 2 == 0)) yield name + x
res6: scala.collection.immutable.IndexedSeq[String] = Vector(John2, John4, John6, John8, John10)

generatorは複数書くことができる。

generator1; genrator2のように使うと、多重ループを表現できる。

scala> for (x <- 1 until 11; y <- 1 until 2) yield x + y
res17: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 3, 4, 5, 6, 7, 8, 9, 10, 11)

scala> for (x <- 1 until 11; y <- 1 until 3) yield x + y
res18: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12)

for〜yield式の別の書き方

上記の例は、以下のようにwithFilter()map()を使って書き換えることができる。

scala> val name = "John"; 1 until 11 withFilter(x => x % 2 == 0) map(x => name + x)
name: String = John
res9: scala.collection.immutable.IndexedSeq[String] = Vector(John2, John4, John6, John8, John10)
Return to top