🦍

翻訳: Parse, don’t validate (バリデーションせずパースせよ)

2024/10/16に公開
4

これまで、型駆動設計を実践することが何を意味するのか、簡潔でシンプルな説明を見つけるのに苦労してきました。誰かに「どうやってこのアプローチを思いついたのですか?」と尋ねられることが多いのですが、満足のいく答えを出せないことがよくあります。そのアイデアが突然のひらめきで浮かんだわけではなく、正しいアプローチを空から引っ張り出す必要がない、反復的な設計プロセスがあると分かってはいるのですが、そのプロセスを他の人にうまく伝えることができていませんでした。
しかし、およそ1ヶ月前、JSON を静的型付け言語で、そして動的型付け言語にパースしたときに経験した違いについてTwitter上で振り返っていた時、ついに私が探していたものを見つけました。そして、そのスローガンはたった3つの英単語で表せます。

Parse, don’t validate (バリデーションせずパースせよ)

型駆動設計のエッセンス (The essence of type-driven design)

では、型駆動設計の本質とは何でしょうか?あなたがすでに、型駆動設計が何かを知らないのであれば、私のキャッチーなスローガンは全く意味をなさないでしょう。幸運なことに、それはこの投稿があなたに役立つということを意味します。私はこれから生々しいほどに正確に、そして詳細に説明します。しかし、その前に、少し単純化された例で練習する必要があります。

可能性の範囲 (The realm of possibility)

静的型システムの美点の一つは「この関数を書くことはあり得るか」という質問にさえ、容易に答えることがあることです。極端な例えですが、以下のようなHaskellの型定義を考えてみましょう。

foo :: Integer -> Void

明らかに答えはいいえです。なぜなら、Void[1]は値を含まない型であり、どんな関数もVoid型の値を生成することはできないからです。この例はあまりにつまらないですが、この質問はより現実的な例を選ぶとより面白くなります。

head :: [a] -> a

この関数はリストの最初の要素を返します。この関数を実装することは可能でしょうか?確かにこれは特に複雑な事には思えませんが、もしこれを実装しようとしても、コンパイラーを満足させることはできません。

head :: [a] -> a
head (x:_) = x
warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘head’: Patterns not matched: []

このメッセージは、関数が部分的であり、つまりすべての可能な入力に対して定義されていないことを親切に指摘しています。特に入力が空のリスト[]だったときの定義が不足しています。これは理にかなっています。空のリストの最初の要素を返すことはできません。返す要素などないのですから。驚くべきことに、この関数も実装することが不可能だということがわかります。

部分関数を全域関数にする (Turning partial functions total)

動的型付言語のバックグランドの人にとっては、このことは面倒なことに思えるかもしれません。もしリストがあれば、リストから要素を取得するのに苦労はしないかもしれません。そして確かに、「リストの最初の要素を取得すること」はHaskellでは不可能ではありません。ただ、少しの手続きを加える必要があるのです。このhead関数を直す方法は2つあります。ここではもっとも簡単なものから始めましょう。

期待値を調整する (Managing expectations)

すでに見たように、head関数は空のリストが入力として与えられたときに返す要素がないため部分関数です。幸運なことにこのジレンマを解消するのは簡単です。約束を弱めればいいのです。関数を呼び出す人がリストに1つの要素を含めることを保証できないので、期待値を調整する必要があります。つまり、一つの要素を返すことに最善を尽くすが、何も返さない権利も確保しておくのです。Haskellではこの可能性をMaybe型を使って表現します。

head :: [a] -> Maybe a

これによってhead関数を実装する自由を得ます。結果的にaという型を生み出すことができないとわかった時に、Nothingを返すことができます。

head :: [a] -> Maybe a
head (x:_) = Just x
head []    = Nothing

問題解決です…ね?この瞬間は…はい、しかしこの方法には隠れたコストがあります。

