参考書籍
http://www.amazon.co.jp/JavaScriptで学ぶ関数型プログラミング-Michael-Fogus/dp/4873116600
事前条件と計算の本体
ある関数に渡る引数の妥当性を検証するために、その関数の内部でチェック処理を実装する事はよくある。プログラマが意図した適切な戻り値を生成するために必要な前処理は、事前条件として定義される。繰り返しになるが、この事前条件は関数を設計する際にごく当たり前に存在するし、プログラムに実装すべきものである。
ちなみに自分がなにか趣味でプログラムを作るときには、この事前条件は一旦無視して、本質的な操作だけを実装している。あとから必要に応じて、事前条件を追加で実装している形だ。(理由は面倒だから、がきっと一番の理由なのだろう)
今回の記事は、この事前条件を計算の本体と分離して実装したらいろいろシンプルになって便利だなぁ、ということがわかった話です。
関数の実装に手を入れる事なく、計算の本体と事前条件を合成する
たとえば、1引数関数を取る関数を定義してみる。
uncheckedSqr :: Int -> Int
uncheckedSqr = n * n
実用的な意味はないが、仮に次のような事前条件があるとする。
- 0以下は受け付けない
- 5より大きい整数は受け付けない
ストレートに実装するなら、次のようになるだろう。
checkedSqr :: Int -> Int
checkedSqr n
| or [(n <= 0), (n > 5)] = error "Invalid."
| otherwise = n * n
今回は、この関数を次のように分解して考え、最後に合成する。
-- 1引数とって、真偽値を返す関数とエラーメッセージを組み合わせたEither値を返す
validator :: String -> (a -> Bool) -> a -> Either String a
validator msg p a = if p a then Left msg else Right a
-- 事前条件と操作の本体を合成するための関数
-- 事前条件を満たさない場合、エラーメッセージがリストに溜まり、Left値となる
checker :: [(a -> Either String a)] -> (a -> b) -> a -> Either [String] b
checker [] f n = Right (f n)
checker fs f n = if length lst > 0 then Left lst else Right (f n)
where
lst = foldr pm [] es
es = map (\g -> g n) fs -- es -> eithersの略
pm (Left msg) msgs = msg : msgs -- pm -> pattern matchの略
pm (Right _) msgs = msgs
-- 今回の事前条件と、uncheckedSqrの本体を合成している
checkedSqr = checker
[(validator "invalid value: under zero" (<= 0)),
(validator "invalid value: greater than 5" (> 5))
] uncheckedSqr
checkedSqr (-3) --Left ["invalid value: under zero"]
checkedSqr 8 --Left ["invalid value: greater than 5"]
checkedSqr 5 --Right 25
ちなみにjavascript
では
今回のキモである、validator
関数とchecker
関数は参考書籍で次のように定義されている。
※上の定義と微妙に異なっているが、考え方は同じはず。
var _ = require('underscore')
function validator(message, fun) {
var f = function() {
return fun.apply(fun, arguments);
};
f['message'] = message;
return f;
}
function checker( /* predicates */ ) {
var validators = _.toArray(arguments);
return function(obj) {
return _.reduce(validators, function(errs, check) {
if (check(obj)) {
return errs;
} else {
return _.chain(errs).push(check.message).value();
}
}, []);
};
}
終わりに
コード量は増えたが、各々の関数の汎用性は増した。事前条件の関数は他でも使えるだろうし、合成関数は、部分適用すれば他の操作に対しても適用できる。
単体テストもシンプルになる。
操作の本体と事前条件を合成する。これは関数型プログラミングを学んでいて良かったなぁ、と思えるわかりやすいケースだった。
なお、事前条件と本体との分離という意味では、Haskellならnewtype
とモジュールを利用して、絶対に事前条件を満たした値しか生成されないような型を作ることで、その目的を果たすこともできる。