【Haskell】データコンストラクタにバリデーションを設ける方法
HaskellでCLIツールを作っているときに, 本記事のタイトルにあるようなことを疑問に思いました.
例えばサイコロを表すデータ型を次のように定義したとしましょう(diceの単数形はdieであることに注意). ここではDie
モジュールの中でDie
型を定義しています.
module Die where data Die = Die Int
しかしこれだと, 以下のような値も構築することができてしまいますね.
die_0 = Die 7 die_1 = Die 0 die_2 = Die (-2)
これではサイコロの目が1から6までであるという前提でコードを書くことができなくなってしまいます. 外部からこのモジュールを利用したい場合なんかは特に困りますね.
こんな状況で使えるのが, 「smart constructor」というパターンです.
smart constructor
早速コードの全体をお見せしましょう.
module Die ( Die , makeDie ) where data Die = Die Int makeDie :: Int -> Maybe Die makeDie n | 1 <= n && n <= 6 = Just $ Die n | otherwise = Nothing
ポイントは2つです.
makeDie
というバリデーション関数を定義する- データコンストラクタ
Die
はモジュール外に提供しない
特に2つめが重要です. いくらmakeDie
という関数で, 不正な数値によるDie
型の構築を排除できても, データコンストラクタDie
が使えてしまうのでは意味がありません.
上のコードでなぜデータコンストラクタDie
が外部から使えないようになっているのかを補足しておきます. もしデータコンストラクタDie
も外部へ提供する場合は以下のように書きます.
module Die where -- もしくは module Die ( Die(..) , makeDie ) where -- もしくは module Die ( Die(Die) , makeDie ) where
pattern synonyms
さてsmart constructorというパターンを使うことにより, 不正な引数で値を構築されることを防ぐことができるようになりました. しかしひとつ困ったことがあります. それは, データコンストラクタが提供されていないため, パターンマッチが使えないということです.
module Foo where import Die func :: Die -> Bar func Die n = -- このような書き方ができない
これでは不便ですから, smart constructorの恩恵を得ながらもパターンマッチが使えるようにしましょう. それを実現するのがPatternSynonymsという言語拡張です. ではこちらもコードの全体をお見せしてしまいましょう.
-- 言語拡張の使用を宣言 {-# LANGUAGE PatternSynonyms #-} module Die ( Die , makeDie , pattern Die ) where -- データコンストラクタにアンダースコアをつけた data Die = Die_ Int -- パターンマッチに使うキーワードを指定 pattern Die n <- Die_ n -- これはさっきと同じ makeDie :: Int -> Maybe Die makeDie n | 1 <= n && n <= 6 = Just $ Die_ n | otherwise = Nothing
まずはじめに言語拡張の使用を宣言しています. これによりpattern
関数が使えるようになります. 使い方はコード内に書かれている通りです.
pattern Die n <- Die_ n
これは, 「モジュール内で言うところのDie_ n
を, モジュール外にはDie n
として提供するよ(ただしパターンマッチでしか使えないけどね)」ということを表しています. こう捉えれば, 左向きの矢印が使われていることも理解しやすくなるかもしれません.
パターンマッチで使うキーワードにDie
を譲ったので, データコンストラクタにはDie_
を使っています.
最後に, モジュール外に提供する識別子の中にpattern Die
が含まれていることにも注意しておきましょう.
PatternSynonymsについては https://qiita.com/as_capabl/items/d2eb781478e26411a44c に詳しく書かれているので, 併せて参照していただくのがよいかと思います.