Maybe型を返すことは、head関数を実装する上で疑いなく便利です。しかし、「利用」する際には、著しく不便になります。
headは常にNothingを返すことがありうるので、その可能性に対処するという負債は呼び出し側にのしかかるこのになります。そして、時にそれは信じられないほどに苛立たしいものになります。その理由は次のコードを考えるとわかります。

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が実行環境からファイルパスのリストを検索する時、事前にリスト空でないことをチェックします。しかし、headがリストの最初の要素を取得するためにmainの中で使われる時、Maybe FilePathという結果によって、絶対にあり得ないとわかっているNothing型に対処しなければなりません。これは以下のいくつかの理由からひどく悪いことです。

  1. 第1に、単純に面倒です。すでに空ではないとチェックしたリストのに、どうして冗長なチェックでコードを散らかさないといけないのでしょうか?
  2. 第2に、潜在的なパフォーマンスコストがあります。この典型例での冗長なチェックのコストはとるに足らないものですが、例えばタイトループの中で (チェックが) 発生する場合など、この冗長なチェックが積み重なるようなもっと複雑なシナリオは想像できるでしょう。
  3. 最後に、また最悪なことに、このコードはバグが起きることを招き寄せています。もし、意図的かどうかに関わらずgetConfigurationDirectoriesがリストが空でないことをチェックすることを、やめてしまったらどうでしょうか?プログラマーはmainを更新するのを覚えていないかもしれません。そしてある日突然、「あり得ない」エラーが起こり得るだけではなく、起こりそうになるのです。

冗長なチェックへのニーズは、型のついたシステムに穴を開けてしまいます。もし私たちがNothingになることはあり得ないと静的に証明することができれば、getConfigurationDirectoriesがリストの要素があることをチェックすることをやめても証明が無効になり、コンパイル時にエラーになるのです。しかしながら、書いてある通り、テストスイートの手動の検査によってバグを見つけなければなりません。

前払いする (Paying it forward)

明らかに修正版のheadはいくつか改善の余地があります。どうにかもう少し賢なって欲しいところです。もしリストが空ではないとすでにチェックされていたなら、headは条件によらず、最初の要素を返すはずです。あり得ないケースを扱うことを強制する必要もありません。どうしたらそれができるでしょうか?

元々の(部分的な)headの型定義を見てみましょう。

head :: [a] -> a

前節では戻り値の約束を弱めることで部分関数を全域関数にする方法を説明しました。しかしそんなことはしたくないので、もう一つの方法を取ることにします。引数の型(今回で言うと[a])を変えるのです。戻り値の型を弱める代わりに、入力の型を強めるのです。それによって、headが空のリストに対してよばれる可能性を完全に排除できます。

そのためには、空ではないリストの型を表現する必要があります。幸運なことに、Data.List.NonEmptyNonEmpty型がまさしくそれです。これは以下のように定義されています。

data NonEmpty a = a :| [a]

NonEmpty aは、実際にはaと通常の(空である可能性のある)[a]のタプルであることに注目してください。これは最初の要素と残りの要素を分けて保管することで空ではないリストを便利にモデリングしています。これによってheadのを非常に簡単に実装できます。[2]

head :: NonEmpty a -> a
head (x:|_) = x

さっきとは異なり、GHCはこの定義を文句なく受け入れます。全域関数であり、部分関数ではありません。この定義を使えば実装は以下のようにできます。

getConfigurationDirectories :: IO (NonEmpty FilePath)
getConfigurationDirectories = do
  configDirsString <- getEnv "CONFIG_DIRS"
  let configDirsList = split ',' configDirsString
  case nonEmpty configDirsList of
    Just nonEmptyConfigDirsList -> pure nonEmptyConfigDirsList
    Nothing -> throwIO $ userError "CONFIG_DIRS cannot be empty"

main :: IO ()
main = do
  configDirs <- getConfigurationDirectories
  initializeCache (head configDirs)

mainの中で冗長なチェックが一切なくなったことに注目してください!代わりに、getConfigurationDirectoriesの中でチェックがまさに1回だけ行われています。これによりData.List.NonEmptynonEmpty関数を使って[a]からNonEmpty aが作り出されています。nonEmpty関数は以下のような型になっています。

nonEmpty :: [a] -> Maybe (NonEmpty a)

Maybe型はまだ姿を見せていますが、今回は、Nothingの場合をとても明確にすることができています。まさに入力値のバリデーションを行っていたのと同じ場所です。一度そのチェックを通せば、NonEmpty FilePath型の値を手にすることができます。それはつまり、リストが空ではないという知識を残すことになります。(型システムによって!)

