Scalaでテスティングフレームワークを試してみる。

ネット情報やガイド本での記述量からの推測だが、Scalaでは何となくテスティングフレームワークがにぎやかな感じがする。逆説的に、Scalaのコードはテストしにくいということかもしれない。Scala型推論などでコード量が少なくて済むというが、Scala初心者の私は、まだ可読性の恩恵を受けている実感がない。参照するAPIや表記法を完全に理解できていないためでもあるが、サンプルコードを眺めてみてもコード量の少ないことを強調している割りには、可読性や試験性(テスト容易性)が本当にいいのかと疑わしいのだ。特に、extendsやwithで大量に継承されるといやだな。作法としてのコードスタイル、テストを容易にするためのコードスタイルなどは提唱されていないのかな。デシジョンカバレッジを100%にするようなテストは書けるのか、そもそもScalaステートメントやブランチを網羅的にテストすることはできるのか。

テスティングフレームワーク

JUnitでさえ十分には使ったことがないのだが、せっかくだからScalaでテスティングフレームワークを学習しておきたい。代表的・標準的なものとしては、以下のようなものが挙げられていた。
 ・ScalaTest
 ・Specs2
 ・JUnit
 ・ScalaCheck
 ・TestNG
 ・ScalaMock
 ・Mockito
 ・Setak
 ・Jacoco
とりあえず、私も、ScalaTestとSpecs2を試してみた。カバレッジを見れるものも知りたかったが、まだリサーチ不足だ。Jacocoは試したがまだうまく動作してくれない。Scala開発の現場での実態はどうなのだろう。非常に小さなサンプリングだが、比較的Scala経験の長い人に伺ってみたところ、ScalaTestやSpecs2をバリバリつかっている感じではなかった。テスト駆動が浸透していないだけなのか、Scalaのテスティングフレームワークが好みにそぐわないのかは判断できない。またネット情報でも、テスト用のコードやデータを自動生成してくれるようなものもあまり見当たらない。「scala-testgenというテスト自動生成ツールをつくりました」という情報があった程度だ。Eclipse上でtest用のコードのテンプレートくらいは自動生成してくれてもいいのだが、Scalaの開発では重いEclipseは敬遠されているということだろうか。IDEならIntelliJ IDEAを選択した方がいいのかな。
テスティングフレームワークは、ユニットテスト用のものと受け入れテスト用のものの両方を提供するものが多い。ユニット(=コンポーネントScalaならClassやObject)の外部仕様をテストするものを「受け入れテストスタイル」といっているようだ。ユニット内部の制御構造などをチェックするものが「ユニットテストスタイル」だ。テスト駆動で開発するなら、ユニットのインタフェースが決まったら(実際にはこのタイミングや判定がはっきりしない)、まず受け入れテストスタイルでテストコードを書けということだろう。しかし、ほとんどのScalaのガイド本は、ユニットテストスタイル、受け入れテストスタイルの順に記述されていて、テスト駆動を推進する感じがないのだ。実態はぜいぜいユニットテストスタイルに留まっているということかもしれない。
受け入れテストスタイルは、ユースケースのシナリオに合わせて記述できそうだ。クラス単体の外部仕様をテストするというより、プレゼンテーション層とやり取りするコントロール層のテストに使えそうだ。ユースケースのシナリオ記述に沿った機能テストに使える。ScalaTestでは before,afterといったメソッドも用意されていて、シナリオの事前条件、事後条件を表現できる。BDD(ふるまい駆動開発)と言っているが、オブジェクト指向であるならもっとユースケースのシナリオ駆動と言い切っていいのではないか。
また、ガイド本にあった受け入れテストスタイルのサンプルが情けない。Hello Worldのサンプルアプリに受け入れテストスタイルを適用するには無理があるだろう。せめてHello Worldの文字列を生成して返すコントロール層とそれを表示するプレゼンテーション層に分離して、コントロール層の機能テストに受け入れテストスタイルを適用するというなら許せるかな。Hello Worldのサンプルが受け入れテストスタイルを誤った方向に誘いそうだ。

ScalaTest

