テストという存在の型について考えてみる
いわゆるユニットテストフレームワークに現れる各種型について考えてみる。
成果は考えない。
このエントリは考えがまとまっていない段階で書かれたものであるため、間違っている可能性があります。
成果物(の途中経過)
途中結果を素っ飛ばした作成中のものは下記に公開している。
https://github.com/pocketberserker/dog/tree/d0ad112e00c72500a2ac5df764b7109249e684de
ドッグフーディングしたいのでこの名前になっただけである*1。
この記事では Scala with Scalaz と 一部 FSharp(というか Persimmon )を使って考察する。
テストケースは Kleisli
テストケースは入力値、実行事前条件、期待値、実行事後条件から構成されるはずだ。 このうち入力値、実行事前条件、実行事後条件は型の内部に隠せるものなので、たぶんよくあるテストケースはこんな感じになる。
trait TestCase { def run(): TestResult = { ??? } }
しかし、テスト結果を合成したい場合にはこれは圧倒的に情報が足りないので、テスト結果に値を持てるようにしたほうがよさそうである。
trait TestCase[A] { def run(): TestResult[A] = { ??? } } // 型コンストラクタはおいおい trait TestResult[A]
さて、これでTestCase#run
は 引数なしで実行できTestResult[A]
を返す操作が行える存在になった。
ここでちょっと考えると、引数なしというのは Unit
を引数にとる、と考えられなくもない。
そうすると都合の良いことに、Kleisli
の構造に一致する。
import scalaz._ type TestCase[A] = Kleisli[TestResult, Unit, A] // ちょっと不恰好だが、以下のように実行できる // testcase.run(())
テスト結果はMonadにできなくもない
テスト結果は多くの場合、成功か失敗かで表されるはずだ。
case class Success[A](value: A) extends TestResult[A] case class Failure[A](reason: String) extends TestResult[A]
しかし、これは色々と大雑把な構造になっている。
- エラーが考慮されていない
- 失敗時の情報が蓄積できない
- "失敗"なのか"テストをスキップ(無視)した"のか判断がつかない
問題1を解決するためにエラーを持てるようにする。
case class Done[A](result: String \/ A) extends TestResult[A] case class Error[A](e: Throwable) extends TestResult[A]
テスト実行完了か、エラーが発生したことが確認できるようになった。 この時点では明らかにMonadである。
問題2について解決を試みる。
case class Done[A](results: ValidationNel[String, A]) extends TestResult[A] case class Error[A](e: Throwable) extends TestResult[A]
成功時に保持すべき値は最終結果ひとつで良いのに対し、失敗情報は多数保持すべきなのでValidationNel
がちょうどよさそうだ。
ただし、実装次第ではMonad則を満たさなくなりそうである。
問題3が残っている。
これはValidationNel
だけでは解決できそうにないので、別の型を導入する。
trait Cause case class Violated(reason: String) extends Cause case class Skipped(reason: String) extends Cause trait AssertionResult[A] case class Passed[A](value: A) extends AssertionResult[A] case class NotPassed[A](cause: Cause) extends AssertionResult[A] type AssertionNel[A] = NonEmptyList[AssertionResult[A]] case class Done[A](results: AssertionNel[A]) extends TestResult[A] case class Error[A](e: Throwable) extends TestResult[A]
Done#results
に入る値を特定のものに絞り(ありえない状態を除外する、といったほうが正しい)、それ以外の入力は受け付けないようにすればTestResult[A]
はMonad則を満たす。
しかしその実装はMonadと呼べるのかというのは、疑問が尽きない。
Persimmon の TestResult は Monad ではない
今回はエラーだった場合はそれ以降何もしないことにしたので、Monadになる可能性があった。 しかし、実行できるアサーションはすべて実行してしまえ、という方針にするとMonad則を満たせなくなる。
Persimmon はそういう方針なので、おそらくMonad則を満たさない。
AssertionNel ~> TestResult
は実装できるか
これに関しては考え方次第である。
- テストケース名や入力値をテスト結果に持たせたいなら、無理
- 変換時は空のデータを持たせ、Runnerで後から差し替えるならできそう
検証が足りていないのでもう少し考慮すべき項目があるかもしれない。
Persimmonに関していえば、変換できなくもない。 テスト名が空な場合にはリフレクションで変数(関数)名前を取得して入れ替える機能が備わっているためだ。
Assertion[A]
は必要か
以下のような型を定義できる。
type Assertion[A] = Kleisli[AssertionResult, Unit, A]
しかし必要性を感じない。
基本的にさっさと実行されてしまうため、機能過多に思える。
これが何かに使えるか考えるなら、AssertionResult[A]
の機能拡充を考えたほうがよさそうである。
TestResult[A] は Applicative であるべきか
現時点のTestResult[A]
は入力値を制御しているためMonad則を満たすが、Applicativeにとどめておくべきかどうかは悩ましい問題だ。
Scalaのfor式は F# のコンピュテーション式に比べて表現に不安を感じるし、テスト失敗情報を蓄積させるならMonadにすべきではないのはその通りだ。 しかし、ScalaだとApplicativeで合成するのはそれはそれで面倒くさい作業にしかならない気もしている。
書きやすいDSLを実装する、という点ではScalaではマクロを使うことも考慮すべきなのだろうが…さてはて。
まとまらない
- FreeやFreeApを使って何かできないか考えていたらいつのまにかこういうものに行き着いた、なぜ
- Operational monad などの利用を考えると、やはりHaskellのライブラリを移植したほうが早いのだろうか…
- とはいえ、Scalazの各種型で色々定義できそうということがわかっただけでも面白かったので良しとする
*1:猫の双対でco猫とかにしてみたかったけどどう見ても名前負け思想だったのでやめた
"なぜshapelessにはGeneric[Boolean]がないの?"と尋ねてみた
Gitterで質問したのでそのメモ*1。 なお私の英語は壊滅的なので片言?なのは許してください。
Hello. Why does Generic[Boolean] instance not exist?
と投稿したところ、milessabinさん(shapeless作者)から
Because there's no obvious sum of product representation of Boolean. I guess you would want false.type :+: true.type :+: CNil?
と返答があった。なるほど。
@milessabin Thank you. I understand. I try to port from Haskell code(using GHC.Generic) to Scala code. GHC.Generics derive some instances of Generic a.
とりあえずこういうことしようとしたんだよーと言ったところ
@pocketberserker gotcha. The idiom here is to hand-write instances for primitives and use Generic for ADTs. I would be very interested to see a side-by-side comparison of something written with GHC generics and something written with shapeless. Would you be able to blog about it?
ほえーと思ってたらなんか後日記事かくことになった。
まぁ、最初から無理っていうのもなんだし、やるだけやってみよう…。
*1:という名のバックアップ
HaskellのlogictをScalaに移植してみた
Haskell に logict というライブラリがあります。
logict: A backtracking logic-programming monad. | Hackage
MonadLogicやLogicTが何者なのかはそのうち解説記事を書くとして、今回はこのライブラリをScalaに移植してみました。
pocketberserker/scala-logic · GitHub
そのうちファーストリリースを出す予定です。
このライブラリを作ったのは、smallcheckをscalaで実装してみたいと思ってコードをみたらLogicTが必要だとわかったためです。
Haskellのlogictとの違い
若干ですが差異があります。
- 型パラメータの順序
- 引数の順序
- logic関数は未実装(RankNTypesに阻まれた)
- いくつか制約を緩めている
制約を緩めているというのは、例えばMonadPlusではなくApplicativePlusでよい、とかですね。
これに関しては私もpull requestをもらうまで気がついていなかったのですが、確かに緩められるものは緩めてもよいですね。
functional dependenciesについて
LogicTのMonadReaderインスタンス実装は@xuwei_kさん、@halcat0x15aさん、@gakuzzzzさんに手伝ってもらいました、ありがとうございます*1。
functional dependenciesをScalaで実現できるのか自体知らなかったので、とても助かりました。 みなさんScalaの函数型なことでわからないことがあったら以下のgitter部屋に突撃するといいと思います。
なぜScalaz?
某猫やkitsの利用も考えたのですが、まだリリースされていなかったり今回の実装に必要そうな機能がなかったりしたので見送りました。 あとはまぁ、Scalazを長らく触っていなかったのでリハビリしたかったとか(某本もでることですし)。
Scalazは長い間開発が続いているだけあって機能が充実していて良いですね。 そういう意味では当分使われ続けるのではないでしょうか。
まとめ
小さいライブラリですがscalazの機能を色々と使っているので、ちょっとした勉強にはなるかもしれません。
*1:よく考えたら全員去年のScalaz勉強会の発表者だ
AppVeyorのSQL ServerにJDBC Driverで接続する
備忘録です。 簡単な経緯は以下の通り。
あと体力あれば URL これ使って scalikejdbc の テストできるか試してみたい
@pocketberserker やってみたんですけど URL テストの実行までいったけれど、(設定間違ってるのか)SQL Server繋げられてないです
2015-02-13 21:10:03 via Twitter Web Client to @pocketberserker
rpscala でそういう話題があったのに関連して、 AppVeyor で頑張れないかを xuwei さんのコードを引き継いで試してみました。
参考資料
- SQL Server 2012 ExpressにJava(JDBC)で外部から接続する - Symfoware
- configuration - Enable tcp\ip remote connections to sql server express already installed database with code or script(query) - Stack Overflow
だいたいこの辺りを情報を AppVeyor で実行できるようにしていきます。
設定やスクリプト
説明が面倒なので結論だけはります。 設定値やプロジェクト名もそのままになっているので、適宜読み替えてください。 また、今回の内容と直接関係のない部分は削っています。
あと、sbtはリポジトリに同梱しているパターンで試しているので、同梱したくない場合はsbtをダウンロードする必要もあります。
# appveyor.yml services: mssql2014 build_script: - netsh advfirewall firewall add rule name="jdbc driver for sql server udp" dir=in action=allow protocol=UDP localport=1434 - sc config sqlbrowser start= auto - net start sqlbrowser - ps: Set-ExecutionPolicy RemoteSigned - ps: .\Setting-SqlServer.ps1 - net stop "SQL Server (SQL2014)" - net start "SQL Server (SQL2014)" - netsh advfirewall firewall add rule name="jdbc driver for sql server tcp" dir=in action=allow protocol=TCP localport=1433 - sqlcmd -S ".\SQL2014" -U sa -P Password12! -Q "create database scalikejdbc;" - del scalikejdbc-core\src\test\resources\jdbc.properties - move scalikejdbc-core\src\test\resources\jdbc_sqlserver.properties scalikejdbc-core\src\test\resources\jdbc.properties - ps: wget http://download.microsoft.com/download/0/2/A/02AAE597-3865-456C-AE7F-613F99F850A8/sqljdbc_4.1.5605.100_enu.tar.gz -OutFile "sqljdbc.tar.gz" - 7z x -y -oC:\projects\scalikejdbc\ sqljdbc.tar.gz - 7z x -y -oC:\projects\scalikejdbc\ sqljdbc.tar - ps: mkdir .\scalikejdbc-core\lib - ps: cp .\sqljdbc_4.1\enu\sqljdbc41.jar .\scalikejdbc-core\lib\
// Setting-SqlServer.ps1 // バージョンに合わせて ComputerManagementの値を差し替える // SQL Server 2008 -> ComputerManagement10 // SQL Server 2012 -> ComputerManagement11 Get-WmiObject -Namespace root\Microsoft\SqlServer\ComputerManagement12 -Class ServerNetworkProtocol | Where-Object {$_.InstanceName -eq 'SQL2014' -and $_.ProtocolName -eq 'Tcp'} | ForEach-Object {$_.SetEnable()} $tcpProperties = Get-WmiObject -Namespace root\Microsoft\SqlServer\ComputerManagement12 -Class ServerNetworkProtocolProperty | Where-Object {$_.InstanceName -eq 'SQL2014' -and $_.ProtocolName -eq 'Tcp' -and $_.IPAddressName -eq 'IPAll'} foreach( $tcpProperty in $tcpProperties ){ $requestedValue = "" if($tcpProperty.PropertyName -eq "TcpPort"){ $requestedValue = "1433" } $tcpProperty.SetStringValue($requestedValue) }
SQL Server 2014 Espressで試していますが、たぶん SQL Server 2008あたりまでは普通に動きそうな気がします。
重要なところ?
SQLServer用のjarを同梱するのはライセンス的につらい感じがしたので、PowerShell 3.0から使える wget
エイリアスコマンドを使ってダウンロードし、AppVeyor にインストールされている 7Zip で解凍するのがミソです。
あと、各種設定をして再起動する前にデータベースを作成してもログイン権限がなくてテーブルにアクセスできないかも?
まとめ
今回はsbtで作業しましたが、gradleでもできるのではないでしょうか。
懸念事項は、AppVeyorの実行速度が遅いことと、mysqlやpotgresql前提で書かれたSQLがSQL Serverで通らないことがまれによくあることです。
scalacheckの内部構造をいじって遊んでみた
SPECIAL DAY Scala Hack-a-son 143th in KABUKIZA - Scala Meetup Group in Tokyo | Doorkeeper
たまにはハッカソンもいいかなと思って参加してみました(噂の rpscala に参加してみたかったというのもある)。
で、当日は scalacheck をいじっていたコードがようやくテストに通るようになったので書き残しておきます。
注意事項
この記事のコードは pull request を送るつもりはないので、そのことを認識して読んでください。
今のところ scalacheck を fork して別プロジェクトとして育てるつもりもないです。
やりたかったこと
基本方針は "いらないものを消す" です。
- scalajs の クロスコンパイル設定を有効にする
- Gen で
Option[T]
ではなくT
を返すようにする - 内部表現
R
trait を削除する - scalaz で置き換えられそうなコードを置き換える
- ランダムを rng か spire/random に差し替える
現時点でできたのは R
を消すまでです。
クロスコンパイル設定
https://github.com/pocketberserker/scalacheck/commit/c7c47204c7245d1a9830aca79b8550b9e63052fc
2015/02/12 時点の scalacheck の master ブランチは Windows 上ではそのままだとビルドできません。 クロスコンパイルのためのコード共有を、Windows で認識できないシンボリックリンクで行っているためです*1。
対処法は sbt-scalajs の要求にあわせてディレクトリ構成変えたり設定変更した程度です。
これに関してはハッカソンの数日前に作業してました。
Gen で T を返す & R trait を削除する
https://github.com/pocketberserker/scalacheck/commit/6d03207ceee9caf6a9e7ad2cb518c532372bd826
quickcheck 系でデータの生成に失敗することはそうそうないので、 Option を返さないようにしました。
…が、ここで suchThat
メソッドの実装でスタックオーバーフローしたり無限ループに陥ったりして、全然進まず。
結局当日の進捗は駄目でした。
後日、よく考えたら一部実装の変更に伴い suchThat
を呼び出すべきではない部分を発見したので、これを削除したら動くようになりました。
R
trait は Gen 用のラベルや sieve を持っているのですが、これもいらないだろうということで削除。
リファクタリングみたいなこと
https://github.com/pocketberserker/scalacheck/commit/e923d97a2ddfeb9b158bcb9524ce4ca844b7eb32
scala.util.Random から別のものに差し替えたいな、と思って依存関係だけ設定したのですが、作業できずじまい…。
Arbitrary 削除断念
CoArbitrary を自前実装してしまったので、消すのも変だなと思い残してます。
その他
値が生成できるまでがんばってしまうので、多少実行速度が劣化したような感覚… filter
や suchThat
のご利用は計画的に。
まとめ
rpscala は懇親会も含め楽しかったです。
CoArbitraryの実装比較
全国 QuickCheck 系ユーザの皆様こんにちは、もみあげです。
今日は
CoArbitraryとScalacheck - scalaとか・・・
に関連して、各言語の QuickCheck 実装の CoArbitrary
や Arbitrary (a -> b)
インスタンスについてみていきましょう。
CoArbitrary とは
QucikCheck のドキュメントによると、関数値の生成に使われる型クラスらしいです。
-- Haskell での定義 class CoArbitrary a where coarbitrary :: a -> Gen b -> Gen b
値と Gen
から新たな Gen
を生成できます。
第2引数の Gen
が必要なのは、最終的な値を生成するための Gen
を作るために、乱数生成器が必要だからだと思ってます。
CoArbitrary
のインスタンスを作るときには、 variant
という関数を用いることが多いです。
-- Haskell での定義 Integral n => n -> Gen a -> Gen a
この関数は Gen
の seed を変更するものです。
詳しく知りたい場合はソースコードを読みましょう。
Arbitrary (a -> b)
では、僕らの欲したインスタンスを見てみましょう。
-- Haskell での定義 instance (CoArbitrary a, Arbitrary b) => Arbitrary (a -> b) where arbitrary = promote (`coarbitrary` arbitrary)
promote
関数という関数は
http://hackage.haskell.org/package/QuickCheck-2.7.6/docs/Test-QuickCheck-Gen-Unsafe.html
Monad
に包まれた Gen
を Monad
な値を生成する Gen
にあげる関数です。
lift
と思ってもよさそうに見えますね。
promote
に適用する関数値の型は a -> Gen b
であり、 関数も Monad
なので条件を満たしています。
流れをまとめると
- 適用された値をから戻り値を生成するジェネレータの seed を変更する関数を作る
promote
で lifting させる- あたかも関数を生成したようにみせかけて property に渡す
- 関数適用したら
Gen
から戻り値がランダムに生成される
といったところでしょうか。
なお、一連の情報からこのインスタンスから得られる関数値の適用結果を使って何かテストするのは避けるべきということが伝わってくる気がします。
実装比較
てきとーに解説を済ませたところで、実装をみていきましょう。
Functional Java
https://github.com/functionaljava/functionaljava/blob/v4.2/core/src/main/java/fj/test/Gen.java#L637
promote
は Monad ではなく、1引数関数クラス F
が生成される Gen
を返します。
purescript-quickcheck
https://github.com/purescript/purescript-quickcheck/blob/v0.4.0/src/Test/QuickCheck.purs#L25-L26
promote
が repeatable
という名称なのと、 Monad
ではなく関数が対象です。
idris-quickcheck
Arbitrary
がほしいなら双対も一緒に定義しろ、という潔さです。
SwiftCheck
promote
は Monad
でも関数でもなく Rose
が対象です。
Persimmon.Dried
今日*1実装しました。
https://github.com/persimmon-projects/Persimmon.Dried/pull/2
たぶん動いています。
Gen.Apply
が 'T option
を返すので、こいつをなんとか 'T
にする苦肉の策として こんな感じにごまかしてます。
FsCheck
考え方はかわらないけど、実装が根本から異なるので気にしない方向で。
まとめ
CoArbitrary
の考え方は共通している- Idris のみ
Arbitrary
でcoarbitrary
の実装を要求する variant
関数はシグネチャは同じだが実装はばらばら(乱数生成器に依存する?)promote
はMonad
か関数かRose
が対象- 実装はそんなに難しくない?
だいたい似たような実装になっていますが、Gen
の構造が他と異なる Persimmon.Dried が若干強引な印象です。
実際、Persimmon.Dried の移植元である scalacheck でも以下の issue で同様の指摘がなされています。
https://github.com/rickynils/scalacheck/issues/136
scalacheck の動向に注視したいところですね。
それはそれとして、Functional Java やばい(褒め言葉)。
*1:ブログ投稿日 or pull request マージ日参照
sonatypeのアカウントを作成してscodec-msgpackをpublishした
数日前(?)に xuwei さんが ,msgpack4z というライブラリをリリースしたことが(少なくとも私の中では)話題です。
msgpack4zというmsgpackのScala用ライブラリを作った - scalaとか・・・
こちらの開発が続くだろうし、私が作った scodec-msgpack はリリースせずお蔵入りしようかとも考えましたが
@pocketberserker もしかしたらパフォーマンス比較などをしたくなるかもしれない?ので、(一旦現状でリリースしてその後ある程度放置でもいいので)リリースしてみてもいい気はします
2015-01-06 10:17:58 via Twitter Web Client to @pocketberserker
これは確かにその通りだと思ったのと、
- msgpack4z は Scala 2.11 のみ、scodec-msgpack は 2.10 もサポート
- scodec-core が次バージョンから scalaz 非依存にするかも? https://github.com/scodec/scodec/pull/39
という状況もあるので、一旦このタイミングでリリースしておくことにしました。
リリースのための準備(アカウント作成等)
で、リリースのために sonatype アカウントを作成してビルド設定をいじりました。
- 自分が作ったScalaライブラリをMaven Centralリポジトリに登録する方法... | PAB@やっぱり求職中なんだよなあ…
- sbt-sonatypeとsbt-release pluginを組み合わせて使うサンプル - scalaとか・・・
この2つの記事が非常に参考になりましたというか、ほぼそのままです。 ちなみに Windows(Surface Pro 3) で msysgit な環境で作業しました。
確かに、アカウント登録は面倒ですけど、それ以降は楽でしたね。 sbt-sonatype, sbt-release パワーでしょうか。 そういえば主要リポジトリを見ていると最近は sbt-buildinfo プラグインも使うプロジェクトが多いみたいですね。
scodec-msgpack の今後?
xuwei さんの記事の
- データ型それぞれ作るの面倒
- 中間データ発生して効率よくなさそう(計測したわけではない)
この記事はわりと同じ感覚です。 これだけの型を作るのは面倒ですし、パターンマッチとかで処理が増えてしまうので…。
とはいえ、純粋関数型的アプローチでどこまでパフォーマンスを得られるかは気になるので、対応できる部分があれば修正していきたいとは思っています。
(scodecが早くなればタダ乗りできますし)
あと記事と全然関係ないけど各種msgpackライブラリで任意回データを投げ合ってvalidかどうか(いつの仕様ならvalidか)一覧にするアプリ作ってみたくなってきた。