headの結果の型を弱める代わりに、引数の型を強めることで前の節での問題を完全に取り除くことができました。

  • コードには冗長なチェックはなく、何のパフォーマンスオーバーヘッドもありません
  • さらに、もしgetConfigurationDirectoriesがリストが空でないことのチェックを止めるように変更された場合、戻り値の型が必ず変わります。同時にmainの型検証が失敗して、プログラムを実行する前に問題を知ることができるのです。

さらに加えると、元のheadの振る舞いをnonEmptyheadを実装することで、置き換えることができます。

head' :: [a] -> Maybe a
head' = fmap head . nonEmpty

この逆は成立しないことに注目してください。新しいバージョンのheadから古いものを得ることはできません。結局のところ、第2のアプローチの方がすべての点で優れています。

パースの力 (The power of parsing)

もしかすると上の例はこの投稿のタイトルと何の関係があるのかと疑問に思っているかもしれません。結局、リストが空でないことをバリデーションする2つの方法を試してみただけで、パースについては触れてもいません。その解釈は間違っていませんが、もう一つの視点を提案したいと思います。私の考えではバリデーションとパースの違いは、ほぼ全て、どのように情報が保持されるかと言うことにあるのです。以下の2つの関数について考えてみてください。

validateNonEmpty :: [a] -> IO ()
validateNonEmpty (_:_) = pure ()
validateNonEmpty [] = throwIO $ userError "list cannot be empty"

parseNonEmpty :: [a] -> IO (NonEmpty a)
parseNonEmpty (x:xs) = pure (x:|xs)
parseNonEmpty [] = throwIO $ userError "list cannot be empty"

これらの2つの関数はほぼ同じものです。与えられたリストが空ではないことをチェックしています。そして、もし空ならエラーメッセージと共にプログラムを中断します。その違いはすべて戻り値の型にあります。validateNonEmptyは常に()を返します。その型には何の情報もありません。しかし、parseNonEmptyは入力値の改良版であるNonEmpty aを返します。これによって型システムの中に、獲得した知識を保持することになります。これらの関数のどちらの同じことをチェックしていますが、parseNonEmptyは呼び出し側に学習した情報へアクセスできるようにしていますが、validateNonEmptyはただその情報を放り捨ててしまっています。

これらの2つの関数は型システムの2つの異なる観点を見事に説明しています。
validateNonEmptyは型検査に十分従っていますが、parseNonEmptyのみがその最大の恩恵を受けることができています。どうしてparseNonEmptyの方が好ましいかわかれば、私が「バリデーションせずパースせよ」というスローガンで何を意味しているかを理解できるでしょう。それでもまだ、あなたはparseNonEmptyの名前に疑いを持っているかもしれません。それは本当になんでもパース(解析)しているのでしょうか?それともただ入力をバリデーションして結果を返しているだけなのでしょうか?パースとバリデーションの正確な定義は議論があるものである一方で、私はparseNonEmptyが正真正銘のパーサーであると信じています(中でも特にシンプルなものではありますが)。

「パーサーとは何か」考えてみてください。本当にパーサーとはただのより構造化されていない入力を消費して、より構造化された出力を生み出すだけのただの関数にすぎません。原理的に、パーサーは部分関数です。ドメイン内の一部の値は範囲内のどの値にも対応しません。そのため、すべてのパーサーはいくらかの失敗という概念を持たざるを得ません。しばしば、パーサーの入力はテキストですが、必ずそうでなければならないということでは決してありません。そしてparseNonEmptyは完全にまあまあなパーサーです。リストを空ではないリストにパースして失敗する時にはエラーを吐いてプログラムを終了します。

柔軟な定義によってパーサーは信じられないほど強力なツールになりました。入力値のチェックをちょうどプログラムと外界の境目でチェックできるようになり、1度これを行えば全く再チェックしなくて良くなったのです!Haskellerはこの力をよく知っています。そして彼らはたくさんの種類のパーサーを日頃から使っているのです。

  • aeson はJSONをパースしてドメインの型として使えるようにするパーサーを提供します。
  • 似たように、 optparse-applicative はコマンドライン引数のためにいくつかのパーサーの組み合わせを扱えるようにするライブラリです。
  • persistentpostgresql-simple といったデータベースのライブラリは外部のデータストアが持っているデータをパースする機構を持っています。
  • servantエコシステムはHaskellのデータタイプをパスコンポーネントやクエリパラメーター、HTTPヘッダーなどからパースする仕組みで作られています。

