カーディナリティから考える関数の複雑さ
概要
関数の複雑さを測る指標としてカーディナリティ(Cardinality) という概念が便利という内容です。
この単語は、Matt ParsonsさんのKeep your types small...というブログの中で紹介されています。
本題
カーディナリティ
カーディナリティとは型に属する値の総数のことです。
例:
• Boolean のカーディナリティ:2 {true, false}
• Unit のカーディナリティ:1 {()}
• Option[A] のカーディナリティ:1 + |A|
• Either[A, B] のカーディナリティ:|A| + |B|
• (A, B)(タプル)のカーディナリティ:|A| × |B|
関数の複雑さ
関数fが次のように定義されているとします。
f :: A -> B
このとき、定義可能な関数の数は次のように表されます。
これは、A のそれぞれの値に対して B のどの値を対応させるかを選ぶためです。
この値をここでは、関数の複雑さと呼ぶことにします。
関数の複雑さが低ければ、必要なテストが少なくて済みます。また、関数合成をすればするほど恩恵が大きくなります。
戻り値を制限するのと引数を制限するのではどちらの方が良いか
safeIsPrime の例
戻り値を制限した場合:
safeIsPrime1 :: PositiveInt -> Bool
ここで PositiveInt は 1 以上の整数のみを許す型です。
-
PositiveIntのカーディナリティは(|Int|- 1) / 2 - 戻り値
Boolのカーディナリティは 2
よって、関数の複雑さは:
引数を制限しないで、戻り値で失敗を表す場合:
safeIsPrime2 :: Int -> Maybe Bool
- 引数
Intのカーディナリティは|Int| - 戻り値
Maybe Boolのカーディナリティは 1 + 2 = 3
よって、関数の複雑さは:
比較すると:
safeIsPrime1 の方が複雑さが小さいです。
よって、カーディナリティに注目すると safeIsPrime1 の方が良いということになります。
safeMax の例
戻り値を制限した場合:
safeMax1 :: NonEmptyList Int -> Int
ここで NonEmptyList Int は空でないリストのみを許す型です。
-
NonEmptyList Intのカーディナリティは|[Int] - 1| - 戻り値
Intのカーディナリティは|Int|
よって、関数の複雑さは:
引数を制限しないで、戻り値で失敗を表す場合:
safeMax2 :: [Int] -> Maybe Int
- 引数
[Int]のカーディナリティは|[Int]| - 戻り値
Maybe Intのカーディナリティは|Int| + 1
よって、関数の複雑さは:
比較すると:
両者の複雑さは数学的には無限ですが、safeMax1 の方がパターン数が少ないです。
そのため、実用上の複雑さや安全性、テスト容易性の観点で safeMax1 の方が優れていると言えます。
まとめ
カーディナリティの観点から見ると
• 戻り値に失敗を含めると、戻り値のカーディナリティが増える → 関数の複雑さが増す
• 引数側で不正な入力をそもそも表現できなくする → 関数の複雑さが下がる
となります。
よって、関数の安全性やテスト容易性、合成のしやすさを考えると、戻り値で制御するよりも、引数を制限する方が望ましいケースが多いといえます。
カーディナリティという指標は、こうした選択の一つの指標になります。
Discussion