2012-01-24

HBase shell で java.lang.Float.NaN を値として持つ行をフィルタリングする

不具合の調査で java.lang.Float.NaN が入っている行をフィルタリングしたいことがあったので調べました。文字列でフィルタリングするときは以下のような感じです。
import org.apache.hadoop.hbase.filter.ValueFilter
import org.apache.hadoop.hbase.filter.BinaryComparator
filter = ValueFilter.new(ValueFilter::CompareOp::EQUAL, BinaryComparator.new("some_value".to_java_bytes))
scan 'table_name', {"FILTER" => filter}
java.lang.Float の場合はそのままでは呼び出せないので以下のようにする必要があります。
import org.apache.hadoop.hbase.filter.ValueFilter
import org.apache.hadoop.hbase.filter.BinaryComparator
filter = ValueFilter.new(ValueFilter::CompareOp::EQUAL, BinaryComparator.new("\x7F\xC0\x00\x00".to_s.to_java_bytes))
scan 'table_name', {"FILTER" => filter}
本来はこれでいけるはずなのですが、なぜか問題のテーブルには \x7F\xC0\x00\x00 ではなく \xFF\xC0\x00\x00 が入っているっぽい。JDK の API ドキュメントでは 0x7fc00000NaN と書いてあるのですが。ということで Scala の REPL で軽く確認してみました。
scala> java.lang.Float.intBitsToFloat(0x7fc00000)
res0: Float = NaN

scala> java.lang.Float.intBitsToFloat(0xffc00000)
res1: Float = NaN
うーん java.lang.Float.NaN はバイナリ的に 2種類ある……のか?

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 さんです。

2011-11-15

List でも "Map#get の返り値でパターンマッチ" 的なことを

List でも MapgetOrElse のようなことや get の返り値でパターンマッチしたいことありませんか?

例えば以下のコードを実行すると java.lang.IndexOutOfBoundsException がスローされます。

val list = List("foo", "bar")
println(list(0))
println(list(2))

list(2) の実行時に例外がスローされるのではなく None が返ってほしいこともあります。

方法は 2通りあります。

1つめは Map にしてしまう方法です。

val list = List("foo", "bar")
val map = list.zipWithIndex.map{case (elem, i) => (i, elem)}.toMap
println(map.get(0))
println(map.get(2))

ちょっとトリッキーな感じはしますが、こうすると getOrElse も使えます。

もう 1つは lift メソッドを使う方法です。

val list = List("foo", "bar")
val f = list.lift
println(f(0))
println(f(2))

1つめのコードのように getOrElse は使えませんが、シンプルに書けます。

2011-10-04

可変長引数を可変長引数をとるメソッドに渡す

とある Java ライブラリのメソッドをラップしてメソッドを作ろうとしていたときのことです。ラップしたいメソッドは以下の java.lang.String#format のように可変長引数をとるものです。
public static String format(String format, Object... args) 
Scala だと scala.collection.immutable.StringLikeformat みたいなものですね。
def format (args: Any*): String
最初は特に何も考えずに以下のように実装してみました。
object Foo {
  def foo(format: String, args: AnyRef*): String = java.lang.String.format(format + " - foo", args)

  def main(args: Array[String]) {
    println(foo("%s", "Fizz"))
    println(foo("%s, %s", "Fizz", "Buzz"))
  }
}
これ、コンパイルはできますが、1つめの println の出力結果は意図とは異なり、2つめの println では例外が発生します。出力結果は以下のようになります。
WrappedArray(Fizz) - foo
java.util.MissingFormatArgumentException: Format specifier 's'
--- snip ---
出力結果からすると java.lang.String#format の引数に可変長引数ではなく WrappedArray という型で渡っているため意図した結果になっていないようです。
以下のようにすることで可変長引数のまま渡すことが出来ました。
object Foo {
  def foo(format: String, args: AnyRef*): String = java.lang.String.format(format + " - foo", args: _*)

  def main(args: Array[String]) {
    println(foo("%s", "Fizz"))
    println(foo("%s, %s", "Fizz", "Buzz"))
  }
}

2011-07-29

HBase で特定行の特定カラムファミリに含まれるカラムを全削除する

