2011-12-15

メソッド呼び出しと演算子記法

Scala Advent Calendar JP 2011 の15日目です。
Scala には Java でいうような演算子はありません。例えば
1 + 1
というものも実際には、メソッドを呼び出しています。つまり以下のように書き換えられるということです。
1.+(1)
前者のメソッドが演算子のように見えるメソッド呼び出し方法は演算子記法と言います。基本的には、この演算子記法は通常のメソッド呼び出しに置き換えられますが、完全に一致するかというとそんなことはありません。例えば以下のような場合です。
1 + 2 * 3
これを REPL で実行すると (実行するまでもないかもしれませんが) 以下のようになります。
scala> 1 + 2 * 3
res0: Int = 7
直感とあうため特に疑問に思うことはありません。これを通常のメソッド呼び出しで書き直すと以下のようになります。
scala> 1.+(2).*(3)
res1: Double = 7.0
あれ? なんかおかしいですね。1.+(2) の部分がメソッド呼び出しではなく 1. + (2) => 1.0 + (2) のように演算子記法と解釈されています。ちょっと例が悪かったので仕切り直します。まずは演算子記法です。
scala> "foo" + "bar" * 3
res8: java.lang.String = foobarbarbar
次に通常のメソッド呼び出しです。
scala> "foo".+("bar").*(3)
res9: String = foobarfoobarfoobar
このように、演算子記法を使うとメソッドは単に前から順に呼び出されるわけではなく、呼び出しに優先順位が考慮されることが改めてわかります。優先順位はメソッドの 1文字目によって決まっていて以下の通りです。
  1. 他の全ての特殊文字
  2. * / %
  3. + -
  4. :
  5. = !
  6. < >
  7. &
  8. ^
  9. |
  10. 全ての英字
  11. 全ての代入演算子
1番目の "他の全ての特殊文字" には例えば "~" (チルダ) があります。
class Foo(val name: String) {
  def ~(foo: Foo): Foo = new Foo("~" + name + foo.name + "~")
  def *(foo: Foo): Foo = new Foo("*" + name + foo.name + "*")
  override def toString: String = name
}

val foo = new Foo("foo")
val bar = new Foo("bar")
val baz = new Foo("baz")

println(foo * bar ~ baz)
println(foo.*(bar).~(baz))
REPL で実行すると以下の結果を得ます。
scala> *foo~barbaz~*

scala> ~*foobar*baz~
演算子記法の場合は "~" のほうが "*" よりも優先順位が高くなっていることがわかります。 ということで、演算子記法はメソッド呼び出しではあるが、通常のメソッド呼び出しとは異なり、メソッドの 1文字目を見て優先順位が考慮されるということを押さえておくと変なところではまらずに済むことがあるかもしれません。

2011-12-03

Play 2.0 の interactive console から DB アクセス

Play! Advent Calendar 2011 3日目です。

Play 2.0 のドキュメント にある Using the Play 2.0 console を見ていたら "Launch the interactive console" なるものを発見。Scala の REPL を起動して書いたコードをインタラクティブに試すことができるらしい。例としてあげられている画像ではコンソールから View のテンプレートをよべている。ということは、これを使えば Rails のようにアプリケーションのコードとして書いた DAO を呼び出すことができる? ということで試してみました。
まずは適当な場所にアプリケーションを作ります。今回は Scala application を選びました。お手軽に試したいので DB は H2 でやってみます。conf/application.conf の DB 接続のコメントアウトされている設定をアンコメントします。DB にアクセスするコードは ここ から拝借しました。そのままでは動かなかったので少し手を加えて以下のように。
package models

import java.sql.Date
import play.api.db._
import play.api.Play.current

import org.scalaquery.ql._
import org.scalaquery.ql.TypeMapper._
import org.scalaquery.ql.extended.{ExtendedTable => Table}
import org.scalaquery.ql.extended.H2Driver.Implicit._ 
import org.scalaquery.session._

object Tasks extends Table[(Long, String, Date, Boolean)]("TASKS") {
    
