Tag Archives: Scala

Scala 2.10 からの型クラス2(Functorはどうする?)

前回の記事にコメントをいただいて、Functor とか Monad なんかの高階型はどうするのか改めて考えてみました。

結論から言うと Scala は関数リテラルで型パラメータとれないので、関数リテラルだけでなんとかしようというのは不可能っぽいです(いまんところの私の頭では)。

しかし、implicity 使ったり context-bound を使わなくてもなんとかなりそうです。ここまで来ると型クラスじゃねーだろっ。と言われるかもしれませんがアドホック多相にはなってると思います。

ということで、Functor をやってみます。Haskell だとこんな感じですね。

class Functor f where
    fmap :: (a -> b) -> f a -> f b

Scala だと

import language.higherKinds
 
trait Functor[A] {
  type F[_]
  def fmap[B](func: A => B): F[B]
}
trait FunctorFunctions {
  def fmap[A, B](func: A => B)(f: Functor[A]): m.F[B] = f.fmap(func)
}

Option 型に適用してみます。

trait FunctorInstance {
  implicit class OptionFunctor[A](option: Option[A]) extends Functor[A] {
    type F[_] = Option[_]
    def fmap[B](func: A => B): Option[B] = option.map(func)
  }
}

使い方。

val allInstances = new FunctorInstance with FunctorFunctions {}
import allInstances._
 
val maybeInt = Option(3)
val func: Int => Int = _ * 2
 
println(maybeInt.fmap(func))
println(fmap(func)(maybeInt))
 
// こんな風にも使えます
def doubleInt(f: Functor[Int]): f.F[Int] = f.fmap(func)
 
println(doubleInt(maybeInt))

def は撲滅できませんでしたが、context-bound が撲滅できるのが私個人としては嬉しいです。それにやっぱりコード量はこっちのほうが少ないし!

でも、この fmap、カリー化を用いた部分適用(という言い方でいい?)できない! ぎゃふん!

Scala 2.10 からの型クラス

Scala で型クラス使おうという人ならば一度は目を通したことがあるだろう「空飛ぶサンドイッチのパーツ」。

Scala での型クラスの実装方法といったらたいていこの記事に書いてあるような方式になるのではないでしょうか(というか、私がググった限りではこのパターンしか見つけられなかった)。

でもこの方式、context-bound 型パラメータ使うんですよね。これのせいで def が登場しちゃうし、カリー化ができない。空飛ぶサンドイッチのパーツ自身、関数リテラル使おうぜ! と言いながらこのせいで def 使っちゃう。悔しい。それから、もっと便利にしようということで Scalaz みたいに Ops とか入れちゃって、型クラス実現するのにめんどうなこと!

ということで、Scala 2.10 から入った implicit classes 使うと型クラス簡単ですぜっていうのが今回の話です。空飛ぶサンドイッチのパーツの Depth をやってみます。

まず型クラス。ジェネリクスはいりません。

trait Depth {
  val: depth: Int
}

// お好みで関数バージョンも作りましょう
trait DepthFunctions {
  val depth: Depth => Int = _.depth
}

そして型インスタンス。

trait DepthInstances {
  implicit class TreeDepth(tree: Tree) extends Depth {
    val depth: Int = tree match {
      case Empty()    => 0
      case Leaf(_)    => 1
      case Node(l, r) => 1 + math.max(l.depth, r.depth)
    }
  }
  implicit class ListDepth(list: List[Int]) extends Depth {
    val depth: Int = list match {
      case xs => xs.size
    }
  }
}

使い方。空飛ぶサンドイッチのパーツでは、def になっていた halfDepth もこれで晴れて関数リテラルに!

val allInstances = new DepthInstances 
  with DepthFunctions {}
import allInstances._
val halfDepth: Depth => Int = _.depth / 2
// ↓ 関数バージョンを使えばこんな書き方もできます。
// val halfDepth: Depth => Int = d => depth(d) / 2
halfDepth(List(1, 2, 3, 4))

簡単! やった!

Play 2.0 の Cache API が使うに耐えない

Play 2.0(厳密にはこの記事の執筆時の最新 2.0.3)の Cache API はほんとうに使うに耐えません(Scala での話。Java ではどうだか試してません)。Ehcache を直で扱ったほうがよっぽどよさそうです。

1. キャッシュを削除できない。

キャッシュってそんなに任意で消すようなものでもないと思いますが、それにしても任意に消せないのはどうも不便です。ちなみに以下のようなコードがキャッシュを消すコードとして一部で出回っていますがデタラメです。null 値が永遠にキャッシュされるだけです。

Cache.set("YourCacheKey", null, 0)