たとえば以下のようなテーブルがあったとします。
> scan 'tbl1'
ROW                               COLUMN+CELL
 row1                             column=fam1:col1, timestamp=1311918146512, value=1-1-1
 row1                             column=fam1:col2, timestamp=1311918153880, value=1-1-2
 row1                             column=fam2:col2, timestamp=1311918161976, value=1-2-2
 row2                             column=fam1:col1, timestamp=1311918179710, value=2-1-1
 row2                             column=fam2:col1, timestamp=1311918231004, value=2-2-1
 row2                             column=fam2:col2, timestamp=1311918264142, value=2-2-2
2 row(s) in 0.0760 seconds
このテーブルの row2 のカラムファミリ fam2 に含まれるカラムをすべて削除したい場合、hbase shell で以下のコマンドを実行しても意図通りの結果にはなりません。
> deleteall 'tbl1', 'row2', 'fam2'
意図したとおりの結果を得るには以下のようにします。
> import org.apache.hadoop.hbase.client.HTable
> import org.apache.hadoop.hbase.client.Delete
> t = HTable.new("tbl1".to_java_bytes)
> t.delete(Delete.new("row2".to_java_bytes).deleteFamily("fam2".to_java_bytes))
テーブルの内容を確認すると
> scan 'tbl1'
ROW                               COLUMN+CELL
 row1                             column=fam1:col1, timestamp=1311918146512, value=1-1-1
 row1                             column=fam1:col2, timestamp=1311918153880, value=1-1-2
 row1                             column=fam2:col2, timestamp=1311918161976, value=1-2-2
 row2                             column=fam1:col1, timestamp=1311918179710, value=2-1-1
検証した HBase のバージョン: 0.90.1-CDH3B4

2011-06-30

Play! Scala なプロジェクトで Scaladoc を Ant で生成する。

play コマンドには javadoc コマンドはありますが、scaladoc コマンドはありません。ですので、Scaladoc を生成する場合は別の手段を用意しなければならないのですが、今回 Ant を使ってやってみました。

build.xml は以下の通りです。
<?xml version="1.0" encoding="UTF-8"?>
<project default="doc" basedir=".">
 <property environment="env"/>
 <property name="base.dir" value="."/>
 <property file="${base.dir}/build.properties"/>
 <fail message="PLAY_PATH environment variable or play.path property needs to reference Play! installation"
       unless="env.PLAY_PATH"/>
 <property name="play.path" value="${env.PLAY_PATH}"/>
 <echo message="play.path = ${play.path}"/>
 <import file="${play.path}/resources/application-build.xml"/>
 <fail message="SCALA_HOME environment variable or scala.home property needs to reference Scala installation"
       unless="env.SCALA_HOME"/>
 <property name="scala.home" value="${env.SCALA_HOME}"/>
 <echo message="scala.home = ${scala.home}"/>
 <property name="sources.dir" value="${base.dir}/app"/>
 <property name="doc.dir" value="${base.dir}/doc"/>

 <path id="build.classpath">
   <pathelement location="${scala.home}/lib/scala-library.jar"/>
   <fileset dir="${base.dir}/lib">
     <include name="*.jar"/>
   </fileset>
   <!-- Play! -->
   <fileset dir="${play.path}/framework">
     <include name="**/*.jar"/>
   </fileset>
   <!-- Play! Scala -->
   <fileset dir="${play.path}/modules/scala-${play.scala.version}/lib">
     <include name="*.jar"/>
   </fileset>
   <!--pathelement location="${build.dir}"/-->
 </path>

 <taskdef name="scaladoc" classname="scala.tools.ant.Scaladoc">
   <classpath>
     <pathelement location="${scala.home}/lib/scala-compiler.jar"/>
     <pathelement location="${scala.home}/lib/scala-library.jar"/>
   </classpath>
 </taskdef>

 <target name="doc">
   <mkdir dir="${doc.dir}"/>
   <scaladoc srcdir="${sources.dir}" destdir="${doc.dir}"
             deprecation="yes" unchecked="yes" 
             classpathref="build.classpath">
     <include name="**/*.scala"/>
   </scaladoc>
 </target>

 <target name="clean">
   <delete dir="${doc.dir}"/>
 </target>
</project>

build.properties に Play Scala のバージョンを指定して実行すると Scaladoc が生成されます。
play.scala.version=0.9.1