これらのすべてのライブラリに共通するテーマは外界とHaskellアプリケーションの境界上にあるものだということです。それらは和型や積型ではなく、Byteのストリームであるため、なんらかのパースを行う必要がありません。データに対処する前にパースを行うことは、多くの種類の重大なセキュリティの問題を含むバグを回避することに役立ちます。

1つこのすべての最初にパースするアプローチに欠点があるとすれば、しばしば値がパースされるのが実際に使われるよりも大幅に前になってしまうことです。このことは動的型付言語ではパースと処理のロジックに一貫性をもたせることを少し難しくします。莫大なテストカバレッジがあればこれらの問題を防ぐことができますが、ほとんどの場合はそのテストのメンテナンスに非常に手間がかかります。しかし、静的型付システムでは、問題が驚くほどシンプルになります。上記のNonEmptyの例で示された通りです。つまり、もしパースと処理のロジックに一貫性がない場合、そのプログラムはコンパイルすることすら失敗します。

バリデーションの危険性 (The danger of validation)

願くば、この時点で、あなたは少なくともパースの方がバリデーションよりも好ましいという考え方に感化されていて欲しいですが、まだ、疑いをひきずっているかもしれません。もし型システムが必要なチャックを強制するとしたら、バリデーションは本当にそんなに悪いものなのでしょうか。もしかするとエラーの見え方は少し悪くなるかもしれませんが、少しの冗長なチェックはそんなに辛くないですよね?

残念なことに、話はそうシンプルではありません。その場その場でのバリデーションは言語理論的セキュリティ領域におけるショットガンパーシングという現象につながります。2016年の論文の The Seven Turrets of Babel: A Taxonomy of LangSec Errors and How to Expunge Themでは著者たちが下記のような定義をしています。

ショットガンパーシングはパースおよび入力値のバリデーションコードを処理コードと混合して全体に分散させるプログラミングのアンチパターンです。つまり、入力に対して大量のチェックを投げかけ、体系的な正当性なしに、そのうちの 1 つまたは 2 つがすべての「悪い」ケースをキャッチすることを期待します。

原文
Shotgun parsing is a programming antipattern whereby parsing and input-validating code is mixed with and spread across processing code—throwing a cloud of checks at the input, and hoping, without any systematic justification, that one or another would catch all the “bad” cases.

彼らは続けてそのようなバリデーション手法に内在する問題について説明しています。

ショットガンパーシングは必然的にプログラムから不正な入力を処理せずに拒否する能力を奪います。入力のストリームの中で遅くなってから発見されたエラーは結果として処理ずみの不正な入力の一部になるでしょう。その結果プログラムの状態を正確に予測することが難しくなります。

原文
Shotgun parsing necessarily deprives the program of the ability to reject invalid input instead of processing it. Late-discovered errors in an input stream will result in some portion of invalid input having been processed, with the consequence that program state is difficult to accurately predict.

言い換えると、最初にすべての入力をパースしていないプログラムは、有効な入力に対して対処しているときに、異なる部分が不正であると発見し、どんな処理が実行済みであろうとも、一貫性を維持するために、突然巻き戻さなければならなくなるリスクにあります。時々(RDBMSのトランザクション処理などでは)こういったことは可能ですが、一般的にはそうではありません

ショットガンパーシングがバリデーションとどういう関係にあるかは直ちに明らかになることはないかもしれませんが、結局のところ、すべてのバリデーションを最初にやっておけばショットガンパーシングのリスクを軽減することができます。問題は、バリデーションベースのアプローチは全てが実際に最初にバリデーションされたのか、もしくはいわゆる「起こり得ない」ケースが実際に起こるのかを確かめることを極端に難しくする、または不可能にすることです。プログラムの全体は例外がどこでも発生させることが起こりうるだけではなく、むしろ定期的に起こる必要があると想定する必要があります。

パースはこの問題を2つのフェーズに階層分けすることで回避しています。パースと実行です。この場合、不正な入力による失敗は最初のフェーズでしか起こり得ません。実行の中での残りの失敗は比べて最小であり最大限の注意を持ってそれらをケアすることができます。

