モナド変換子を使ってテストを便利に(PureScript)
こんにちは。Monaca開発チームの内藤です。
私は普段、プログラミングをする場合はJavaScriptや、JavaScriptに変換出来るAltJSを使うことが多いのですが、今回は、数あるAltJSの中の一つであるPureScriptについて取り上げてみたいと思います。
そもそもPureScriptとはどんな言語かというと、いわゆる関数型プログラミング言語の一種で、文法はHaskellに似ていて(というか、Haskellを参考に作られていて)、TypeScriptやScalaと同じく静的な型を持ち、型推論の機能を持つという特徴があります。名前にPureと付いているように、純粋(参照透明)であることも、この言語の大きな特徴です。また、全体的にHaskellに似ているものの、JSON(もしくはレコード)を取り扱いやすくなっているなど、細かい点では違いがあります。
最近のプログラミング言語では、TypeScriptやScala、Go言語など、静的な型を持つものが人気になっていますので、その意味では、PureScriptも今後、もっと注目されてくるのではないかと期待しています。
オフィシャルサイトはこちらです。 https://www.purescript.org/
私も実は、まだそこまで詳しいわけではなく、手探りでいろいろと試している状態になりますので、もしも不適切な部分がありましたらご容赦下さい。
今回は、ごく簡単なテストコードを実行してみて、これを改良するということをしてみます。
まずはプロジェクトの雛形の作り方ですが、今回は、TestApp
というプロジェクト名にして、次のように作成しましょう。spagoというのは、PureScript用のパッケージマネージャ(npmみたいなもの)です。
% mkdir TestApp
% cd TestApp
% npm init -y
% npm install spago purescript
% npx spago init
% npx spago install assert integers transformers
そして、あとで使いやすいように、package.jsonのscriptを次のようにしておきます。
"scripts": {
"build": "spago build",
"test": "spago test"
},
それでは、簡単なテストコードを書いていきましょう。test/Main.purs
を、次のようにします。
module Test.Main where
import Prelude
import Effect (Effect)
import Effect.Class.Console (log)
import Test.Assert as TA
sampleTest1 :: Effect Unit
sampleTest1 = do
log "--- sampleTest1 ---"
TA.assertEqual' "test1: 1 + 1 = 2"
{ expected: 2
, actual: 1 + 2
}
TA.assertEqual' "test2: 3 * 4 = 12"
{ expected: 12
, actual: 3 * 4
}
main :: Effect Unit
main = do
sampleTest1
ごく簡単に、sampleTest1を呼び出しているだけです。コードを見ればなんとなく分かると思いますが、expected
と actual
の値を比較して、正しいかどうかをテストしています。ただし、このテストはわざと失敗するようにしました。1 + 2 = 2 は成立しませんから、失敗するのは当然ですね。実行は
% npm run test
初回はビルドするのに少し時間がかかります。結果は、予想通りエラーとなり、次のような表示がされます。
--- sampleTest1 ---
test1: 1 + 1 = 2
Expected: 2
Actual: 3
エラーメッセージ、、、
では、上記のテストコードを直して、sampleTest1関数を次のようにしましょう。
sampleTest1 :: Effect Unit
sampleTest1 = do
log "--- sampleTest1 ---"
TA.assertEqual' "test1: 1 + 1 = 2"
{ expected: 2
, actual: 1 + 1
}
TA.assertEqual' "test2: 3 * 4 = 12"
{ expected: 12
, actual: 3 * 4
}
さて、これで再度実行 npm run test
してみると
--- sampleTest1 ---
[info] Tests succeeded.
はい、無事に成功しまし た!!
しかし、、、これをみると、確かに成功はしていることは分かるのですが、ちょっとそっけないですよね。これだけだと、いくつのテスト(assertEqual'
)に成功したのか分からなくて、かなり不安にならないでしょうか? できれば、せめていくつテストが通ったか簡単にわかる方がいいのではないでしょうか?
それで、これを解決するために、今回はモナド変換子を用いて、assetEqual'
が何回実行されたのかを数える機能を追加していきたいと思います。
PureScriptの特徴的なところは、テスト部分はほとんど変更せずに、あくまでテストコードを書いているのに、その裏では assertEqual'
が何回行われたかをカウントするといった機能を実装することが出来ることです。これは、Haskellと同じく「行間をプログラミング出来る」というか、行間で引き渡させる情報(コンテキスト)をState
モナドやMaybe
モナドなどによりコントロール出来るという特徴によって実現されています。少し厄介なのは、テスト中にログを表示するなどの副作用も取り扱いたいので、そのためにはEffect
というモナドも利用する必要があるということです。
これを解決するために、StateT
モナドというState
モナドの変換子を使います。(この仕組みはHaskellと同じなので、Haskellに詳しい方であればお馴染みの方法です)
まず、最初に最終的に解決したコードを記載します。
module Test.Main where
import Prelude
import Effect (Effect)
import Effect.Class.Console (log)
import Test.Assert as TA
import Data.Int (decimal, toStringAs)
import Control.Monad.State.Trans (StateT, get, lift, modify_, runStateT)
import Data.Tuple (Tuple(..))
newtype TestResult = TestResult {
total :: Int
}
instance showTestResult :: Show TestResult where
show (TestResult obj) =
">>> total: " <> toStringAs decimal obj.total
type TestEffect = StateT TestResult Effect Unit
assertEqual' :: forall a. Eq a => Show a => String -> { actual :: a, expected :: a } -> TestEffect
assertEqual' title test = do
lift $ do
log title
TA.assertEqual' title test
modify_ (\(TestResult obj) -> TestResult $ obj { total = obj.total + 1 })
pure unit
doTest :: TestEffect -> Effect Unit
doTest testCode = do
(Tuple result state) <- runStateT
( do
testCode
testResult <- get
pure testResult
) $ TestResult { total: 0 }
log $ show result
pure unit
sampleTest1 :: TestEffect
sampleTest1 = do
lift $ log "--- sampleTest1 ---"
assertEqual' "test1: 1 + 1 = 2"
{ expected: 2
, actual: 1 + 1
}
assertEqual' "test2: 3 * 4 = 12"
{ expected: 12
, actual: 3 * 4
}
main :: Effect Unit
main = do
log "start test"
doTest sampleTest1
個別に説明します。
まず、assertEqual'
した回数を保存するオブジェクト(レコード)として
newtype TestResult = TestResult {
total :: Int
}
を定義しておきます。
次に、これをShowクラスのインスタンスにします。
instance showTestResult :: Show TestResult where
show (TestResult obj) =
">>> total: " <> toStringAs decimal obj.total
そして、あとで使いやすいように、新しい型を定義しておきます。
type TestEffect = StateT TestResult Effect Unit
この、StateT
というのは先にも説明したように、State
モナドの変換子で、型(Type
)、型を受け取って型を返す種(Type->Type
)、型(Type
)の3つを受け取り、新しい型を返す型コンストラクタです。
今回の場合、TestResult
という状態を持ち、副作用であるEffect
もリフトして使う出来るような型がTestEffect
になります。
そして、既存のTA.assertEqual'
をラップして、新しいassertEqual'
を定義します。
assertEqual' :: forall a. Eq a => Show a => String -> { actual :: a, expected :: a } -> TestEffect
assertEqual' title test = do
lift $ do
log title
TA.assertEqual' title test
modify_ (\(TestResult obj) -> TestResult $ obj { total = obj.total + 1 })
pure unit
これは、lift
することでEffect
が使えるので、その中で「テストタイトルの表示」をしてから、オリジナルのTA.assertEqual'
を呼び出し、その後、状態であるTestResult
のtotal
を1つ増やしています。
PureScriptでは、変数は再代入出来ないので 、modify_
関数を使って新しい値を再作成し、更新しています。React Hooksで、useState
で更新用の関数を使って値を更新するのに似ていますね。
また、testSample1
を呼び出すために、次のdoTest
関数を定義しておきます。これは、TestEffect
型 で保持していた計算結果を出力し、そして、それを破棄して Effect Unit
型に戻して終わります。
doTest :: TestEffect -> Effect Unit
doTest testCode = do
(Tuple result state) <- runStateT
( do
testCode
testResult <- get
pure testResult
) $ TestResult { total: 0 }
log $ show result
pure unit
また、実際のテストコードであるsampleTest1
は、もともとのsampleTest1
とほぼ同じですが、戻り型がEffect Unit
からTestEffect
に変更されています。内部で呼び出しているTA.assertEqual’
も、今回定義したassertEqual'
に変更されています。
sampleTest1 :: TestEffect
sampleTest1 = do
lift $ log "--- sampleTest1 ---"
assertEqual' "test1: 1 + 1 = 2"
{ expected: 2
, actual: 1 + 1
}
assertEqual' "test2: 3 * 4 = 12"
{ expected: 12
, actual: 3 * 4
}
最後に、メイン関数ですが、doTest
経由でsampleTest1
を呼び出しています。
main :: Effect Unit
main = do
log "start test"
doTest sampleTest1
これで完成です。呼び出してみましょう。
start test
--- sampleTest1 ---
test1: 1 + 1 = 2
test2: 3 * 4 = 12
>>> total: 2
[info] Tests succeeded.
正しく、total: 2
と表示されるようになりました。
ごく簡単なサンプルになりますが、PureScriptでモナド変換子を使ってプログラミングをしてみました。
私自身、まだ関数型プログラミングには慣れていないので、関数型プログラミング言語としてよりは、便利な命令型プログラミング言語として使っている感じですが、とても面白いと思っています。
やはり、プログラミング言語は、ユーザーが多い方がエコシステムが充実してどんどん発展していくと思うので、もし興味をもたれたら、ぜひ、PureScriptをやってみて欲しいです。