こんにちは、馬場です。
Scala でDI (Cake Pattern導入編)で、CakePatternはメンバがつまづくところ第2位と書きましたが、ではわかりにくい第1位が何だったのか。それがScalaQueryです。今回は、その難解さの一因、Scalaの黒魔術・暗黙的型変換について考えていきたいと思います。
メソッドがない?!
このプロジェクトでは、諸々の事情でデータベースアクセスのAPIとしてScalaQueryを利用することになりました。このScalaQuery、とにかくドキュメントが少ないので、基本的な記述方法を確認するのにソースを見ることが多かったのです。
あるとき、以下のようなupdate文を実行するプログラムを作成しようと考えました。
1 |
UPDATE users SET first = 'Homer Jay' , last = 'Brown' WHERE first='Homer' |
マニュアルを見るとupdateは以下のように記述する、と書いてあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import org.scalaquery.ql.basic.BasicTable import org.scalaquery.ql.TypeMapper._ import org.scalaquery.ql._ ... object Users extends BasicTable[(Int, String, Option[String])]("users") { def id = column[Int]("id", O NotNull) def first = column[String]("first", O Default "NFN", O DBType "varchar(64)") def last = column[Option[String]]("last") def * = id ~ first ~ last } ... // UPDATE users SET first = 'Homer Jay' WHERE first = 'Homer' を実行する val q7 = for(u <- Users if u.first is "Homer") yield u.first val updated1 = q7.update("Homer Jay") |
このサンプルでカラムを1つ更新するプログラムの記述方法はわかりました。でもこれだけではカラムを2つ更新する場合の記述方法がわかりません。そこで、ScalaQuery のソースコードを眺めてみるのですが、そもそも BasicTable にmap メソッドがない。update文の定義がみたいのに、q7 が何のクラスのインスタンスなのかすらわからない... どういうことなのでしょう。
この出所のわからないメソッドが突然わいてくる現象の正体こそ、Scala の黒魔術・暗黙的型変換のしわざなのです。
暗黙的型変換とは
Scalaには型変換の方法を記述しておくことにより、明示的に変換しなくても暗黙的に型を変換してくれる機能があります。
例えば、Scala のListやMapにはJavaのListやMapにはない便利メソッドがいろいろ用意されています。ただ、これらの便利メソッドは当然JavaのListやMapでは使えません。ScalaではまだまだJavaのライブラリを使うことが多いのですが、JavaのライブラリのメソッドはJavaのCollectionを返すので、当然Scalaの便利メソッドは使えません。
こんなとき、暗黙的型変換の出番です。以下のようにメソッドをimportします。
1 |
import scala.collection.JavaConversions._ |
JavaConversionには、以下のようなJavaのCollectionをScalaのCollectionに変換するメソッドがたくさん定義されています。
1 |
implict def asScalaBuffer[A](l: List[A]): Buffer[A] |
このimplicit がポイントで、implicit があることによりコード中にJavaのCollectionからScalaのCollectionへの変換処理を明示的にかかなくても、Scalaが必要ならば暗黙的に型を変換してくれます。
1 2 |
val arrayList = new ArrayList() val stringList = arraylist.map (_.toString) |
上のコードも、Arraylistに存在しないmapメソッドが実行されるていますが、暗黙的にScala のBufferに変換されるので、Scala のBufferにあるmapメソッドなどが利用できるわけです。
黒魔術の種明かし
ScalaQueryでもこの暗黙的型変換が多くつかわれています。それらはすべてBasicImplicitConversions で定義されています。
1 2 |
// UPDATE users SET first = 'Homer Jay' WHERE first = 'Homer' を実行する val q7 = for(u <- Users if u.first is "Homer") yield u.first |
まずこのBasicTableのインスタンスUsersですが、
1 |
implicit def tableToQuery [T <: org.scalaquery.ql.TableBase[_]] (t: T): Query[T] |
BasicImplicitConversionsの型変換の定義により、BasicTableがQueryに暗黙的に型変換されたあと、
Queryのmapメソッドが実行され、Queryが生成されます。(for(...) yield () 構文はmapメソッドと同じ処理を実行します。)
その後、
1 |
val updated1 = q7.update("Homer Jay") |
では、
1 |
implicit def productQueryToUpdateInvoker [T] (q: Query[ColumnBase[T]]): BasicUpdateInvoker[T] |
この型変換の定義により、Queryがに暗黙的にBasicUpdateInvokerに型変換されたあと、updateメソッドが実行されています。
わかってみたらまあそうかなとおもうのですが、 まずは暗黙的型変換をしてる、ということを発見するまでに一苦労。途中IDEが気を利かせてimport文の整理をしてくれたばっかりに、型変換の定義が抜けてしまい、サンプルが全く動かなくなったりもしました。その後も updateを探し出すのに丹念に型変換の定義をみてかなくてはいけなくて、、参りました。
結論
このような感じで、ScalaQueryの暗黙的型変換の罠には何人もの人がはまってしまったのです。ScalaQueryのようなDSLは、表現を簡潔にしたいので暗黙的型変換を多用されると思うのですが、やはりソースを見なくては処理をおえないような場面では、暗黙的型変換を使ってはいけないでしょう。DSLなのにドキュメントが少なくソースをがっつり見なくてはいけない状況にも問題があるとは思いますが。
もちろんこの暗黙的型変換、便利だなと思う場面もあるわけです。次回はそんなよい例をみつつ、Scalaの暗黙的型変換の使いどころについて考えたいと思います。
※ それでもScalaQueryは今でも使い続けています。ドキュメントも少し増えましたし、なにより1回わかってしまうとやはり書き方が簡潔だしこれはこれでいいな、と思ってしまうのですよね。それが暗黙的型変換。難しい。