バリデーションせずパースせよの実践(Parsing, not validating, in practice)

今の所このブログポストは何かのセールスピッチです。「そこの読者のあなた、パースしないわけにはいきません!」と言っているような感じです。そしてもし私がちゃんと仕事ができたのであれば、少なくとも読者のうちの何人かはこの考え方を購入してくれたでしょう。しかしながら、たとえあなたが「何を」「どうして」やらないといけないのか理解できたとしても、「どうやって」実現すればいいのかまだ自信がないかもしれません。

私のアドバイスは「データ型に集中しなさい」です。

キーバリューペアのタプルのリストを受け付ける関数を書くことを考えてみるとあなたは不意に重複したキーをどうしたらいいかわからないことに気づきます。一つの解決策は、リストの中にいかなる重複も存在しないと表明 (assert) する関数を書くことです。

checkNoDuplicateKeys :: (MonadError AppError m, Eq k) => [(k, v)] -> m ()

しかし、このチェックは危ういものです。最も簡単に忘れ去れれてしまうでしょう。なぜなら、その戻り値が使われておらず、削除することも常に可能で、それでも型検査は通るからです。より良い解決法はMapと言った重複したキーを許可しないデータ構造を選ぶことです。普段あなたがするように、先ほどの関数をタプルのリストではなく、Mapを受け付けるように調整して実装しましょう。

一度このようにすれば、この新しい関数の呼び出し側ではタプルのリストを渡しているので型検査は失敗するでしょう。もしそれが呼び出し側の引数の一部から与えられた値であったり、他の関数の戻り値の一部だった場合に、呼び出しの連鎖を辿ってリストからMapに変換し続けることができます。最終的には、その値が作られた最初の場所に辿り着くか、重複が実際に許されるべきところを発見するでしょう。現時点では、修正されたバージョンのcheckNoDuplicateKeysを差し込むことができます。

checkNoDuplicateKeys :: (MonadError AppError m, Eq k) => [(k, v)] -> m (Map k v)

これでもう重複チェックは削除することができません。プログラムが実際に処理するのに必要な処理になったからです!

この想定上のシナリオは以下の2つのシンプルなアイディアを強調します。

  1. 不正な状態を表現できないようなデータ構造を使ってください。 可能な限りもっとも厳格なデータ構造をモデリングしてください。もし現在使っているエンコーディングで特定の可能性を排除するのがあまりにも難しいのであれば、関心のある属性をより簡単に表現できるエンコーディングを検討してください。リファクタリングを恐れないで。
  2. 証明する責任を可能な限り押し上げてください、ただしその範囲まで。可能な限り早い段階でデータを可能な限り最も厳密な表現にしてください。システムの境界で、そのデータに対してあらゆるアクションが取られる前に。もしあるコードの枝葉がデータをより厳格な表現にすることを求めた場合、その枝葉が選ばれた時すぐにそのデータをより厳格な表現にパースしてください。直和型を賢明に使い、データ型が制御フローを反映し、適応するようにしてください。[3]

言い換えれば、関数を書くときに、与えられたデータではなく、欲しいデータの表現をするようにしてください。そうするとその設計プロセスは、両者が中間のどこかで落ち合うまで、両方の側から働きかけるギャップを埋める活動になります。インタラクティブに設計の部品を調整することを恐れないでください。そのリファクタリングのプロセスで何か学ぶことがあるかもしれないので!