  lazy val database = Database.forDataSource(DB.getDataSource())
  
  def id = column[Long]("ID", O PrimaryKey, O AutoInc)
  def name = column[String]("NAME", O NotNull)
  def dueDate = column[Date]("DUE_DATE")
  def done = column[Boolean]("DONE")
  def * = id ~ name ~ dueDate ~ done
  
  def findAll = database.withSession { implicit db:Session =>
      (for(t <- this) yield t.id ~ t.name).list
  }
  
}
db/evolutions/default というディレクトリを作って evolutions の DDL も用意します。
# --- !Ups

create table tasks (
  id bigint not null primary key,
  name varchar(255) not null,
  due_date timestamp,
  done boolean
);

create sequence task_seq start with 1000;

insert into tasks values (1, 'task 1', '2011-11-11', true);
insert into tasks values (2, 'task 2', '2011-12-03', false);
ScalaQuery を使っているので依存関係を設定します。Web 上のリポジトリにはまだ Scala 2.9.1 用の jar ファイルがなかったため github からソースを取得して publish-local したのちに project/Build.scala の該当箇所を以下のようにしました。
  val appDependencies = Seq(
    // Add your project dependencies here,
    "org.scalaquery" %% "scalaquery" % "0.10.0-SNAPSHOT"
  )

  val main = PlayProject(appName, appVersion, appDependencies).settings(defaultScalaSettings:_*).settings(
    // Add your own project settings here      
    resolvers += Resolver.file("Local Repository", file(Path.userHome.absolutePath + "/.ivy2/local"))(Resolver.ivyStylePatterns)
  )
このあたりで Play のコンソールを立ち上げて確認してみます。update も compile もうまくいきました。次に今回のメインである Scala の REPL を立ち上げます。
[sandbox] $ console
[info] Starting scala interpreter...
[info]
Welcome to Scala version 2.9.1.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0).
Type in expressions to have them evaluated.
Type :help for more information.

scala>
起動したので早速 DB アクセスを試してみます。TAB 補完も効いていい感じです。
scala> models.Tasks.findAll
java.util.NoSuchElementException: None.get
        at scala.None$.get(Option.scala:274)
        at scala.None$.get(Option.scala:272)
        at play.api.Play$.current(Play.scala:36)
        at models.Tasks$.database(Tasks.scala:15)
        at models.Tasks$.findAll(Tasks.scala:23)
--- snip ---
やっぱり最初からはうまくいかないですね。色々調べてみて play.api.Play.current あたりが怪しい。考えてみると play コマンドでコンソールを立ち上げて run や start などで Play アプリが起動します。ということはまだ Play は起動していないので、当然のことながら DB の接続を管理する DataSource もできていないはずです。ということで REPL から Play アプリの起動方法を探しました。
すると play.api.Playstart なるメソッド発見。これでいけそうです。
scala> import java.io.File
import java.io.File

scala> import play.api.Application
import play.api.Application

scala> import play.api.Play
import play.api.Play

scala> import play.core.ApplicationClassLoader
import play.core.ApplicationClassLoader

scala> 

scala> val appBase = new File(".")
appBase: java.io.File = .

scala> val application = Application(appBase, new ApplicationClassLoader(Thread.currentThread.getContextClassLoader, Array()), None, Play.Mode.Dev)
application: play.api.Application = Application(.,play.core.ApplicationClassLoader@332946f7,None,Dev)

scala> Play.start(application)
[info] play - database [default] connected at jdbc:h2:mem:play
play.api.db.evolutions.InvalidDatabaseRevision: Database 'default' needs evolution! [An SQL script need to be run on your database.]
        at play.api.db.evolutions.EvolutionsPlugin$$anonfun$onStart$1.apply(Evolutions.scala:387)
        at play.api.db.evolutions.EvolutionsPlugin$$anonfun$onStart$1.apply(Evolutions.scala:383)
        at scala.collection.immutable.Map$Map1.foreach(Map.scala:118)
        at play.api.db.evolutions.EvolutionsPlugin.onStart(Evolutions.scala:383)
        at play.api.Play$$anonfun$start$1.apply(Play.scala:58)