2. キャッシュの寿命のデフォルト値を制御できない。

現在の実装では、キャッシュの寿命を指定しないと 0 を指定したことになります。0 を指定すると永遠にキャッシュするという意味になります。設定ファイルなどで調整できません。

3. null 値をキャッシュできない(取り出せない)。

null をキャッシュする意味があるのかというと、たまにあります。非常に長い処理の結果が null だったりしたら null も重要な情報のひとつです。Scala なら None 使えよって話もありますが。

それで、null 値をキャッシュに入れることはできるのです。ところが取り出そうとすると NullPointerException で落ちます。なので、前述のデタラメキャッシュ削除コードを使っていたりすると潜在バグの温床になります。NullPointerException が出ること自体 Play のバグのような気もしますが……。

Scala-Tools のサービスが終了した件

sbt で標準で参照しているリポジトリ、scala-tools のサービスが先日終了したようです。しばらく移行期間として移行先のリポジトリにリダイレクトをしていたのですが、それも終了したとのこと(たまたま今落ちてるだけかもしれないとのこと。でも将来なくなるのは確か)。

そこで scala-tools を参照しているビルドツールは設定変更を余儀なくされています。とりあえず、Play 2.0 は、project/Build.scala を以下のように修正すれば良いようです。

import sbt._
import Keys._
import PlayProject._

object ApplicationBuild extends Build {

  val appName = "myapp"
  val appVersion = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
  )

  val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings(
    // Add your own project settings here
    // 以下のように移転先リポジトリを指定
    resolvers ++= Seq(
      "sonatype-releases" at "https://oss.sonatype.org/content/repositories/releases/",
    ),
    // 以下のようにこれまでの Scala-Tools リポジトリを見ないようにする
    externalResolvers ~= (_.filter(_.name != "Scala-Tools Maven2 Repository"))
  )

}

MongoURI がいい加減すぎワロエナイ

Heroku + MongoLab 環境に Casbah を使って MongoDB に接続する Play 2.0 で作成したアプリケーションをデプロイしようとしました。

それで Heroku 上で MONGOLAB_URI の設定値として提供される MongoDB の接続 URI を com.mongodb.casbah.MongoURI(com.mongodb.MongoURI)に食わせたらまったくもってでたらめなパースをするので自分でなんとかするコードを書きました。使い方は以下のコード内のコメント参照。MongoDB の接続 URI は conf/application.conf 内で mongodb.default.uri で設定するか、起動時に -D オプションで与えるようにしてください。

package mongodb

import play.api._
import Play.current
import com.mongodb.casbah.Imports._

/**
 * MongoDB をサクッと使うためのオブジェクトです。
 */
object Mongo {
  /**
   * MongoDB 接続用 URI をパースするための正規表現
   */
  val uriPattern = "mongodb://(?:([^:^@]+)(?::([^@]+))?@)?([^:^/]+)(?::(\\d+))?/(.+)".r

  /**
   * MongoDB に接続されたブロックを提供します。
   * 接続 URI は mongodb.default.uri の設定値を使用します。
   *
   * 使い方の例
   * {{{
   *   MongoDB.withDB { db =>
   *     db("employee").findAll().toSeq
   *   }
   * }}}
   */
  def withDB[A](f: MongoDB => A): A = withDB("default")(f)

  /**
   * MongoDB に接続されたブロックを提供します。
   *
   * 使い方の例
   * {{{
   *   MongoDB.withDB("mydb") { db =>
   *     db("employee").findAll().toSeq
   *   }
   * }}}
   *
   * @param name 設定名
   */
  def withDB[A](name: String)(f: MongoDB => A): A = {
    // Play を使わない場合は↓このあたりを変えるといい
    val uri = Play.configuration.getString("mongodb." + name + ".uri").getOrElse("mongodb://localhost/test")

    // URI のパース
    val (usernameOpt, passwordOpt, host, portOpt, database) = parseURI(uri)

    // MongoDB に接続
    val conn = portOpt map { port =>
      MongoConnection(host, port.toInt)
    } getOrElse {
      MongoConnection(host)
    }

    try {
      // DB 取得
      val db = conn(database)
      // 必要とあれば認証処理を行う
      (usernameOpt, passwordOpt) match {
        case (Some(username), Some(password)) => db.authenticate(username, password)
        case _ =>
      }
      // 引数で指定された処理を実行
      f(db)
    } finally {
      conn.close()
    }
  }

  /**
   * MongoDB 接続 URI をパースします。
   */
  def parseURI(uri: String) = {
    uri match {
      case uriPattern(username, password, host, port, database) =>
        (Option(username), Option(password), host, Option(port), database)
    }
  }
}