purescript

Partial制約

はじめに

カレンダー空いていたのでまた記事書きます。何とか途切れないようにしたいですね。今回はPartial制約について書きたいと思います。

PureScript では以下のような関数を書くとコンパイラに怒られます。

foo :: Maybe Int -> String
foo (Just n) = show n

エラーメッセージを見ると、引数が Nothing の場合の実装が足りてないことが分かります。ただ下のほうに「代わりに Parital 制約を付けてもいいよ」とも書いています。

A case expression could not be determined to cover all inputs.
The following additional cases are required to cover all inputs:

  Nothing

Alternatively, add a Partial constraint to the type of the enclosing value.

実際に Partial 制約を付けるとコンパイルが通るようになります。

foo :: Partial => Maybe Int -> String
foo (Just n) = show n

この Partial が何者なのか、何故あると嬉しいかを見ていきます。

Partial関数とは

この "Parital" は意味的には Partial関数の Partial を指しています。Parital関数とは引数の型が取りうる全ての値に対して返値が定義されていない関数の事を指します。例えば上記 foo 関数の例ですと、引数の値が Nothing の場合に返値が定義されていません。Partial関数の逆は Total関数です。Total関数は取り得る全ての引数に対して返値が定義されています。

良く挙げられる例ですと、haskell の Prelude に含まれる head 関数はParital関数です。

head :: [a] -> a

リストの先頭の値を取り出す関数なのですが、リストが空の場合の挙動が定義されていません(というか定義できません)。結果の型を Maybe a にすればTotal 関数にできます。実際 Prelude代替ライブラリでは結果の型を Maybe aに変えた headMaybe みたいな関数が提供されています。

Partial 関数を使いたいとき

当然ですが、Paritial関数は良くない関数です。ある引数に対しては挙動が定義されていないため、そのような引数が実際に渡ってきた場合、プログラムがクラッシュしてしまいます。Partial 関数は引数の型を適切な型に変えるか、結果の型を Maybe で包むことによって Total関数に変えることができます。

ただ、たまにPartial関数を使いたい・作りたいというケースがあります。結果の型がいちいち Maybe で包まれているのが邪魔であったり、引数の型が適切変えるのが難しいといったケースです。パフォーマンスヘビーな箇所なら例外ケースの判定や余計な型で包むのは避けたい場合があると思います。

その場合、Partial関数に適切な引数を渡すのはユーザ側の責任になります。使う側が適切な引数が渡されることを保証する必要があります。

Partial制約

人間なので、間違えることはあります。Parital関数を扱うときに間違いが起こりにくいよう PureScript では ver 0.9 から Partial制約を導入しました。これは Parital関数の場合、Parital制約が付くよう強制する仕組みです。Parital制約は Partial関数をTotal関数にはしてくれませんが、使う際に注意を促してくれます。

foo :: Partial => Maybe Int -> String
foo (Just n) = show n

Partial制約の実態は単なるパラメータの無い型クラス制約です。そのためPartial関数を利用する関数自身も Partial制約を必要とします。

bar :: Partial => Int -> Maybe Int -> String
bar i n =
  ..
  foo n
  ..

ただPartial制約はあくまでも目印であって、関数がどのような引数を前提としているかはコメントまたはドキュメントに書く必要があります。

unsafeParital関数で制約を解除

Partial関数を使う際、渡す引数が適切であると保証できる場合、unsafePartial 関数を使ってPartial制約を外します。 unsafePartial は次のような型を持っています。

unsafePartial :: forall a. (Partial => a) -> a

例として unsafeHead :: forall a. Partial => Array a -> a 関数を使う場面を考えます。何も付けずに使うと定義した関数も Partial制約が付いてしまいます。

foo :: Partial => Array Int -> String
foo []  = "Empty"
foo [x] = "Just one: " <> show x
foo xs  = "Many. Head is " <> show (unsafeHead xs)

ここで、最初に配列が空の場合の処理があるため、unsafeHead に渡す配列xsは必ず空ではないことを保証できます。そこで unsafeHead の呼び出しの結果を unsafePartial を使って Partial制約を消してやります。

foo :: Array Int -> String
foo []  = "Empty"
foo [x] = "Just one: " <> show x
foo xs  = "Many. Head is " <> show (unsafePartial $ unsafeHead xs)

結果 foo関数のPartial制約は消え、晴れてTotal関数になりました(いや、元々Total関数でしたが、型がちゃんとそれを表現するようになりました)。

(ちなみに unsafePartial という関数名、分かりにくいのは僕だけでしょうか。保証ができたときに使うのに unsafe が入っています。むしろ ImSureItsSafe とかそんなのにしたほうが良くね?と思っています。)

ちなみにPartial関数の使い方を誤ると?

Partial関数に対して適切でない引数を渡した場合、例外が投げられます。その例外を捕捉することもできます。ただ PureScriptの例外は現状 Error型しかなく、IO例外と区別が難しいため、捕捉すること前提でParital関数を危険な使い方をするのは良くないと考えています。

まとめ

Partial制約のおかげで危険なPartial関数が、少し安全に扱えるようになりました。

PureScript:「やはりPartial関数は悪い文明!! Partial制約付けやる!!」