【Haskell】データコンストラクタにバリデーションを設ける方法

HaskellCLIツールを作っているときに, 本記事のタイトルにあるようなことを疑問に思いました.

例えばサイコロを表すデータ型を次のように定義したとしましょう(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 に詳しく書かれているので, 併せて参照していただくのがよいかと思います.