ここには手に余るほどの追加のアドバイスがあります。順番は特にありません。

  • データ型がコードに情報を与えるようにして、コードがデータ型をコントロールすることがないようにしてください。 今書いているコードを関数が必要だからという理由だけでレコードのどこかにBoolをくっつける誘惑を回避してください。リファクタリングを恐れず、正しいデータの表現をしてください。型システムは変更が必要な箇所をすべて検知して、後で頭痛に苦しめられないようにしてくれます。

  • m()を返す関数は強い疑いを持って扱ってください。 ときには、命令型的な副作用があり、意味のある結果を返さないために、これらが本当の意味で必要になることはあります。しかし、もし主たる目的が例外を投げることであれば、もっとより良い方法があるはずです。

  • 複数パスでデータをパースすることを恐れないでください。 ショットガンパーシングを避けるというのはただ、パースされていないデータに対して処理を行ってはいけないというだけのことで、入力値の一部が、他の入力値のパース方法を変えてはいけないということではありません。多くの有用なパーサーは文脈依存です。

  • 非正規化されたデータ型を使うのを避けてください、"特に"それがミュータブルな時は。 同じデータを複数の箇所で複製することは、複数の場所の同期が外れる (同時に変更されない) という、表現可能であることが自明な不正な状態を作り出します。Single Source of Truth(信頼できる唯一の情報源)を懸命に求めましょう。

    • 非正規化されたデータの表現を抽象境界の後ろ側にとどめておいてください。 もし、非正規化されたデータがどうしても必要であれば、カプセル化を使って、小さい、信用できるモジュールが唯一その表現に一貫性があることに責任を持つようにさせてください。
  • バリデーターを抽象データ型を使ってパーサーに「見える」ようにしてください。 ときに、は整数が特定の範囲にあることを保証など、不正な状態を本当の意味で表現できないようにするのHaskellが提供するツールを考えるとただただ非現実的です。そのときにはスマートコンストラクタで抽象newtypeを使って、バリデーターをパーサーとして「偽装」します。

いつもあなたがそうしているように、最もいい判断をしてください。どこかにある一つのerror "impossible"の呼び出しを取り除くためだけに singletons を持ち出してアプリケーション全体を書き換えるのは割に合わないでしょう。それらの状況を放射線物質かのようにみなし、適切な処理をしてください。もしすべての他のことに失敗したら、少なくとも誰かは問わず次にコードを修正する必要がある人のために不変条件をドキュメントとして残すためにコメントを残してください。

以上です。本当に。願わくば、このブログポストがHaskellの型システムの利点を活かすのにPhDが必要なく、GHCの最新で最高の輝しい新しい言語拡張を利用する必要もないことを、これがただただ素晴らしいことを通して証明できれば幸いです。ときに、Haskellを最大限に使う上で最も大きな障害になるのは、単にどのようなことができるのかを認識することです。そして残念なことに、Haskellの小さなコミュニティはデザインパターンと暗黙値になってしまったテクニックをドキュメントにするリソースが不足しています。

このブログ記事に書かれているアイデアは新しいものではありません。実際、核心となるアイデアである「全域関数を書く」というのは、概念的には非常に単純です。それにもかかわらず、私がHaskellのコードを書く方法について、実践的で具体的な詳細を伝えるのは驚くほど難しいと感じています。抽象的な概念について長時間話すのは簡単ですが(その多くは非常に価値があります!)、プロセスに関して役立つ情報を伝えることはできていないように感じます。私の希望としては、これはその方向に向けた小さな一歩となることです。

悲しいことに、この特定のトピックについて多くの他の情報源を知りませんが、一つ知っているのは、Type Safety Back and Forthです。もし、これらのアイデアについて別の視点からアクセスしやすい解説や、別の具体例を見たい場合は、ぜひ読んでみることをお勧めします。また、これらのアイデアに関してもっと高度な内容を知りたい場合は、Matt Noonanの2018年の論文「Ghosts of Departed Proofs」もお勧めです。この論文では、ここで説明したものよりもさらに複雑な不変条件を型システムで表現するためのいくつかのテクニックが概説されています。

最後に付け加えたいのは、このブログ記事で説明したようなリファクタリングを行うのは、必ずしも簡単ではないということです。私が示した例はシンプルですが、現実はしばしばそれほど単純ではありません。型駆動設計に慣れている人でも、特定の不変条件を型システムで表現するのは本当に難しいことがあります。ですから、自分が思い通りに問題を解決できなかったとしても、それを個人的な失敗だとは考えないでください!このブログ記事で述べた原則は、目指すべき理想と考えてください。絶対に守るべき厳格な要件ではありません。大切なのは、挑戦し続けることです。

脚注
  1. 厳密に言うと、Haskellでは「ボトム」(どんな値にも存在し得る構造)を無視しています。これらは他の言語のnullとは違い、「実際の」値ではありません。無限ループや例外を発生させる計算のようなものです。そして、慣用的なHaskellでは、通常これらを避けようとします。したがって、これらが存在しないと仮定して推論することにも価値があります。しかし、私の言葉を鵜呑みにしないでください。Danielssonらの論文「Fast and Loose Reasoning is Morally Correct」が、その理由を納得させてくれるでしょう。 ↩︎

  2. 実はData.List.NonEmptyheadをすでに提供していますが、ただ説明のために実装しています。 ↩︎

  3. 時には、Dos攻撃を避けるために、ユーザー入力をパースする前に何らかの認可を行う必要がありますが、それで問題ありません。認可は比較的小さな範囲で行われるはずであり、システムの状態に大きな変更を引き起こすことはないでしょう。 ↩︎

