Parse, don’t validate
1つのエッセイの中で、いくつかのことを主張している
このノートではエッセイの流れや要点のみを書く

主張は分散させて別のノートに書く
要するに、
失敗しうるデータ構造を内部にまで入れない
境界部分で全て根絶させる
流れとしてはこんな感じ
プログラムの外部から内部に入る時に失敗し得ない型にすると良い
ここで言う失敗しうる型は Maybe a
で、失敗し得ない型は NonEmpty a
失敗し得ない型にすることで、内部ではhandlingが不要になり、実装がシンプルになる
中盤は、validationではなく、
parseしようという話
値の正当性を確かめる時に、validationではなくparseしよう
ただ値チェックするだけのvalidationは漏れうるし、handlingも増える
parseし、仕様を満たしたデータ構造に変換すると良い
内部がシンプルになる
静的に漏れていないことを確認できる
後半は、Parse, don't validateの実践例
元々2つの課題があった
プログラム中でnull checkのようなhandlingが頻発するとダルい
仕様を満たしていることをチェックすることを強制したい
後者をやるためにはparseするしかない
前者に対しては、validation、parseであまり差がないと思う

どちらも境界部分でやればいい
このエッセイはなぜかvalidationを境界でやることをあまり前提していない
用語の注意
validate
値が正しいかどうかをチェックする
正しい場合は、voidを返す
このエッセイでのvalidateはvoidを返す

実際は、Boolを返すようなものもvalidateに含まれると思う

正しくない場合はthrowする
例.hsvalidateNonEmpty :: [a] -> IO ()
validateNonEmpty (_:_) = pure ()
validateNonEmpty [] = throwIO $ userError "list cannot be empty"
構造的でない外部の入力を、構造的なデータに変換する
正しい場合は、それを型に込めて値を返す
正しくない場合はthrowする
>a parser is just a function that consumes less-structured input and produces more-structured output. ref
例.hsparseNonEmpty :: [a] -> IO (NonEmpty a)
parseNonEmpty (x:xs) = pure (x:|xs)
parseNonEmpty [] = throwIO $ userError "list cannot be empty"
本来は、両者とも
throw
を返す必要はないが、恐らく説明のわかりやすさのために
throw
している

Maybe
などを使っても良いはずだが、そうするとvalidateの説明がわかりづらくなる
validateには問題がある
チェック自体を強制できない
漏れる
チェックする関数を呼び忘れたことに気づけない
境界に置くことを強制できない
validate関数は、返り値の型が Void
なので、どこで実行するかに制限を設けられない
だから、プログラムの内部もで自由に実行できてしまう
すると、handlingがプログラム内に分散する
チェック済みであるという情報を伝達できない
[(key,value)]
というデータが渡ってきた時に、「 key
に重複がない」ことをチェック済みかどうか判別できない
parseの結果を信頼できるものにする
こうではなく
jsconst data = parse(input); // dataはまだ信頼できない
if (validate(data)) {
const trusted = data;
}
これはアンチパターン
こうする
tstry {
const trusted = parse(input); // 結果は信頼できる
} catch (error) {
throw new Error();
}
parseの結果が100%信頼できるようにする
関連
speaker notesがちゃんとある

hsgetConfigurationDirectories :: IO [FilePath]
getConfigurationDirectories = do
configDirsString <- getEnv "CONFIG_DIRS"
let configDirsList = split ',' configDirsString
when (null configDirsList) $
throwIO $ userError "CONFIG_DIRS cannot be empty"
pure configDirsList
getConfigurationDirectories
は、 [FilePath]
を返す
この関数内ではenvから文字列を読み込んで、空でないかチェックした後に、 [FilePath]
を返している
この関数でcheckがあろうとなかろうと、 [FilePath]
は「正しいpathのリスト」という扱いにすれば問題なくないか?
これが空リストになるかどうかはあまり関係なくない?

実際には、このコード例には main
もあるけどこれはセットで見ないといけないか
main
の中では head
を使っており、 Nothing
の方は現時点では到達することがない
それは、 getConfigurationDirectories
内でチェックしているから。
このチェックがなくなった場合、 error "should never..."
のところに到達してしまう
これは最初想定していなかったerrorがthrowされることになるので問題
という感じだろうか

hs main :: IO ()
main = do
configDirs <- getConfigurationDirectories
case head configDirs of
Just cacheDir -> initializeCache cacheDir
Nothing -> error "should never happen; already checked configDirs is non-empty"