はじめに
ふと思い立ち数年越しの続編...前編(2020年...)はこちら
Scala Schoolは古く、scala3に対応していないので今更続きをやる意味があるのか微妙なところですが、せめてBasics Continuedまでだけでもやっておくかと思い。
完全に自分用・自己満足。
本編
apply methods
applyという名前でメソッドを定義しておくと、クラスやオブジェクトが一つの目的(メソッド)で利用されるときに便利なシンタックスシュガーが使える。
object Foo {
def apply() = 100
}
class Bar {
def apply() = 200
}
Foo() //結果は100. Foo.apply()とわざわざやらなくてもOK
val b = new Bar
b() //結果は200. 同様にbar.apply()とやらなくでもOK
Objects
オブジェクトは一つのインスタンスを保持するために使われる。
また、クラスと同名のオブジェクトはCompanion Objectというらしい。
Functions are objects
関数の正体はトレイト(の実装)で、applyメソッドのシンタックスシュガーによりそのオブジェクトを関数のように呼ぶことができる、ということらしい。
object addOne extends Function1[Int, Int] {
def apply(m: Int): Int = m + 1
}
addOne(1) //結果は2
このFunctionトレイトはFunction0から22まであるらしい。22というのは完全にマジックナンバーで、単純に引数の数は22もあれば十分でしょってことみたい。
objectだけでなくclassもFunction*トレイトをextendsできます。
また、extends Function1[Int, Int]
は extends (Int => Int)
と省略して書けるらしい。
Packages
ファイルの先頭でパッケージを宣言するとそのファイルの中身はすべてそのパッケージに所属する。
値や関数はobjectやclassの外には書けない。
Pattern Matching
val times = 1
times match {
case 1 => "one"
case 2 => "two"
case _ => "some other number"
}
times match {
case i if i == 1 => "one"
case i if i == 2 => "two"
case _ => "some other number"
}
caseに直接値を書いてマッチングする方法と、値を変数iにとらえてifで条件を書く方法(matching with guards)があるみたい。
_
はワイルドカードで上のケースでマッチしなかったすべての状態を扱う。
matchは異なる型の値も扱うことができる。
def bigger(o: Any): Any = {
o match {
case i: Int if i < 0 => i - 1
case i: Int => i + 1
case d: Double if d < 0.0 => d - 0.1
case d: Double => d + 0.1
case text: String => text + "s"
}
}
こんな感じ。
クラスのメンバーでマッチングする場合
def calcType(calc: Calculator) = calc match {
case _ if calc.brand == "HP" && calc.model == "20B" => "financial"
case _ if calc.brand == "HP" && calc.model == "48G" => "scientific"
case _ if calc.brand == "HP" && calc.model == "30B" => "business"
case _ => "unknown"
}
これは"painful"な方法とのこと。もう少しスッキリ書くには後述のcase classを使うとよいらしい。
Case Classes
ケースクラスはパターンマッチングで使うためにデザインされた値を保持するためのクラスらしい。
また、newを使わずに生成できる(Scala SchoolのBasicsでは特に言及されてないですが、ケースクラスには自動的にコンパニオンオブジェクトが作られ、applyメソッドがファクトリーになっているため)。
各メンバーの値が同じなら ==
でtrueになるようにできている。普通のクラスと同様にメソッドを持つことも可能。
先ほどの"painful"なコードが
case class Calculator(brand: String, model: String) //ケースクラスの定義
def calcType(calc: Calculator) = calc match {
case Calculator("HP", "20B") => "financial"
case Calculator("HP", "48G") => "scientific"
case Calculator("HP", "30B") => "business"
case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel)
}
このようになる。
最後のマッチは
case Calculator(_, _) => "Calculator of unknown type" //パターン1
case _ => "Calculator of unknown type" //パターン2
case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c) //パターン3
このように様々な書き方ができるよと紹介されていた。パターン3の変数cに再束縛するやりかたは
case c: Calculator => "Calculator: %s is of unknown type".format(c)
としたほうがわかりやすくないか?と思った。全く同じように動くし。
型を書かずに
case c => "Calculator: %s is of unknown type".format(c)
でもいいし。
Exceptions
javaのようにtry-catch-finallyで例外を使うことができるようです。
例外をパターンマッチングさせることができるという点とtryもifなどと同様「式」であるという点が異なります。
val result: Int = try {
remoteCalculatorService.add(1, 2)
} catch {
case e: ServerIsDownException => {
log.error(e, "the remote calculator service is unavailable. should have kept your trusty HP.")
0
}
} finally {
remoteCalculatorService.close()
}
例外が発生しなければ remoteCalculatorService.add(1, 2)
の結果が、例外が発生しcatchに入ったら0が result
に代入されます。
おわりに
またScalaちょっと触ろうかなという気持ちがわいてきたのでよい復習になりました。
今はJetBrains AcademyのFunctional Programming in Scalaをやっています。こちらはつい最近出たコースなのでScala3が前提になってます。