generated at
Parse, don’t validate
Alexis King氏のエッセイ
1つのエッセイの中で、いくつかのことを主張している
このノートではエッセイの流れや要点のみを書くmrsekut
主張は分散させて別のノートに書く


要するに、
プログラムの外部との境界で、正しい仕様を表現したデータ構造に変換すると、色々嬉しい
失敗しうるデータ構造を内部にまで入れない
境界部分で全て根絶させる



流れとしてはこんな感じ
プログラムの外部から内部に入る時に失敗し得ない型にすると良い
ここで言う失敗しうる型は Maybe a で、失敗し得ない型は NonEmpty a
失敗し得ない型にすることで、内部ではhandlingが不要になり、実装がシンプルになる
中盤は、validationではなく、parseしようという話
値の正当性を確かめる時に、validationではなくparseしよう
ただ値チェックするだけのvalidationは漏れうるし、handlingも増える
parseし、仕様を満たしたデータ構造に変換すると良い
内部がシンプルになる
静的に漏れていないことを確認できる
後半は、Parse, don't validateの実践例



元々2つの課題があった
プログラム中でnull checkのようなhandlingが頻発するとダルい
仕様を満たしていることをチェックすることを強制したい
後者をやるためにはparseするしかない
前者に対しては、validation、parseであまり差がないと思うmrsekut
どちらも境界部分でやればいい
このエッセイはなぜかvalidationを境界でやることをあまり前提していない
だからプログラム中でvalidationが必要になってshotgun parsingが出てきてだるいよね、みたいな話にもなっている



用語の注意
validate
値が正しいかどうかをチェックする
正しい場合は、voidを返す
このエッセイでのvalidateはvoidを返すmrsekut
実際は、Boolを返すようなものもvalidateに含まれると思うmrsekut
正しくない場合はthrowする
例.hs
validateNonEmpty :: [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
例.hs
parseNonEmpty :: [a] -> IO (NonEmpty a) parseNonEmpty (x:xs) = pure (x:|xs) parseNonEmpty [] = throwIO $ userError "list cannot be empty"
本来は、両者とも throw を返す必要はないが、恐らく説明のわかりやすさのために throw しているmrsekut
Maybe などを使っても良いはずだが、そうするとvalidateの説明がわかりづらくなる



validateには問題がある
チェック自体を強制できない
漏れる
チェックする関数を呼び忘れたことに気づけない
境界に置くことを強制できない
validate関数は、返り値の型が Void なので、どこで実行するかに制限を設けられない
だから、プログラムの内部もで自由に実行できてしまう
すると、handlingがプログラム内に分散する
チェック済みであるという情報を伝達できない
[(key,value)] というデータが渡ってきた時に、「 key に重複がない」ことをチェック済みかどうか判別できない


parseの結果を信頼できるものにする
こうではなく
js
const data = parse(input); // dataはまだ信頼できない if (validate(data)) { const trusted = data; }
こういうものをshotgun parsingと呼ぶ
これはアンチパターン
こうする
ts
try { const trusted = parse(input); // 結果は信頼できる } catch (error) { throw new Error(); }
parseの結果が100%信頼できるようにする
parseを全域関数として定義する


関連





speaker notesがちゃんとあるmrsekut







前半の話の例 ref
hs
getConfigurationDirectories :: 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のリスト」という扱いにすれば問題なくないか?
これが空リストになるかどうかはあまり関係なくない?mrsekut
実際には、このコード例には main もあるけどこれはセットで見ないといけないか
main の中では head を使っており、 Nothing の方は現時点では到達することがない
それは、 getConfigurationDirectories 内でチェックしているから。
このチェックがなくなった場合、 error "should never..." のところに到達してしまう
これは最初想定していなかったerrorがthrowされることになるので問題
という感じだろうかmrsekut
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"