--- snip ---
ですよね evolutions してないとダメですよね。ということで今度は REPL から evolutions を起動する方法を探します。しばらく探していると……、ありました。play.api.db.evolutions.OfflineEvolutions ってやつが。applyScript メソッドでいけそうです。
scala> import play.api.db.evolutions.OfflineEvolutions
import play.api.db.evolutions.OfflineEvolutions

scala> OfflineEvolutions.applyScript(appBase, Thread.currentThread.getContextClassLoader, "default")
[warn] play - Applying evolution script for database 'default':

# --- Rev:1,Ups - 3022590
create table tasks (
id bigint not null primary key,
name varchar(255) not null,
due_date timestamp,
done boolean
);

create sequence task_seq start with 1000;

insert into tasks values (1, 'task 1', '2011-11-11', true);
insert into tasks values (2, 'task 2', '2011-12-03', false);
applyScript メソッドの第 2 引数で渡している ”default" というのは application.conf で設定した db.default.driver=org.h2.Driver などの "db." の次の文字列を渡すみたいです。で、再チャレンジ。
scala> Play.start(application)
play.api.Configuration$$anon$2: Configuration error [Cannot connect to database at [jdbc:h2:mem:play]]
        at play.api.Configuration.play$api$Configuration$$error(Configuration.scala:310)
        at play.api.Configuration$$anonfun$reportError$1.apply(Configuration.scala:270)
--- snip ---
今度は DB につながらないと。調べてみたのですが原因が分からなかったので今回はインメモリじゃなくファイルでやることにしました。application.conf を以下のように書き換えました。
-db.default.url=jdbc:h2:mem:play
+db.default.url=jdbc:h2:h2db/play
再再チャレンジ。最初からやり直します。
scala> import java.io.File
import java.io.File

scala> import play.api.Application
import play.api.Application

scala> import play.api.Play
import play.api.Play

scala> import play.api.db.evolutions.OfflineEvolutions
import play.api.db.evolutions.OfflineEvolutions

scala> import play.core.ApplicationClassLoader
import play.core.ApplicationClassLoader

scala>

scala> val appBase = new File(".")
appBase: java.io.File = .

scala> OfflineEvolutions.applyScript(appBase, Thread.currentThread.getContextClassLoader, "default")
23:25:21.311 [run-main] DEBUG com.jolbox.bonecp.BoneCPDataSource - JDBC URL = jdbc:h2:h2db/play, Username = sa, partitions = 1, max (per partition) = 0, min (per partition) = 0, helper threads = 3, idle max age = 60 min, idle test period = 240 min
23:25:21.320 [run-main] WARN  com.jolbox.bonecp.BoneCPConfig - Max Connections < 1. Setting to 20
23:25:22.209 [run-main] WARN  play - Applying evolution script for database 'default':

# --- Rev:1,Ups - 3022590
create table tasks (
id bigint not null primary key,
name varchar(255) not null,
due_date timestamp,
done boolean
);

create sequence task_seq start with 1000;

insert into tasks values (1, 'task 1', '2011-11-11', true);
insert into tasks values (2, 'task 2', '2011-12-03', false);


scala> val application = Application(appBase, new ApplicationClassLoader(Thread.currentThread.getContextClassLoader, Array()), None, Play.Mode.Dev)
application: play.api.Application = Application(.,play.core.ApplicationClassLoader@18a8507c,None,Dev)

scala> Play.start(application)
[info] play - database [default] connected at jdbc:h2:h2db/play
[info] play - Application started
今度はうまくいきました。これでデータが取得できるか。
scala> models.Tasks.findAll
res2: List[(Long, String)] = List((1,task 1), (2,task 2))
とれました!! Play 1.x でやりたかったことが 2.0 でできるようになっていてますます 2.0 のリリースが楽しみになりました。
おそらく多くの人が望んでいる機能なのでベータがとれるころにはもっと簡単にできるようになっているかもしれません。
明日 (12/4) は @Masahito さんです。