テクノロジー
やってみた

モナド変換子を使ってテストを便利に(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を呼び出しているだけです。コードを見ればなんとなく分かると思いますが、expectedactual の値を比較して、正しいかどうかをテストしています。ただし、このテストはわざと失敗するようにしました。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'を呼び出し、その後、状態であるTestResulttotalを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をやってみて欲しいです。

前の記事へ

次の記事へ

一覧へ戻る

「テクノロジー」カテゴリの最新記事

PAGE TOP