入力の型を厳格にすることでhandlingを避ける
除算をする2つの関数
code:maybe.hs
safeDivide :: Int -> Int -> Maybe Int
safeDivide i 0 = Nothing
safeDivide i j = Just (i div j)
code:notempty.hs
safeDivide :: Int -> NonZero Int -> Int
safeDivide i (NonZero j) = i div j
どちらも型安全に除算を行う
この関数は実行時エラーは起こさない
型安全と言える
誰がNothingをhandlingするか?が異なる
maybe.hsの場合、呼び出し側は、
好きにIntを突っ込んで、
notempty.hsの場合、呼び出し側は、
Intより制限のあるNonZero Intを突っ込んで
結果の利用時にはhandlingの必要はない
NonZero Intを作るためには何らかの処理を噛ませないといけないので、この2つだけ比較すればどちらも嬉しさは変わらないように見える
Entityを定義するときに差が出る
code:hs
data Order = Order { items :: Item } code:hs
data Order = Order { items :: NonEmpty Item }
前者を選んだ場合、
プログラム全体に、Maybeが現れる
それを扱う関数の殆どが返り値の型がMaybeになる
誰かが自分の中でhandlingしないといけない
引数の制限が緩いので、1つ1つ関数の責務も大きくなる
safeDivideなら、引数はIntなので「0で割る」こともサポートする必要がある
後者を選んだ場合、
型の定義に嘘がない
プログラム内部ではhandlingが一切不要になる
handling的なことが必要なのは外部と接続する箇所だけ
validationするのは、外部と接続する箇所だけになるから
layered architectureで言う、最も外側でvalidationさえすれば、
内側では常にNotEempty Itemを持ち回すことができる
その中でいちいちnull checkのようなことをする必要がない
code: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
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"
getConfigurationDirectoriesは、[FilePath]を返す
この関数内ではenvから文字列を読み込んで、空でないかチェックした後に、[FilePath]を返している
main内のconfigDirsは必ず空にはならないため、head configDirsは必ずJust FilePathを返す
が、Maybeなので条件分岐する必要がある
問題としては大きく2つある(記事内では3つ挙げられている)
絶対にNothingにならないのにhandlingが必要(このノートの趣旨)
getConfigurationDirectoriesでcheckされているかどうかがデータ構造から読み取れない
関連
参考
良い記事
だけど、forwardとbackの用語の使い方がいまいちピンと来ていないmrsekut.icon
Maybeの方を「pushing responsiblity forward」
NonEmptyの方を「pushing responsibility back」
と呼んでいる
時系列で見れば逆じゃない?という気もする
とにかく、これを自分の中で説明できるまではこの用語は使わないほうが良さそう
上の記事の次の記事
予定の文脈で
push forwardが、「前倒し」
push backが「先延ばし」