GitHubで編集を提案

Discussion

Moq10Moq10

原文のメッセージは「パースしろ。バリデーションはするな」ではないでしょうか?

まじまっちょまじまっちょ

ご指摘ありがとうございます。
語順は変えていますが、意味は変わっていないので翻訳としては適切な範囲内かと思います。
Parse, don't validateという言葉は元の記事の方でもスローガン的に使われており、日本語としての語感を大事にしたため上記になっています。

koba-e964koba-e964

誤訳の修正です。

JSONを静的に、そして動的に型付言語にパースしたときに

JSON を静的型付け言語で、そして動的型付け言語にパースしたときに です。

もっと複雑なシナリオでの冗長なチェックはまるでびっしりとしたループの中で起こっているかのように、増し加えられていきます。

原文は

one could imagine a more complex scenario where the redundant checks could add up, such as if they were happening in a tight loop.

なので、例えばタイトループの中で (チェックが) 発生する場合など、この冗長なチェックが積み重なるようなもっと複雑なシナリオは想像できるでしょう。です。

一つの解決策はその関数がリストの中にいかなる重複も許さないと表明する関数にすることです。

一つの解決策は、リストの中にいかなる重複も存在しないと表明 (assert) する関数を書くことです。 です。

現時点では、修正されたバージョンの

その場所に、修正されたバージョンの です。

ときにこれらは、絶対的な副作用があり、意味のある結果を返さないときに、本当の意味で必要になることはあります。

"imperative effect" というのは Haskell の用語で「命令型的な副作用」といった意味を持ちますので、ときにはこういった関数は本当に必要かもしれません。なぜなら意味のある結果を返さず命令型的な副作用を発生させるかもしれないためです。 が正しいです。意味のある結果を返さず の部分は戻り値の型の () 部分に対応します。

複数の経路でデータをパースすることを恐れないでください。

複数のパスでデータをパースすることを恐れないでください。 です。この「複数パス」 (multiple passes) という用語はデータに対して複数回処理を行うことを指す言語処理系界隈の用語です。

多くの有用なパーサーはコンテキストに柔軟に対応します。

多くの有用なパーサーは文脈依存です。 です。「文脈依存文法」(context-sensitive grammar) という用語があります。

同じデータを複数の箇所で複製することは、いとも簡単に不正な状態を表現してしまう状態を作り出します、それも一貫性のない場所で。

原文は

a trivially representable illegal state: the places getting out of sync

となっていて、この : は説明の : なので 複数の場所の同期が外れる (同時に変更されない) という、表現可能であることが自明な不正な状態 が正しいです。

Single Source of Truth(信頼できる唯一の情報源)を求めて戦いましょう。

戦う ではなく 骨を折る全力で努力する が近いでしょう。

シングルトンを壊して、アプリケーションの全てを書き換えるには値しないかもしれませんが、一つのerror "impossible"の呼び出しをただ取り除いてください。

ここでの "break out" は何か道具を持ち出すといった意味ですので どこかにある一つのerror "impossible"の呼び出しを取り除くためだけに [singletons](https://hackage.haskell.org/package/singletons) を持ち出してアプリケーション全体を書き換えるのは割に合わないでしょう。 が正しいです。

ドキュメントに一定のコメントをしてください。

ここの "leave a comment to document the invariant" は 不変条件をドキュメントとして残すためにコメントを残す です。

認可は比較的小さな範囲で行われるべきであり、システムの状態に大きな変更を引き起こすべきではありません。

ここの should は 〜はずだ なので、認可は比較的小さな範囲で行われるはずであり、システムの状態に大きな変更を引き起こすことはないでしょう。 です。

まじまっちょまじまっちょ

詳細にご確認いただいて、ご指摘いただきありがとうございます。
大変勉強になりました。参考にさせていただき、修正した上で更新しております。