case classはコンパニオンオブジェクトのなかで定義すべき理由
Scalaでデータなどを扱うときにはcase class
を使うのが一般的である。
このcase class
はコンパニオンオブジェクトに書くのが当たり前のようになっているがそれはなぜか。
ScalaでもJavaと同様、インナークラスを定義できる。
しかし、その違いとして「パス依存性」という特徴がある。
インナークラスの型はどのアウタークラスから生成されたかで決まる。
REPLでアウタークラスとインナークラスを定義し、インスタンスを2つ作る。
var
変数にインスタンスを再代入しようとすると、type mismatch
エラーとなることがわかる。
scala> class Outer { | class Inner { | private val hoge = "hoge" | } | } defined class Outer scala> val o1 = new Outer o1: Outer = Outer@18769467 scala> val o2 = new Outer o2: Outer = Outer@368102c8 scala> val in1 = new o1.Inner in1: o1.Inner = Outer$Inner@77556fd scala> val in2 = new o2.Inner in2: o2.Inner = Outer$Inner@1996cd68 scala> var in = in1 in: o1.Inner = Outer$Inner@77556fd scala> in = in2 <console>:13: error: type mismatch; found : o2.Inner required: o1.Inner in = in2 ^ scala> in1.isInstanceOf[Outer#Inner] res0: Boolean = true
インナークラスの型は、どのインスタンスから生成されたかに依存している。(パス依存性)
その型を汎用的に表記するためにはOuter#Inner
のように記述する必要がある。
さて、このようなパス依存性の特徴を確認したうえで、MVCで設計したControllerからServiceトレイトを利用する例を考えてみよう。
UserController.scala
import UserService._ class UserController extends Controller with UserService { def list: Seq[User] = getUsers }
UserService.scala
object UserService { // データベースから取得するレコードの代わりにモックとしてこのデータを利用する val users: Seq[(Int, String)] = Seq((1, "hoge"), (2, "fuga"), (3, "foo"), (4, "bar")) } trait UserService { import UserService._ case class User(id: Int, name: String) def getUsers: Seq[User] = users.map(u => User(id = u._1, name = u._2)) }
よくある実装のしかただが、ここではあえてUserService
トレイトの中にcase class User()
を定義している。
sbt console
からUserController
を複数インスタンス化し、list
の値を比較してみよう。
scala> val userController1 = new UserController userController1: UserController = UserController@297eb9d9 scala> val userController2 = new UserController userController2: UserController = UserController@4faf14f1 scala> userController1.list == userController2.list res0: Boolean = false
同一のデータ(users
)を利用しているのだから、どのインスタンスから実行しても結果は同値になるはずなのだが、
userController1.list == userController2.list
として比較した結果は、false
として返っている。
これが、先に説明したScalaのインナークラスにおける「パス依存性」に起因する問題である。
このように、今回のUser
のようにインナークラスはインスタンスに依存するため、このままではテストなどが正常に行えない。
そこで、このcase class User
をUserService
のコンパニオンオブジェクトに定義するように修正する。
object
はシングルトンであるから、そこに定義しているインナークラスがインスタンスに依存する問題を解消できる。
UserService.scala
object UserService { val users: Seq[(Int, String)] = Seq((1, "hoge"), (2, "fuga"), (3, "foo"), (4, "bar")) // case class をコンパニオンオブジェクトで定義する case class User(id: Int, name: String) } trait UserService { import UserService._ def getUsers: Seq[User] = users.map(u => User(id = u._1, name = u._2)) }
これを、先ほどと同様にsbt console
からインスタンス化しその値を比較しよう。
scala> val userController1 = new UserController userController1: UserController = UserController@3094148b scala> val userController2 = new UserController userController2: UserController = UserController@32fef5c6 scala> userController1.list == userController2.list res0: Boolean = true
今度は各インスタンスから呼び出したlist
メソッドの結果は同値となった。
このように、ScalaではTraitやclassの中にcase class
を定義すると、それらは実体化する際アウタークラスのインスタンスに依存するため、
かならずコンパニオンオブジェクトなど、object
のなかで定義することを心がけなければならない。
trait / case class / object どう使ったら良いのかって話
聞きかじった話を、今後考える課題としてメモ。
結論Scalaではclass
は使わないってお話。
オブジェクト指向でシステムを設計するときに必要なものは、ざっくり分けると
- インタフェース(メソッド)
- データ(メンバ)
このふたつ。
あとは、これを実体化して利用するだけ。
Scalaでは、インタフェースとしてのTraitを複数(ひし形に)継承できる。 これはつまり、その組み合わせの数だけ統一されたインタフェースで、 具体的な実装に起こすことができる。
例えば、Webサーバーを作ろうとしたときに、
- 同期処理用のトレイト
- 非同期処理用のトレイト
のふたつを用意しておいて、それぞれに対して
のように複数のプロトコルに対応したトレイトをつくる。
そうすると、その種類を乗じた数(ここでは2 * 3)のパターンだけインタフェースを作れる。
必要なデータはcase class
に持たせる。
ヘッダー情報などは、case class
にマッピングするということ。
そしてこれを実体化するときは、シングルトン的にobject
を呼び出す。
ついついDTOに余計な実装が入ってしまうなどのことは起きがちであるが。
そもそも、実装とデータは分離されるべきで、そのためにtrait
とcase class
を使い分ける。
ちなみに、トレイトを複数組合せるとき、例えば先の例では同期的処理を行う場合も非同期的な処理を行う場合も結果を同じ形でやり取りしたい。
そこで、FutureやOptionをモナドとして扱う必要がある。
そのためのモナドだったのか。その点はもう少し考えたい。
【FPIS】Scala関数型デザイン&プログラミング 読書メモ
出版から半年ほど時間が経ってしまいましたが、「Scala関数型デザイン&プログラミング」を読み進めていく際の自分用のメモを公開しようと思います。
関数型プログラミングとは
副作用のない関数=純粋関数
- 純粋関数だけで構築=関数型プログラミング
- プログラムの書き方を制約する
- 純粋関数だけで構築=関数型プログラミング
副作用のある関数=結果を返す以外の何かをする関数
- 変数を変更
- データ構造を直接変更
- オブジェクトのフィールドを設定
- 例外をスロー、またはエラーで停止
- コンソールに出力、入力を読み取る
- ファイルを読み取る、ファイルに書き込む
- 画面上に描画する
関数型プログラミングの利点
- モジュール性の向上
- バグが発生しづらい
- テスト
- 再利用
- 並列化
- 一般化
- 推論
- バグが発生しづらい
副作用の問題点
- テストしにくい
副作用を持つプログラム
副作用を持つScalaプログラム
class Cafe { def buyCofee(cc: CreditCard): Coffee = { val cup = new Cofee() cc.charge(cup.price) cup } }
- クレジットカードへのチャージ
- クレジットカード会社への問い合わせ
- カード決済の承認・課金
- 決済記録を内部システムに保存
- 問題点
cc.charge(cup.price)
が副作用- クレジットカードへのチャージ
- 外部とのやり取りが必要
- クレジットカードへのチャージ
- 解決策
CreditCard
にクレジットカードへのチャージを組み込まないPayments
オブジェクトを利用
paymentsオブジェクトの追加
class Cafe { def buyCoffee(cc: CreditCard, p: Payments): Coffee = { val cup = new Coffee() p.charge(cc, cup.price) cup } }
p.charge(cc, cup.price)
部分は副作用を持つ- テスタビリティは改善
Payments
はインタフェースでも構わない
- 問題点
- 解決策
class Cafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = { val cup = new Coffee() (cup, Charge(cc, cup.price)) } }
疑問 cupと、Chargeを返しているけれど、 クレジットカードへのチャージが失敗した場合でも、Coffeeを返してしまう? トランザクションをどのようにはるかが課題
case class Charge(cc: CreditCard, amount: Double) { def combine(other: Charge): Charge = if (cc == other.cc) Charge(cc, amount + other.amount) else throw new Exception("Can't combine charges to different cards") }
- n杯のコーヒーの購入を実装
buyCoffees
はbuyCoffee
をベースとした実装
class Cafe { def buyCoffee(cc: CreditCard): (Coffee, Charge) = ... def buyCoffees(cc: CreditCard, n: Int): (List[Coffee], Charge) = { val purchaces: List[(Coffee, Charge)] = List.fill(n) (buyCoffee(cc)) val (coffees, charges) = purchases.unzip (coffees, charges.reduce((c1, c2) => c1.combine(c2))) } }
Charge
がどのように処理されるか、Cafe
は全く関知しないCharge
をファーストクラスの値として扱う- 複数の注文をコーヒーショップが1つのチャージにまとめる
- クレジットカードの手数料を節約
- 複数の注文をコーヒーショップが1つのチャージにまとめる
def coalesce(charges: List[Charge]): List[Charge] = charges.groupBy(_.cc).values.map(_.reduce(_ combine _)).toList
疑問 最後にtoListするのはなんで?
関数とはいったい何か
- 純粋関数は推論しやすい
- 入力型A、出力型Bの関数fは、A型のすべての値aをB型の1つの値bに関連付ける
- bがaの値によってのみ決定されるようにする計算
A => B
は単一の型- 内部プロセスまたは外部プロセスの状態は計算結果f(a)とは無関係
- 入力型A、出力型Bの関数fは、A型のすべての値aをB型の1つの値bに関連付ける
- 参照透過性(式が参照透過である)
- プログラムの意味を変えることなく、式をその結果に置き換えることができる
式eがあり、すべてのプログラムpにおいて、pの意味に影響をあたえることなく、p内すべてのeをeの評価結果と置き換えることができるとしたら、eは参照透過です。関数fがあり、式f(x)が参照透過なすべてのxに対して参照透過であるとしたら、fは純粋関数です。
- プログラムの意味を変えることなく、式をその結果に置き換えることができる
参照透過性、純粋性、置換モデル
def buyCoffee(cc: CreditCard): Coffee = { val cup = new Coffee() cc.charge(cup.price) cup }
buyCoffee
が純粋であるためにはp(buyCoffee(aliceCreditCard))
が任意のpに対して、p(new Coffee())
と同じ振る舞いである- しかし、クレジットカード会社に問い合わせが発生している
- 置換モデル
- 関数が実行するすべてのことがその戻り値によって表される
- 等価による置換
- 参照透過性によりプログラムの等式推論が可能になる
- 合成可能関数
並んだ、並んだ、mapの処理が
OptionやCollectionに対して、連続して処理をする場合など。 mapが並ぶこと、ありますよね。
seq.map(f).map(g)
こんな感じにmapが連続してチェーンするなら、関数合成してしまいます。
compose
する間でもないので、andThen
を使います。
seq.map(f.andThen(g))
かなりシンプルなのと( )
を省略すれば可動性もよいかと。
headOptionを見つけたら
リストから、条件にマッチする先頭の要素をOptionで取り出すときに
list.filter(somefunc(_)).headOption
としたり、
条件にあった値の存在を確認するときにlist.filter(somefunc(_)).headOption.nonEmpty
なんて書いてあるコードを見つけたら、ほかのCollection APIが使えないかを疑うべき。
先頭の値を探して返す場合は、
list.find(somefunc(_))
とすればよい。また、値の有無を判別するならば、
list.exists(somefunc(_))
のようにexists
を使えば済むはなしである。
Scalaで全角数字を半角数字に変換。その再帰、たたみ込める?
全角数字を半角数字に置き換えるというよくある処理。
再帰関数を自分で定義して書くなら、次のようになります。
def fullWidthNumberToHalfWidthNumber(str: String): String = { val fullWidthNumbers = List("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") @scala.annotation.tailrec def convert(str: String, acc: Int): String = if (acc > 9) str else convert(str.replaceAll(fullWidthNumbers(acc), acc.toString), acc + 1) convert(str, 0) }
再帰関数を定義している分、冗長になるためもう少し、シンプルに記述したいところです。
そこで、foldLeft
を使って、これを書き直すと下記のように記述できます。
def fullWidthNumberToHalfWidthNumber(str: String): String = { val fullWidthNumbers = List("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") fullWidthNumbers.zipWithIndex.foldLeft(str){ case (x, (c, i)) => x.replaceAll(c, i.toString) } }
一度、fullWidthNumbers.zipIndex
とすることで、List(("1", 1), ("2", 2), ...)
のようなインデックス付きのリストを作っています。
また、foldLeft
に渡す関数の引数についても{ case (x, (c, i)) => }
このような省略をできるのもScalaの便利なところ。
省略をしなければ次のような意味を持ちます。これを、case
から書き始められるのは非常に便利です。
(str, tuple) => (str, tuple) match { case (x, (c, i)) => // ... }
再帰関数を定義しようとするときは、いったんfoldLeft
などたたみ込み関数で代用出来ないか考えてみると、よいかもしれません。
GuavaのChacheをScalaから使って、定期的にインスタンス変数のキャッシュをリセットする
GuavaのChacheをScalaから使って、定期的にインスタンス変数のキャッシュをリセットする
DBのコネクション数などを考えれば、Remote Actorのインスタンス変数に、 値をキャッシュするようなケースは容易に想像できる。
このインスタンス変数でキャッシュをするような場合に、これを定期的にリフレッシュする方法として、 Google製のJavaライブラリGuavaから、Cacheを使うやり方を紹介する。
下記の例では、cache.get()
で、所定のキーを呼び出しそのキーが存在する場合は、キーに対応する値を返す。
cache
にキーが存在しない場合、Callable
をインスタンス化することで、call
メソッドでキーに対応する値を返すようにしている。
このキャッシュを1秒間でリセットするようにしており、Thread.sleep(1000)
をした後、cache
に対してキーを呼び出すと、call
が再度呼び出されるはずである。
object cacheTest { def main(args: Array[String]) = { val cache: Cache[String,String] = CacheBuilder .newBuilder().maximumSize(10).expireAfterWrite(1, java.util.concurrent.TimeUnit.SECONDS).build() val a1 = cache.get("aa", new Callable[String] { def call(): String = { println("called") "hoge" } }) println(a1) val a2 = cache.get("aa", new Callable[String] { def call(): String = { println("called") "hoge" } }) println(a2) Thread.sleep(1000) val a3 = cache.get("aa", new Callable[String] { def call(): String = { println("called") "hoge" } }) println(a3) } }
これを実行すると、出力は次のようになる。
called hoge hoge called hoge