Haskell / 数字4つで10を作れ (3)

前回からの続きです。

  • 4つの数と3つの四則演算(×÷+−)を組み合わせて、答えが10になるような計算式を作れ。
  • 数字、演算ともに重複あり。
  • 単一の負符号はなし。

Octocat GitHub hanepjiv/make10_hsでソースコードを公開しています。

テスト環境

順番が前後しますが、実装時にはテスト環境を構築して利用しました。

開発中はテストを行ないながら、少しづつ機能を拡張していきます。

doctest

コメント形式でテストケースを記述します。下記のように呼び出します。

Main.hs

※ 次のソースは配布ファイルには含まれていません

module Main ( main
     , myadd
     ) where

import Prelude
import qualified Test.DocTest as DocTest

main :: IO()
main = DocTest.doctest [ "-packageghc"
   , "Main.hs"
   ]

-- | myadd
-- >>> myadd 1 1
-- 2
--
-- >>> myadd 1 2
-- 3
--
myadd = (+)

上の例では、 doctest の引数に doctest を呼び出している Main.hs を指定しているので、 実行ファイル自身のドキュメントから、テストを作成しています。

実行すると次のようにテストが成功します。

cabal run
Preprocessing executable 'Sample00' for Sample00-0.1.0.0...
[1 of 1] Compiling Main     ( Main.hs, dist/build/Sample00/Sample00-tmp/Main.o )
Linking dist/build/Sample00/Sample00 ...
Running Sample00...
Examples: 2  Tried: 2  Errors: 0  Failures: 0

実際にはテスト対象がライブラリの場合等もありますので、 実行ファイルとテスト対象を分けて記述します。

test/doctests.hs
module Main where
-- =============================================================================
-- -----------------------------------------------------------------------------
import Prelude

import Control.Arrow ( Kleisli(..)
   , runKleisli
   , (>>>)
   )

import qualified Test.DocTest as DocTest
-- =============================================================================
-- -----------------------------------------------------------------------------
main :: IO ()
main = flip runKleisli () $
 (Kleisli $ const $ DocTest.doctest [ "-packageghc"
        , "-isrc"
        , "src/Game/Make10.hs"
        ])
 >>>
 (Kleisli $ const $ DocTest.doctest [ "-packageghc"
        , "-isrc"
        , "-iexample/make10"
        , "example/make10/Main.hs"
        ])

Arrow形式で記述していますが、ソースコード2つを指定しています。 最上位のソースを指定すれば、 import しているソースを全て収集してくれます。

hspec

テストフレームワーク。Ruby の rspec リスペクトだそうです。

  • test/Spec.hs
  • test/Game/Make/CellSpec.hs
  • test/Game/Make/ExpandSpec.hs
  • test/Game/Make/OperatorSpec.hs

‘<モジュール名>’ + ‘Spec.hs’ という名前のファイルを収集してテストケースを作成してくれます。

あまり深くは使用せず、QuickCheck を呼び出すジャンプ台として使用しました。

QuickCheck

ランダムテストジェネレーターです。

test/Game/Make10/OperatorSpec.hs
module Game.Make10.OperatorSpec ( spec
    ) where
-- =============================================================================
-- -----------------------------------------------------------------------------
import Prelude

import Test.Hspec
import Test.QuickCheck

import Game.Make10
-- =============================================================================
-- -----------------------------------------------------------------------------
spec :: Spec
spec = --do
  describe "Operator" $ do
    it "ADD" $ property $ \ x y -> function ADD  (x :: Rational) y == x + y
    it "SUB" $ property $ \ x y -> function SUB  (x :: Rational) y == x - y
    it "RSUB" $ property $ \ x y -> function RSUB (x :: Rational) y == y - x
    it "MUL" $ property $ \ x y -> function MUL  (x :: Rational) y == x * y
    it "DIV" $ property $ \ x y ->
 case y of
   0 -> True
   _ -> function DIV (x :: Rational) y == x / y
    it "RDIV"  $ property $ \ x y ->
 case x of
   0 -> True
   _ -> function RDIV (x :: Rational) y == y / x

上記では (x :: Rational) として、明示的に Rational(有理数) を指定しているのがポイントです。 function の型推定で y も Rational と推定してくれます。

property毎にランダムにx, yを複数(デフォルトでは100)パターン生成してテストしています。

test/Game/Make10/Arbitrary/Operator.hs
module Game.Make10.Arbitrary.Operator(Operator(..)) where
-- =============================================================================
-- -----------------------------------------------------------------------------
import Prelude

import Control.Applicative ( (<$>)
    )

import Test.QuickCheck

import qualified Game.Make10
-- =============================================================================
-- -----------------------------------------------------------------------------
newtype Operator = Operator { getBase :: Game.Make10.Operator }
     deriving (Show)
-- -----------------------------------------------------------------------------
instance Arbitrary Operator where
  arbitrary =  gen <$> arbitrary
    where
      gen :: Int -> Operator
      gen i = Operator $
       toEnum $ mod (abs i)
    (fromEnum (maxBound :: Game.Make10.Operator) + 1)

独自のデータ型をランダムに作成したい場合、Arbitrary クラスのインスタンスにします。

上記のように 独自型 (Game.Make10.Operator) から newtype で新規型 (Game.Make10.Arbitrary.Operator) を作成し、それを Arbitrary のインスタンスにしています。

新規型を作らずに、直接、独自型を Arbitrary のインスタンスにすることも出来ます。 しかし、型を定義しているファイルとは別のファイルでインスタンス化すると、「orphan instance」として、警告されてしまいます。

(arbitrary :: Game.Make10.Arbitrary.Operator) の実装は、 (arbitrary :: Int)で生成した乱数から、Game.Make10.Operator が Bounded, Enumを派生していることを利用して、ランダムな Game.Make10.Operator を生成しています。

test/Game/Make10/CellSpec.hs
module Game.Make10.CellSpec  ( spec
    ) where
-- =============================================================================
-- -----------------------------------------------------------------------------
import Prelude

import Test.Hspec
import Test.QuickCheck

import Game.Make10 hiding ( Operator
    )

import Game.Make10.Arbitrary.Operator
-- =============================================================================
-- -----------------------------------------------------------------------------
spec :: Spec
spec = --do
  describe "apply" $ do
    it "apply op Atom Atom" $ property $
      \ m n op ->
      case apply (getBase op) (Atom (m :: Rational)) (Atom n) of
 Right x -> x == function (getBase op) m n
 _ -> True

上のように、新規型から getBase で、独自型を取り出して使用します。

おわりに

Python と Haskell で同じ問題を解くことで言語機能を比較することが出来ました。

Haskell は、 よく言われるように「コンパイルを通すのがキツいが、通ればバグが潰せている」という特徴があります。

テストフレームワークと合わせると、「コード記述量に対する生産性は高い」と感じました。

ただ、嵌ってしまうと、時間はかかったような気がします。

また、今回はdo記法なしのスタイルを貫きましたが、多様なスタイルで記述できる分、他者のコードを読み解くのは相当に難易度が高いように思います。

ともあれ

「Haskell で make10 やってみました」ということで。