戦略 Scala 日記

素人プログラマの思考のセンス

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 UserUserServiceのコンパニオンオブジェクトに定義するように修正する。
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に余計な実装が入ってしまうなどのことは起きがちであるが。
そもそも、実装とデータは分離されるべきで、そのためにtraitcase 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はインタフェースでも構わない
  • 問題点
    • Paymentsをインタフェースにせざるを得ない
      • モック実装は必ずしも使いやすいとは言えない
        • buyCoffeeの呼出し後に調べなければならない内部状態が実装に含まれる
          • chargeの呼び出しによって適切に変更されていることをテストで確認する必要がある
          • モックフレームなどを利用する方法はある
            • buyCofeeがコーヒー1杯分をきっちり請求することをテストするだけならそこまでする必要はない
    • buyCoffeeメソッドの再利用が難しくなる
      • ある1人がコーヒーを複数注文した場合
        • クレジットカードに複数回チャージすることになる
    • つまり副作用がある
      • buyCoffeeテストにクレジットカードサーバーが必要
      • 2つのトランザクションを1つにまとめることが出来ない
  • 解決策
    • コードを再利用できるように
    • 合成ができるようにする
    • チャージの作成をチャージの処理または解釈から切り離す
    • buyCoffeeが副作用を発生させずにChargeオブジェクトを返す
    • テストをしやすい
      • 支払いを処理する仕掛けを切り離している
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杯のコーヒーの購入を実装
    • buyCoffeesbuyCoffeeをベースとした実装
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つのチャージにまとめる
      • クレジットカードの手数料を節約
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)とは無関係
  • 参照透過性(式が参照透過である)
    • プログラムの意味を変えることなく、式をその結果に置き換えることができる

      式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())と同じ振る舞いである
  • 置換モデル
    • 関数が実行するすべてのことがその戻り値によって表される
    • 等価による置換
    • 参照透過性によりプログラムの等式推論が可能になる
  • 合成可能関数

Kindle版も出版されています。
電子書籍で買えばよかった…。

並んだ、並んだ、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