Edited at
HaskellDay 12

部分関数をどう扱うか(spoonの紹介)

More than 1 year has passed since last update.

純粋な振りして例外投げる関数滅べばいいのに

部分関数ダメゼッタイ


お行儀の悪い関数

Haskellにはいくつか「お行儀の悪い関数」があります。ここで言う「お行儀の悪い」とはIOが付いているわけでもないのに例外を投げる関数のことです。数学やhaskellでは「部分関数(partial functions)」と呼ばれているようです。今回はその「お行儀の悪い関数」をどう扱うかという話をします。はっきりと言ってバッドノウハウの類いの話です。

そもそもなぜ部分関数が問題であるのかはPartial Function Considered Harmful - 純粋関数空間で詳しく説明されているのでこちらも併せて読むとより良いと思います(丸投げ

haskellにおける部分関数で有名なものにhead, last, tail, initがありますね。これらの関数は空リストを渡すと例外を投げます。divは第二引数に0を渡すと、また!!はリストの長さ以上のインデックスを指定すると例外を投げます。あとはreadfold(l|r)1などなど。

また、undefinederrorや網羅的ではないパターンマッチを使った関数も部分関数になるでしょう。

> head []

> last []
> tail []
> init []
-- *** Exception: Prelude.head: empty list

> 10 `div` 0
-- *** Exception: divide by zero

> [1] !! 1
-- *** Exception: Prelude.!!: index too large

unknown = undefined
> unknown
-- *** Exception: Prelude.undefined

data Piyo = Foo | Bar

partial :: Piyo -> Int
partial Foo = 1

> partial Bar
-- *** Exception: Non-exhaustive patterns in function

このような関数は型を見ても部分関数であるか判断できず、突然例外を投げるため非常にやっかいです。


対策


例外を投げる値を弾く関数でラップする

一番シンプルで、おそらくベストな方法は例外を投げる値かどうかを判断する方法です。

safeHead :: [a] -> Maybe a

safeHead [] = Nothing
safeHead xs = Just . head $ xs

すでにいくつかの関数ではsafeに例外を投げないような関数が定義されているのでこちらを使うと良いでしょう。


IOで包む

問題はここからです。

どのような値で例外を投げるか知ることができれば上記のような方法で解決できますが、何らかの理由でどのような値で例外を投げるか知ることができなかったり、あるいは例外を投げる条件が非常にややこしかったりするとどうしましょう。

今回はheadがそのような面倒な関数だとします。

こういう時はdeepseq + returnを使うか、evaluateを使ってIOで包むとControl.Exceptioncatchによってキャッチすることができるようになります。

import Control.Exception

bomb :: Int
bomb = head [] --WHNFの時点で例外

deepseq bomb (Just `fmap` return bomb) `catch` \(SomeException _) -> return Nothing
-- IO Nothing

Just `fmap` evaluate bomb `catch` \(SomeException _) -> return Nothing
-- IO Nothing

 IOの伝播が問題にならない場合はこれで解決です。

ちなみにevaluateの例で(evaluate . Just) bombとするとJust bombの時点で弱頭部正規形(weak head normal-form, WHNF)になるため、例外は捕捉できません。

理由はdeepseqはNFまで評価します(従ってNFDataのインスタンスである必要があります)が、evaluateはWHNFまでしか評価しない点です。従ってevaluateを使う場合は、WHNFまで評価された時点で例外を投げるかどうか気をつける必要があります。


unsafe

上記のようにIOで包めば例外をキャッチすることができるようになりますが、今度はIOを引きずり回すことになります。IOを扱うにはIOの文脈でなければならないので、それを使う関数にもIOが伝播していきます。

どうしてもIOを伝播させたくない場合、最後の手段であるunsafePerformIOを使えばなんとかなります。部分関数が内部でunsafeを使っていない限りは例外を投げること以外は純粋なはずなので、実装的には変なことは起こらないはず・・・多分・・・きっと・・・。

import Control.Exception

import System.IO.Unsafe

bomb :: Int
bomb = head []

unsafePerformIO $ Just `fmap` evaluate bomb `catch` \(SomeException _) -> return Nothing
-- Nothing


spoon

やっとspoonの登場です。spoonはこのような部分関数&&例外を投げる値を弾けない&&IOをどうしても伝播させたくない時に使います。

spoonは内部でdeepseq + returnを、teaspoonは内部でevaluateをしてからunsafePerformIOをしています。

import Control.Spoon

spoon $ head [1]
-- Just 1

spoon $ head []
-- Nothing

前述のようにevaluateではWHNFまでしか評価しないので使うときは注意しましょう。

spoonも内部ではunsafePerformIOを使っているので根本的解決にはなっていませんが、部分関数をどうにかするためという目的が分かる分、直接unsafePerformIOを使うよりも心理的抵抗は小さいのではないでしょうか。


まとめ

今回は「部分関数をどう扱うか」ということについて考えてみましたが、先ほど書いた通り「事前に入力値をチェックする関数で包む」のがベストというのは変わらないでしょう。

チェックすることが現実的でない時は、もうしょうがないです。「こんな関数作ったやつが悪いんだー」と思いながらunsafeを使うことも考えます。せめてもの足掻きとして、spoonを使って心の平穏を保ちましょう。決してunsafePerformIOやspoonを使うことを推奨するわけではありません。「どうにもならない時の最後の手段がちょっとマシになるよ」という思いでこの記事を書きました。バッドノウハウ。

また、このような悲劇を生まないためにも部分関数作るのはかなり強い理由がない限り避けましょう。


参考

Partial Function Considered Harmful - 純粋関数空間

safe: Library of safe (exception free) functions

spoon: Catch errors thrown from pure computations