6
1

More than 3 years have passed since last update.

Haskellのassertを文っぽく使う

Posted at

基本的な使い方

プログラムの潜在的なバグを発見したい時にassertは便利です。Haskell (GHC) にもControl.Exceptionモジュールにassert関数が用意されています。筆者が以前書いた記事

でもちょろっと紹介しました。

Control.Exceptionのassert関数は

assert :: Bool -> a -> a

という型を持ち、第1引数にチェックしたい条件を指定します。最適化が有効な場合(または -fignore-asserts が指定された場合)にはこの関数は第1引数を評価せずに第2引数をそのまま返し、最適化が無効な場合は第1引数が True であることを確認した上で第2引数をそのまま返す、という挙動をします。

他のプログラミング言語ではassertは文として用意されていることが多いかと思いますが、このassertは式として使います。また、assertを含む式が評価されない場合はassertの検査は走りません。

典型的な使い方は

succInt :: Int -> Int
succInt x = assert (x < maxBound) (x + 1)

という風に、関数の返り値の部分に組み込んで使う、という感じになります。

関数の入力をチェックしたい場合は

succInt :: Int -> Int
succInt x | assert (x < maxBound) False = undefined
          | otherwise = x + 1

という風にガード節に組み込んでも良いかもしれません。

letの途中に埋め込みたい

Haskellで純粋な関数を書く時に、let式でたくさんの変数を定義することがあるかと思います。先に行われる(べき)計算を上に、後に行われる(べき)計算を下に書けば、変数への再代入ができないという点を除いて手続き型と似たような雰囲気で書けます。

foo a b c = let x = a + 1
                y = sum [ a^i | i <- [1..b] ]
                z = bar x y
            in baz x y z

さて、letで定義した変数が特定の条件を満たすことを検査するにはどうすれば良いでしょうか?「典型的な使い方」に則るなら、 in の後にassertを挟むことになります。

foo a b c = let x = a + 1
                y = sum [ a^i | i <- [1..b]]
                z = bar x y
            in assert (x < maxBound) $ assert (y >= 0) $ baz x y z

しかし、この書き方だと変数の定義とassertの位置が離れてしまいます。この例だとletの部分が短いので可読性にはそれほど問題はないかもしれませんが、変数の個数がもっと多かったり、個々の定義が複数行にまたがっている場合を想像してください。できれば、変数の定義の直後にassertを書きたいものです。

こんな時はBangPatternとの組み合わせが使えます。正格な束縛を適当に用意してやって、その右辺をassertを含む式にすれば良いのです。assertの値はなんでも良いのですが、unit () を使うのが素直でしょう。

{-# LANGUAGE BangPatterns #-}

foo a b c = let x = a + 1
                !_ = assert (x < maxBound) ()
                y = sum [ a^i | i <- [1..b]]
                !_ = assert (y >= 0) ()
                z = bar x y
            in baz x y z

その辺の手続き型言語の記述に比べると !_ =() が邪魔ですが、このくらいは我慢しましょう。

do式の中に埋め込みたい

Haskellではdo式を使うと手続き型っぽい記述ができます。do式の「文」としてassertを使いたいと思うのは自然です。この際、

import Prelude hiding (pi)
import Control.Exception

-- ダメな例
main = do { pure (assert (pi > 3.05) ())
          ; putStrLn "pi > 3.05"
          }

pi = 3

と書いたのでは条件はチェックされません。

アクションをassertに包む書き方

import Prelude hiding (pi)
import Control.Exception

-- 微妙な例
main = do { assert (pi > 3.05) (pure ())
          ; putStrLn "pi > 3.05"
          }

pi = 3

は、IOなどの正格なモナド(アクションが正格に評価されるモナド)ではうまく行きますが、非正格なモナド(アクションの評価が非正格なモナド)ではうまくいきません。

例えば、Identityモナドを使った以下のコードではassertは評価されません。

import Prelude hiding (pi)
import Data.Functor.Identity
import Control.Exception

message :: Identity String
message = do { assert (pi > 3.05) (pure ())
             ; pure "pi > 3.05"
             }

main = putStrLn $ runIdentity message

pi = 3

この場合にも「letの途中に埋め込みたい」と同じ手法が使えて、BangPatternsを有効にして次のように書けばモナドの種類に依存せずにassertを実行することができます。

do { let !_ = assert (pi > 3.05) ()
   ; pure "pi > 3.05"
   }

あるいは、追加の pure が必要になりますが、 <- でも同じことができます。

do { !_ <- assert (pi > 3.05) (pure ())
   ; pure "pi > 3.05"
   }

(この場合、assertが除去された後のコードは pure () >>= \ !_ -> ... となります。モナドが具体的であれば最適化によって pure () >>= \ !_ -> が除去される可能性はありますが、抽象的な(型変数 m で参照されるような)モナドの場合は無駄なコードが実行される可能性があります。多分。)

おまけ:letの脱糖とパターンマッチ

letを使う際にBangPatternsを省略することはできません。たとえ = の左辺を () のような値コンストラクターとしても同様です。

main = let _ = assert (pi > 3.05) () -- ダメ
           () = assert (pi > 3.05) () -- ダメ
       in putStrLn "Hello"

これは、Haskellのlet式をcaseへ脱糖する際にirrefutable patternが使用されるためです。上記のコードをcaseを使って脱糖すると次のようになります:

main = case assert (pi > 3.05) of
         ~_ -> -- ワイルドカードなので、 ~ の有無にかかわらず、常に成功する(値は評価されない)
           case assert (pi > 3.05) () of
             ~() -> -- ~ がなければ値が評価されるが、 ~ があるので値は評価されずに常に成功する
               putStrLn "Hello"

したがって、letを使ってこの記事のテクニックを使用するには、BangPatternsが必須となります(あるいは、Strict拡張を有効にすれば明示的に ! を書く必要がなくなります)。

ちなみに、doのbindを使う場合は、パターンの部分をワイルドカード _ ではなく具体的な値コンストラクターとすればマッチが行われるようになります。

do { () <- assert (pi > 3.05) (pure ()) -- きちんと評価される
   ; pure "pi > 3.05"
   }

余談

Control.Exceptionのassert関数は割と貧弱(例えば、「条件」を表す文字列がエラーメッセージ中に含まれない。せいぜい、assert位置の行番号と関数名だけ)です。Hackage等を探せば「もっとリッチなassert」が見つかるかもしれません。「もっとリッチなassert」がControl.Exceptionと同様に「式」に対して使うことを想定している場合は、この記事のテクニックを使って文っぽく使うことができるかと思います。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1