一応、ガイド本をもとに、ScalaTestとSpecs2を試してみた。ScalaTestにはFunSuiteやFunSpecのトレイトが用意されている。これらは matchers を内包しないので ShouldMatchers や MustMatchers をミックスインするスタイル(extends FubSuite with MustMatchers など)で利用する。一方、Specs2の Specification は matchers も内包しているので継承するだけ(extends Specification のみ)で matchers が使える。
scalatestの公式サイト(http://www.scalatest.org/download)からScala 2.10用のjarを「Download ScalaTest 1.9.2 Jar」のリンクからダウンロードし、Eclipseのビルドパスに追加しておく。build.sbtを編集して、以下の記述を追加し、cmdからsbtを起動する。追加の際は、Scala式のセパレータである空行を挟んでおくこと。


libraryDependencies ++= Seq(
"org.scalatest" %% "scalatest" % "1.9.2" % "test"
)
まずは、FunSuiteを継承したクラスを作成し、都度assertを使って記述してみる。基本はJUnitと変わらない感じだ。コードは割愛する。

Specs2

Specs2のSpecificationは、その名のとおり、テストを書くのではなく、仕様をコードで表現するんだという意気込みが伝わってくる。Specs2を誤解して捉えていたかもしれない。しかし、私にはSpecificationの表記法についていけそうにない。Scalaでもない、別のスクリプト言語といった感じだ。Specs2のSpecificationは後回しにしたい。
Specs2で注意する点をネット上で見つけた。Specs2では複数のテストケースの実行はデフォルトで並行に行われる。before,afterのメソッドが適用される順序は保証されないかも知れない。明示的にsequential()を呼び出して、並行処理しないようにする必要があるそうだ。
ScalaTestもSpecs2も、libraryDependenciesをbuild.sbtに確実に記述し、必要なjarがビルドパスに追加されていれば、テスティングフレームワークは動作するようだ。手順やサンプルコードは、ガイド本に従えばよい。また後日、詳細に調査してみます。

ScalaでPlay2を試してみる。

ScalaのWebアプリケーションフレームワークには、LiftとPlay2があるが、今回Play2を試してみた。数日前にplay-2.1.3をインストールしたのだが先ほど改めてサイトをみたらplay-2.2.0なるものが登場していた。手順の再確認を兼ねて、play-2.2.0で確かめてみる。

(1) Play2のインストール

http://www.playframework-ja.org/ から play-2.2.0.zip をダウンロードして、展開する。私の環境では C:\pleiades\play-2.2.0 にしてみた。PATHにC:\pleiades\play-2.2.0を追加しておく。cmdコンソールからplayを実行する。sbtが動作して必要なファイルをretrieveしてくれる。初回は数分かかる。この時点では「This is not a play application!」と表示されて終わる。


> play
Getting org.fusesource.jansi jansi 1.11 ...

retrieving :: org.scala-sbt#boot-jansi

confs: [default]
1 artifacts copied, 0 already retrieved (111kB/75ms)
... (snip)
play 2.2.0 built with Scala 2.10.2 (running Java 1.7.0_25), http://www.playframework.com

This is not a play application!

Use `play new` to create a new Play application in the current directory,
or go to an existing application and launch the development console using `play`.

You can also browse the complete documentation at http://www.playframework.com.
>

ネット情報などを参考にプロジェクト(myPlayApp)を作成し、そのプロジェクトをeclipseに取り込めるようにしてみよう。play new myPlayApp を実行すると、myPlayAppのディレクトリが作成される。そこに cd して、再度 play を実行する。すると、プロジェクト名をプロンプトにしたplayのコンソールが動き出す。help playでコマンドを確認してみよう。compile,run,start,eclipseなどのコマンドがあることが分かる。exitでコンソールを抜けるようだ。eclipseコマンドで、myPlayAppプロジェクトをeclipseから読み込める形式に変換してくれるようだ。

> play new myPlayApp

... (snip)

The new application will be created in C:\pleiades\play-2.2.0\myPlayApp

What is the application name? [myPlayApp]
> myPlayApp // 名前を入力
Which template do you want to use for this new application?
1 - Create a simple Scala application
2 - Create a simple Java application
> 1 // 1を選択
OK, application myPlayApp is created.

Have fun!
> cd myPlayApp // 生成されたプロジェクトディレクトリに移動
> play // そこでplayを実行
Getting org.scala-sbt sbt 0.12.2 ...

retrieving :: org.scala-sbt#boot-app

confs: [default]
40 artifacts copied, 0 already retrieved (8381kB/2550ms)
[info] Loading project definition from C:\pleiades\play-2.1.3\myPlayApp\project
[info] Set current project to nengou (in build file:/C:/\pleiades/play-2.1.3/myPlayApp/)
... (snip)

[myPlayApp] $ // プロンプトが変わる
[myPlayApp] $ help play // コマンド一覧を見てみる
Welcome to Play 2.2.0!
These commands are available:

                                                        • -

classpath Display the project classpath.
clean Clean all generated files.
compile Compile the current application.
console Launch the interactive Scala console (use :quit to exit).
dependencies Display the dependencies summary.
dist Construct standalone application package.
exit Exit the console.
h2-browser Launch the H2 Web browser.
license Display licensing informations.
package Package your application as a JAR.
play-version Display the Play version.
publish Publish your application in a remote repository.
publish-local Publish your application in the local repository.
reload Reload the current application build file.
run Run the current application in DEV mode.
test Run Junit tests and/or Specs from the command line
eclipse generate eclipse project file
idea generate Intellij IDEA project file
sh execute a shell command
start Start the current application in another JVM in PROD mode.
update Update application dependencies.
...
[myPlayApp] $ eclipse // プロジェクトをEclipse用に変換

[info] Resolving org.scala-lang#scala-library;2.10.2 ...
[info] Resolving com.typesafe.play#play-jdbc_2.10;2.2.0 ...
[info] Resolving com.typesafe.play#play_2.10;2.2.0 ...
 ... (snip)
[info] Done updating.
[info] Compiling 5 Scala sources and 1 Java source to C:\pleiades\play-2.2.0\myPlayApp\target\scala-2.10\classes...
[info] Successfully created Eclipse project files for project(s):
[info] myPlayApp

[myPlayApp] $ exit // playを終了
>

(2) Play2の動作確認

これでEclipseから扱えるプロジェクトに変換されたはずだ。コマンドをみると、Intellij IDEA用のコマンド「idea」も用意されているようだ。次は、eclipseからmyPlayAppプロジェクトを読み込んでみる。その前にmyPlayAppの動作を確認してみよう。


> play
[myPlayApp] $ run

(Running the application from SBT, auto-reloading is enabled)

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

ブラウザでURLに http://localhost:9000/ を指定すると「Your new application is ready.」のページが開く。一旦、Ctrl+Dでplayを終了させて、私は、myPlayAppのプロジェクトディレクトリをEclipseのworkspaceの下に移動させた。playのバージョンが変わるごとに面倒かもしれないので。

(3) Eclipseへのプロジェクトの取り込み

Eclipseを起動し、先ほど作成してworkspaceに移動したプロジェクトをインポートする。「ファイル --> インポート --> 既存プロジェクトをワークスペースへ」を選択して、場所を指定する。私の場合、C:\pleiades\workspace\myPlayApp だ。以下のような構成になっていた。


myPlayApp
|
+-- app/
| +-- controllers/
| | +-- Application.scala // 開いてみよう!
| +-- views/
| +-- index.scala.html // 開いてみよう!
| +-- main.scala.html // 開いてみよう!
+-- conf/
| +-- application.conf
| +-- routes
+-- logs/
+-- project/
| +-- project/
| +-- target/
| +-- build.properties
| +-- Build.scala
| +-- plugins.sbt
+-- public/
+-- target/
| +-- native_libraries/
| +-- resolution-cache/
| +-- scala-2.10/
| +-- streams/
+-- test/
| +-- ApplicationSpec.scala // 開いてみよう!
| +-- IntegrationSpec.scala // 開いてみよう!
+-- build.sbt // 開いてみよう!
+-- README
build.sbtも用意されているし、Specs2で書かれたテストコードもあるようだ。cmdを開いて、sbtとたたくとsbtコンソールになるかと思えば、いきなりplayコンソールになる。ついでだからtestを試してみた。ApplicationSpec.scalaとIntegrationSpec.scalaが動作していることが分かる。大量のjarをretrieveしているようで初回は結構時間がかかった。testの結果、勿論エラーはないようだ。

> sbt
> set SCRIPT_DIR=C:\scala\bin\
> java -Xmx512M -Xss1M -jar "C:\scala\bin\sbt-launch.jar"
[info] Loading project definition from C:\pleiades\workspace\myPlayApp\project
[info] Set current project to myPlayApp (in build file:/C:/pleiades/workspace/myPlayApp/)
[myPlayApp] $
[myPlayApp] $ test
... (snip)

[info] Done updating.
[info] Compiling 5 Scala sources and 1 Java source to C:\pleiades\workspace\myPlayApp\target\scala-2.10\classes...
[info] Compiling 2 Scala sources to C:\pleiades\workspace\myPlayApp\target\scala-2.10\test-classes...

[info] ApplicationSpec // test/ApplicationSpec.scalaがチェックされている。
[info] Application should
[info] + send 404 on a bad request
[info] + render the index page
[info] Total for specification ApplicationSpec
[info] Finished in 2 seconds, 197 ms
[info] 2 examples, 0 failure, 0 error
[info] IntegrationSpec // test/IntegrationSpec.scalaがチェックされている。
[info] Application should
[info] + work from within a browser
[info] Total for specification IntegrationSpec
[info] Finished in 2 seconds, 703 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 0, Failed 0, Errors 0, Passed 0
[success] Total time: 10 s, completed 2013/09/20 15:54:03
[myPlayApp] $

再度、runしてみる。ブラウザでURLに http://localhost:9000/ を確認する。先ほどと同じ「Your new application is ready.」のページが開く。

[myPlayApp] $ run
(Running the application from SBT, auto-reloading is enabled)

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

(Server started, use Ctrl+D to stop and go back to the console...)

(4) Eclipse上での編集

今度は、app/controllers/Application.scalaを、たとえば以下のようにEclipse上で修正して、保存する。


package controllers
import play.api._
import play.api.mvc._
object Application extends Controller {
def index = Action {
Ok(views.html.index("Your new application の準備ができた。"))
}
}
後はブラウザ上で表示のページをリロードするだけだ。誰かが勝手にcompileしてuploadしてくれている。「Your new application の準備ができた。」に変わって表示される。実行中のPlayのコンソールをみると、いつのまにかログが追加されている。

[info] play - Application started (Dev)
[info] Compiling 1 Scala source to C:\pleiades\workspace\myPlayApp\target\scala-2.10\classes...
(RELOAD)

[info] play - Application started (Dev)

playがやってくれていたのだ。重いけど機能していることは確認できた。ところで build.sbt には以下のように記述されている。specs2の記述がないのにさきほどtestは動作していた。jdbc,anorm,cacheのどれかから参照されているのだろうか。各ライブラリの依存関係をある程度認識しておかないと、検討がつかないな。依存関係をcall-graphっぽく見せてくれるコマンドなどはあるのだろうか。

name := "myPlayApp"

version := "1.0-SNAPSHOT"

libraryDependencies ++= Seq(
jdbc,
anorm,
cache
)

play.Project.playScalaSettings

【追記 - 2013/09/22】依存関係の表示には、playコマンドのdependencies、またプラグインのsbt-dependency-graphなどが有効との情報をいただきました。またspecs2の参照は、jdbc,anorm,cacheからではなく、play.Project.playScalaSettingsの中から辿られるということです。コメントを参照のこと。また、playコマンドでは、startも使われるコマンドであると後から認識しました。

main,App,Applicationとか、冒頭からわかりにくいぞScalaさん。

なぜmainの記述方法が3つも用意されているのだ。「def main(args: Array[String])」の1行を省略させたいがためか、それをJavaとの比較で優位性を強調したいのか。Scalaの学習者は、冒頭から余計に悩まされることになる。


object Hi {
def main(args: Array[String]) = {
println("Hi! main")
}
}

object Hi extends Application {
println("Hi! Hi! Application")
}

object Hi extends App {
println("Hi! Hi! App")
}
上記の3つは、どれも似たような動作をする。Applicationの方は推奨ではないらしく、Appに書き換わる経過措置のようだ。

ScalaLispのようにコードとデータを区別する必要もない。Scalaでは言いすぎだが、Lispは正しくそうだ。Scalaの開発者はそれを意識しているのか、サンプルコードにソースコードを読み込んで処理させるものが目立つ。コードをread-evalして実行させたいのだ。Lispでは常套のことだった。上記のAppの実装もそれに近い。App内の実行ステートメントを保持して、mainの中でevalしている感じだ。


object Hi {
println("Hi! before")
def main(args: Array[String]) = {
println("Hi! main")
}
println("Hi! after")
}
と書くとコンパイルエラーにはならず、以下のように表示される。mainは最後に呼び出されていることが分かる。

Hi! before
Hi! after
Hi! main
では次にAppで試す。mainはoverrideしている。

object Hi extends App {
println("Hi! before")
override def main(args: Array[String]) = {
println("Hi! main")
}
println("Hi! after")
}
これだと以下のようにmainしか表示されない。

Hi! main
before、afterの実行ステートメントは実行されない。それを実行してくれるmainを書き換えているからだ。最後にApplicationで試す。mainは同様にoverrideしてみる。

object Hi extends Application {
println("Hi! before")
override def main(args: Array[String]) = {
println("Hi! main")
}
println("Hi! after")
}
これだと以下のようにbefore,after,mainの順に表示される。最初の通常のmainの記述と同じ結果だ。

Hi! before
Hi! after
Hi! main
この結果から、Appのみは、記述された実行ステートメントを保持して、mainの内部でevalしているようだ。必要ですか、Appが。そもそも、def mainの外に書いたprintln("Hi! before")やprintln("Hi! after")が実行されるというのは問題のない仕様なのだろうか。

ScalaでSBTを試してみる。

ScalaベースのビルドツールであるSBT(Simple Build Tool)を試してみる。

「始める sbt」に沿って学習

ぎこちない和訳だが「始める sbt」(http://scalajp.github.io/sbt-getting-started-guide-ja/setup/)を参照しながら進めた。まず、sbt-launch.jarとsbt.batの2つのファイルが必要だ。sbt-launch.jarはダウンロードしてくる。sbt.batは新規に作成して、


set SCRIPT_DIR=%~dp0
java -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=384M -jar `dirname $0`/sbt-launch.jar "$@"
と記述せよとある。しかし、実際に実行すると「Error occurred during initialization of VM Could not reserve enough space for object heap」といったエラーが出た。VMのサイズを512Mくらいに小さくする必要があるようだ。

set SCRIPT_DIR=%~dp0
java -Xmx512M -jar "%SCRIPT_DIR%sbt-launch.jar" %*
これだとうまくいく。これら2ファイルをPATHの通ったところに配置する。とりあえず、C:\scala\bin に押し込んでおいた。「始める sbt」の手順に沿って、Hello, Worldを試してみる。私は、C:\pleiades\workspace\hiというディレクトリを作って、Hi.scala に以下のように記述した。

object Hi {
def main(args: Array[String]) = {
println("Hi!")
}
}
Linux風に echo 'object Hi { def main(args: Array[String]) = println("Hi!") }' > Hi.scala とやると、シングルquoteまで書き込まれてcompile時にエラーとなってしまうではないか。手作業で修正し、いよいよsbtを実行する。必要なjarファイルが各所からretrieveされてくる。資料によると、デフォルトでは、Maven,Typesafe,Sonatypeの3つのリポジトリを参照するようだ。そのため、初回のsbtは結構時間(3〜5分)がかかる。retrieveが終わると、sbtのコンソールが起動された状態になる。(※以下ではecho offしていないので、setやjavaのコマンドもechoとして表示されている。)

$ sbt
$ set SCRIPT_DIR=C:\scala\bin\
$ java -Xmx512M -jar "C:\scala\bin\sbt-launch.jar"

Getting org.fusesource.jansi jansi 1.11 ...
downloading http://repo1.maven.org/maven2/org/fusesource/jansi/jansi/1.11/jansi-1.11.jar ...
[SUCCESSFUL ] org.fusesource.jansi#jansi;1.11!jansi.jar (785ms)

retrieving :: org.scala-sbt#boot-jansi

confs: [default]
1 artifacts copied, 0 already retrieved (111kB/86ms)
Getting org.scala-sbt sbt 0.13.0 ...

...(snip)

[info] Set current project to hi (in build file:/C:/pleiades/workspace/hi/)
sbt>

そしてrunコマンドで先ほどのHiオブジェクトを実行させる。scalaに必要なjarがチェックされ、Hiオブジェクトをコンパイルして実行してくれる。一応「Hi!」と出た。

sbt> run
[info] Updating {file:/C:/pleiades/workspace/hi/}hi...
[info] Resolving org.scala-lang#scala-library;2.10.2 ...
[info] Resolving org.scala-lang#scala-compiler;2.10.2 ...
[info] Resolving org.scala-lang#scala-reflect;2.10.2 ...
[info] Resolving org.scala-lang#jline;2.10.2 ...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to C:\pleiades\workspace\hi\target\scala-2.10\classes...
[info] Running Hi
Hi!
[success] Total time: 2 s, completed
sbt> exit
>
sbtには、基本コンフィグ(build.sbt)とフルコンフィグがある。とりあえず、各プロジェクトでは基本コンフィグ用のbuild.sbtスクリプトファイルを作成しておけというようだ。フルコンフィグでは、Build.scalaとしてScalaで記述することになる。build.sbtには以下の5行を記述した。空行がScala式のセパレータになるので削ってはいけない。最後行には空行はなくてもいい。

name := "Hi"

version := "1.0"

scalaVersion := "2.10.2"

build.sbtはプロジェクトの直下におく。Hi.scalaもsrc/main/scalaの下に移動する。テスティングフレームワークの利用も考慮して、main,testで分けているようだ。またscalajavaの共存を意識したディレクトリ構成だ。

Hiプロジェクト
+-- project/ // フルコンフィグ用
+-- src/
| +-- main/
| | +-- java/
| | +-- scala/
| | +-- Hi.scala // Hiオブジェクトはここに配置する
| +-- test/
+-- target/
+-- build.sbt // 前述の基本コンフィグ用のスクリプトファイル
構成を変更した後、再度sbtを実行し、sbtコンソールでcompileとrunを順に実行する。「Hi!」と出ている。

> sbt
> set SCRIPT_DIR=C:\scala\bin\
> java -Xmx512M -Xss1M -jar "C:\scala\bin\sbt-launch.jar"
[info] Set current project to Hi (in build file:/C:/pleiades/workspace/hi/)
sbt> compile
[info] Updating {file:/C:/pleiades/workspace/hi/}hi...
[info] Resolving org.scala-lang#scala-library;2.10.2 ...
[info] Resolving org.scala-lang#scala-compiler;2.10.2 ...
[info] Resolving org.scala-lang#scala-reflect;2.10.2 ...
[info] Resolving org.scala-lang#jline;2.10.2 ...
[info] Resolving org.fusesource.jansi#jansi;1.4 ...
[info] Done updating.
[info] Compiling 1 Scala source to C:\pleiades\workspace\hi\target\scala-2.10\classes...
[success] Total time: 6 s, completed
sbt> run
[info] Running Hi
Hi!
[success] Total time: 0 s, completed
sbtのコマンドは、compile,run,reload,test,exitだけ覚えておけば十分そうだ。reloadは構成を変更したとき、testはテスティングフレームワーク(scalatestなど)で書いたテストコードを実行するときだ。次は、Hi.scalaを以下のように編集してみる。mainの関数をやめ、Appを継承させるのだそうだ。再度sbtを実行し、sbtコンソールでcompileとrunを順に実行する。

object Hi extends App {
println("Hi! Hi! App")
}
今度はたしかに「Hi! Hi! App」と出る。Appを継承するとmainを書かなくていい。似たようなApplicationでも同様だそうだ。いまいちこのあたりの仕組みが分かっていない。Appはなんのため。似たようなApplicationは。sbtだと[success] Total time ...も表示されるが、それはsbtがやっているのか、Appがやっているのか。早くScalaTestを取り込めるようにbuild.sbtの依存関係に追加しておきたいのだが、AppとApplicationが気になって次に進めない。

Scalaで永続化を試してみる。

Scalaでは、永続化に関してはSLICK(=ScalaQuery)を利用するべきなのかもしれないが、まずはベタでJDBCを利用してDBへのアクセスを試してみた。

(1) スキーマ定義とエンティティクラス

helloというデータベースにaccountというテーブルを定義してみた。このテーブル(account)に対応するエンティティクラス(Account)を作成してみる。


use hello; // MySQLの場合
// \connect hello; // Progresqlの場合
create table account (
uid varchar(10) primary key,
name varchar(20),
pwd varchar(32)
);
insert into account (uid,name,pwd) values ('ID00001', '管理者Aさん','aaa');
insert into account (uid,name,pwd) values ('ID00002', 'Bさん','bbb');
insert into account (uid,name,pwd) values ('ID00003', 'Cさん','ccc');
select * from account;
エンティティクラス(Account)は以下の感じだ。

package hello.domain

class Account(a_uid: String, a_name: String = null, a_pwd: String = null) {

var uid: String = a_uid
var name: String = a_name
var pwd: String = a_pwd

override def toString(): String = {
uid + "(" + name + ")"
}
def dump(): Unit = {
println("uid=" + uid)
println("name=" + name)
println("pwd=" + pwd)
}
}

初心者なので「(): Unit = 」などは省略しない。引数にvalやvarを指定して、属性の定義を割愛できるようだが、初心者はやるべきではないと思う。以下の感じでテストできる。

package hello
import hello.domain.Account

object Main {
def main(args:Array[String]) = {
var user = new Account("ID00001", "Aさん")
user.pwd = user.name + "のパスワード";
user.dump
println(user)
}
}

(2) DBへのアクセッサクラス

そして、DBにアクセスするために3つのクラスを定義してみた。初心者なので、まだJavaっぽくしか書けない。まず、DBからConnectionを取得するオブジェクト(PersistenceMgr)だ。一応、MySQLとProgresqlに接続できるように書いてみた。


package hello.db
import java.sql.{Connection, DriverManager};

object PersistenceMgr {

/**
* postgresqlmysqlに対応。都度、dbmsに所定の字面をセットすること
* propertiesなどから取得するようにはしたい。
* アカウントは、/に任意に設定。database名はhelloで固定
* encodingはUTF-8であること
*/
// val dbms: String = "postgresql"
val dbms: String = "mysql"
var conn: Connection = null

/**
* Connectionを取得する。1回生成したConnectionを使いまわす。
*/
def connect(): Connection = {
if (conn == null){
var connStr: String = null
dbms match {
case "mysql" =>
connStr = "jdbc:mysql://localhost/hello"
Class.forName("com.mysql.jdbc.Driver").newInstance()
case "postgresql" =>
connStr = "jdbc:progresql://localhost/hello"
Class.forName("org.postgresql.Driver").newInstance()
case _ =>
}
if (connStr != null) {
conn = DriverManager.getConnection(connStr, "", "")
}
}
conn
}
}

次に、DBへのアクセッサのベースクラス(PersistenceBase)だ。データベースの所在を掩蔽し、Connectionに対してアクセスする。

package hello.db
import java.sql.{Connection, Statement};

class PersistenceBase {

/**
* Connectionはコンストラクタ実行時に取得される。
*/
private val conn: Connection = PersistenceMgr.connect
private var stmt: Statement = null

/**
* Statementを取得する。
*/
def statement: Statement = {
close // まず、念のためstmtのクローズを試みる。
stmt = conn.createStatement // stmtを生成する。
stmt
}

/**
* Statementを明示的にクローズする。
*/
def close {
if (stmt != null){
stmt.close
stmt = null
}
}
}

最後にPersistenceBaseを継承して、テーブル(account)に対応するアクセッサクラス(AccountDA)を書いてみる。Connectionを掩蔽し、Statementに対してアクセスする。select,updateといったSQLのイメージをあえて残しておく。

package hello.db
import hello.domain._
import java.sql.{ResultSet}
import scala.collection.mutable._

class AccountDA extends PersistenceBase {

private def createAccount(rs: ResultSet): Account = {
new Account(rs.getString("uid"), rs.getString("name"), rs.getString("pwd"))
}

/**
* ユーザーを検索する。
*/
def selectUsers(): List[Account] = {
var arr = new ListBuffer[Account]
try {
val rs: ResultSet = statement.executeQuery("SELECT * FROM account ORDER BY uid")
while (rs.next) {
arr += createAccount(rs)
}
} finally {
close
}
arr.toList
}
/**
* UIDを指定してユーザーを取得する。
*/
def selectUser(uid: String): Account = {
var user: Account = null
try {
val rs: ResultSet = statement.executeQuery("SELECT * FROM account WHERE uid='" + uid + "'")
if (rs.next) {
user = createAccount(rs)
}
} finally {
close
}
user
}
/**
* ユーザー情報を更新する。現在、名前のみ更新できる。
* 更新した件数を返す。通常は1件。
*/
def updateUser(user: Account): Int = {
var res = 0;
try {
res = statement.executeUpdate(
"UPDATE account SET name='" + user.name + "' WHERE uid='" + user.uid + "'"
);
} finally {
close
}
res
}
}

(3) アカウントを扱うコントローラクラス

永続化の部分はできたので、アカウントを扱うコントローラ(AccountMgr)を書いてみる。ここでDBアクセスを掩蔽する。


package hello.controller
import scala.collection.mutable._
import hello.domain._
import hello.db._

object AccountMgr {

/**
* DBアクセッサを保持しておく。
*/
private var ada: AccountDA = new AccountDA

/**
* 登録されているユーザー全員を取得する。
*/
def users(): List[Account] = {
ada.selectUsers
}

/**
* ユーザーを認証する。
* UIDとPWDで認証し、該当するアカウントがあればそれを生成して返す。
*/
def login(uid: String, pwd: String): Account = {
var user = ada.selectUser(uid)
if (user != null){
// 指定のUIDに該当するアカウントが存在する場合
if (pwd != user.pwd){
// パスワードが一致しない場合
user = null
}
}
user
}

/**
* ユーザー情報を更新する。現在は、名前のみ更新できる。
*/
def updateUser(user: Account): Int = {
ada.updateUser(user)
}

/**
* ユーザーリストをダンプする。
*/
def dumpUsers(v: List[Account]) {
var i = 1;
for (e <- v){
println("[" + i + "]=" + e)
i += 1
}
}
}

以上で、コントローラからDBへのアクセスも可能になった。ログイン認証なども実装できそうだ。以下の感じで動作を確認できる。

package hello
import hello.domain.Account
import hello.controller.AccountMgr

object Main {
def main(args:Array[String]) = {

val amgr = AccountMgr
var users = amgr.users // ユーザーリストを取得
amgr.dumpUsers(users)

var user = users.head // リスト先頭のユーザーを取得
user.name += "(更新)" // 名前を変更
amgr.updateUser(user) // 更新
amgr.dumpUsers(amgr.users) // ユーザーリストを再取得

user = amgr.login("ID00001","aaa") // 認証成功
println(user)
user = amgr.login("ID00001","xxx") // 認証失敗
println(user)
}
}

以上で一応、DBへのアクセスの手順も確認できた。次回は、ScalaらしくSLICKを使って練習してみよう。SLICKにもベタでSQLを記述するモードと、SQL掩蔽してくれるモードがあるようだ。

(おまけ) Eclipse上での型推論

Scalaでは型推論を利用して記述量を少なくできるが、Eclipse上では、型はできるだけ省略しない方がいいというジレンマがある。省略すると、Eclipseを操作しているときに型推論がちょくちょく動作するが、これが結構重い感じなのだ。例えば、


val amgr = AccountMgr
var users = amgr.users
users.head
だと、「amgr.」や「users.」と打ち込んだ時点で、型推論とメソッドや属性の一覧メニューの作成が始まるようだ。このタイミングで割り込まれると結構重く感じられる。

val amgr: AccountMgr = AccountMgr
var users: List[Account] = amgr.users
users.head
のように明示的に型を指定しておくと、結構すんなりいく感じなのだ。

久々にScalaを試してみる。

久々にScalaを試している。以前少し使ったことはあるが、ゼロからの学習に等しい。そのメモを残しておこう。

(1) Scala開発環境の準備

Scalaは、公式サイト( http://www.scala-lang.org/download/ )から最新版(2013/09時点では2.10.2)をダウンロードしてアーカイブを解凍する。解凍したものは、C:\scala に配置した。環境変数を設定する。
SCALA_HOME に C:\scala を設定しておく。
・ PATH に %SCALA_HOME%/bin を追加する。なお、Scalaをインストールすると勝手にPATHに追加されているようだ。
なお、JavaJDKは事前にインストールしておく。また、テキストエディタとしてはサクラエディタを推奨するとの情報をいただいたので、事前にインストールしておいた。sinst2-1-0-0.exe をダウンロードして実行しておいた。合わせて、Eclipsescalaを利用したいので Eclipse 3.7(Indigo)を準備しておいた。typesafe社が提供するScala IDE for Eclipseではなく、Java用のEclipseである。実際には、日本語対応されたPreiades All in One JavaEclipse 3.7 をダウンロードしてインストールした。play2のフレームワークも入れてみた。以下の構成になる。


C:\java\jdk1.7.0 // JDK
C:\scala\ // Scala 2.10
C:\pleiades\eclipse-scala // Eclipse 3.7 (※scala用ではない)
C:\pleiades\eclipse-scala\workspace
C:\pleiades\eclipse-scala\workspace\hello // 誰もがscalaで最初につくるHelloWorldプロジェクト
C:\pleiades\play-2.1.3 // Play2フレームワーク
C:\pleiades\play-2.1.3\myPlayApp // 誰もがplay2で最初につくるアプリ

(2) Scalaの動作確認

Scalaの動作を確認してみる。cmdからscalaのバージョンを確認し、scalaのコンソールを起動してみる。


> scala -version
Scala code runner version 2.10.2 -- Copyright 2002-2013, LAMP/EPFL

> scala
Welcome to Scala version 2.10.2 (Java HotSpot(TM) Client VM, Java 1.7.0_25).
Type in expressions to have them evaluated.
Type :help for more information.

scala> 1
res0: Int = 1

scala> 1 + 1
res1: Int = 2

scala> var a = 1
a: Int = 1

scala> a + 1
res2: Int = 2

scala> 'hello
res3: Symbol = 'hello

scala> "hello"
res4: String = hello

scala> res4
res5: String = hello

scala> res4 + res3 + res2
res6: String = hello'hello2

scala> :quit

read-eval-printのループが機能している。REPLと呼ぶそうだ。評価結果はres+行番号の変数にバインドされているようだ。Symbolをconcatinateするとシングルquoteがくっつくのか。Lispとはちょっと違うな。:quitまたはexitでコンソールを抜けられる。単にquitやbyeはダメみたい。もう少し確認してみよう。

scala> // 単にenterなら空行のみ。

scala> println("hello") // 履歴にはバインドされない。
hello

scala> print("hello") //
hello
scala> print("hello"); print(' '); print("world")
hello world
scala> print("hello"); print(' '); print("world"); 1
hello worldres5: Int = 1

scala> true // Trueはエラー
res6: Boolean = true

scala> null // nul,nil,Nullはエラー
res7: Null = null

scala> Unit // unitはエラー
res8: Unit.type = object scala.Unit

scala> ' ' // ''はエラー
res9: Char =

scala> 'a'
res10: Char = a

scala> 'a
res11: Symbol = 'a

scala> () // 履歴にはバインドされない。

scala> (1) // リストにはならない。1と等しい
res13: Int = 1

scala> (1, 2) // これはリストになる
res14: (Int, Int) = (1,2)

scala>

値を返さない(返値がUnit)のS式と()の結果が同じだ。Unit=()と言える。そもそも、LispでS式と呼んでいたものはScalaでは何と呼べばいいのだろう。返値がUnit(つまり値として()を持つ唯一のオブジェクト)である場合、REPLは改行のみを行うようだ。print("hello")の実行、()の実行結果から分かる。また何も入力しない/white-spaceのみの入力の場合も、read部が受け付けていないというより、print部が改行のみを行うと理解した方がいいのだろうか。

(3) Eclipseの設定

Eclipseは、Preiades All in One Javaの日本語Eclipse 3.7(pleiades-java-3.7.2.exe)をダウンロードしてインストールした。C:\pleiades\eclipse-scala に配置した。typesafeが提供するscala用のEclipseではないが、個人環境でJava用とScala用で区別したいので、新たなEclipseを「eclipse-scala」というディレクトリ名でインストールした。メニューなども既に日本語対応なので、新たに日本語プラグインを入れる必要はない。
なお、typesafeが提供するScala IDE for Eclipseは利用しないことにした。日本語対応していないようだし、さらに重いとの情報もある。Scala IDE for Eclipsehttp://scala-ide.org/download/sdk.html からダウンロードできるし、scala 2.10.xに対応したIDEである。
Eclipseを起動した後、scala 2.10 IDE for Eclipseをインストールする必要がある。「ヘルプ --> 新規ソフトウェアのインストール」を選び、「追加」で「http://download.scala-ide.org/sdk/e37/scala210/stable/site」のURLを指定し、適当に「scala 2.10 IDE」などとタイトルをつけて登録する。インストールするコンテンツが5つリストされる。最小でも「Scala IDE for Eclipse」は選択すること。ソースを含めてすべてを選択してもいい。Wizardを進めると、以下のものがインストールされたと表示された。InSynth Features, Play2, IDE for Eclipse, Search, Worksheet, ScalaTest for Scala IDEだ。Eclipse を再起動して有効となる。
hello worldを試してみる。「ファイル --> 新規 --> プロジェクト」で「scalaウィザード --> scalaプロジェクト」を選択できるようになっている。プロジェクト名に「hello」と入れてみる。サンプルをもとにMainという名前のscalaオブジェクトを新規に作成し、


object Main {
def main(args: Array[String]) = {
println("hello")
}
}
保存して「Main.scala」ファイルを選択して「実行 --> scala アプリケーション」で実行してみる。プロジェクトはデフォルトでUTF-8になっている。うまくいく。

おまけ(日本語コーディング)

Scalaは日本語コーディングに適しているとも聞いたので、ちょっと確認してみた。変数名まで日本語にしてみる。


object Main {
def main(引数: Array[String]) = {
println("こんにちは")
}
}
うまくいく。今度は「Main.scala --> メイン.scala」にリファクタリングしてみた。「object Main --> object メイン」も修正してみる。

object メイン {
def main(引数: Array[String]) = {
println("こんにちは")
}
}
確かにこれでもうまくいく。日本語で、属性名、引数名、メゾッド名、クラス名、ファイル名を表現しても大丈夫そうだ。さすがに環境依存文字はやめた方がいいだろう。Scalaに限ったことではなく、Unicodeが扱えるJavaでは行えたことではあるが、型推論やgetter/setterの省略などができるScalaでは、より日本語コーディングに近づけるということだろう。

実は、日本語コーディングは、Lispの時代(1987年ころ)に結構やった覚えがある。26年前か。確か、JARGONと呼んでいたスクリプト言語だった。エキスパートシステムを構築するための言語で、InterLispで書かれていた。自然言語でルールを記述したいというニーズから開発された言語だった。「もし…ならば…」をやたらと記述していたな。ClassとInstance(確かFactと呼んでいた)の概念があり、属性をSlotと呼んでいた。Characteristicと呼ばれる論理属性を特化させたものもあった。Classからこれらの属性や関数を継承してInstanceを生成できた。最低限のオブジェクト指向であったはずだ。まだCLOSは登場していなかったし、オブジェクト指向という言葉があったのかどうかも定かではない。SmalltalkPrologの影響は受けていたはずだ。当時のコードは残っていないが、Scalaで日本語コーディングしたものが、非常に当時のJARGONを思い出させてくれた。

将来なりたいもの

子供が「将来なりたいもの」について話してくれた。夢のない将来なのだが、一応メモしておこう。

下の子(小3)は「大学に行ってお金を使うより、高校を出たら働きたい。会社で(オフィスで)パソコンを使うのは面倒なので、レストラン(おそらくウェイトレス)かスーパー(おそらくレジ係)で働きたい」と。

上の子(小5)は「市役所や区役所で働きたい。仕事が楽でお金もしっかりもらえそう」と。