validateの翻訳です。これまで、型駆動設計を実践することが何を意味するのか、簡潔でシンプルな説明を見つけるのに苦労してきました。誰かに「どうやってこのアプローチを思いついたのですか?」と尋ねられることが多いのですが、満足のいく答えを出せないことがよくあります。そのアイデアが突然のひらめきで浮かんだわけではなく、正しいアプローチを空から引っ張り出す必要がない、反復的な設計プロセスがあると分かってはいるのですが、そのプロセスを他の人にうまく伝えることができていませんでした。静的型付け言語で、そして動的型付け言語にパースしたときに経験した違いについてTwitter上で振り返っていた時、ついに私が探していたものを見つけました。そのスローガンはたった3つの英単語で表せます。design)では、型駆動設計の本質とは何でしょうか?あなたがすでに、型駆動設計が何かを知らないのであれば、私のキャッチーなスローガンは全く意味をなさないでしょう。幸運なことに、それはこの投稿があなたに役立つということを意味します。私はこれから生々しいほどに正確に、そして詳細に説明します。その前に、少し単純化された例で練習する必要があります。possibility)静的型システムの美点の一つは「この関数を書くことはあり得るか」という質問にさえ、容易に答えることがあることです。極端な例えですが、以下のようなHaskellの型定義を考えてみましょう。Void明らかに答えはいいえです。Void[1]は値を含まない型であり、どんな関数もVoid型の値を生成することはできないからです。この例はあまりにつまらないですが、この質問はより現実的な例を選ぶとより面白くなります。aこの関数はリストの最初の要素を返します。この関数を実装することは可能でしょうか?確かにこれは特に複雑な事には思えませんが、もしこれを実装しようとしても、コンパイラーを満足させることはできません。[]このメッセージは、関数が部分的であり、つまりすべての可能な入力に対して定義されていないことを親切に指摘しています。入力が空のリスト[]だったときの定義が不足しています。これは理にかなっています。空のリストの最初の要素を返すことはできません。返す要素などないのですから。驚くべきことに、この関数も実装することが不可能だということがわかります。total)動的型付言語のバックグランドの人にとっては、このことは面倒なことに思えるかもしれません。リストがあれば、リストから要素を取得するのに苦労はしないかもしれません。確かに、「リストの最初の要素を取得すること」はHaskellでは不可能ではありません。手続きを加える必要があるのです。このhead関数を直す方法は2つあります。ここではもっとも簡単なものから始めましょう。expectations)すでに見たように、head関数は空のリストが入力として与えられたときに返す要素がないため部分関数です。幸運なことにこのジレンマを解消するのは簡単です。約束を弱めればいいのです。関数を呼び出す人がリストに1つの要素を含めることを保証できないので、期待値を調整する必要があります。一つの要素を返すことに最善を尽くすが、何も返さない権利も確保しておくのです。Haskellではこの可能性をMaybe型を使って表現します。aこれによってhead関数を実装する自由を得ます。結果的にaという型を生み出すことができないとわかった時に、Nothingを返すことができます。Nothing問題解決です...ね?この瞬間は...はい、しかしこの方法には隠れたコストがあります。Maybe型を返すことは、head関数を実装する上で疑いなく便利です。「利用」する際には、著しく不便になります。headは常にNothingを返すことがありうるので、その可能性に対処するという負債は呼び出し側にのしかかるこのになります。時にそれは信じられないほどに苛立たしいものになります。その理由は次のコードを考えるとわかります。non-empty"getConfigurationDirectoriesが実行環境からファイルパスのリストを検索する時、事前にリスト空でないことをチェックします。FilePathという結果によって、絶対にあり得ないとわかっているNothing型に対処しなければなりません。これは以下のいくつかの理由からひどく悪いことです。第1に、単純に面倒です。空ではないとチェックしたリストのに、どうして冗長なチェックでコードを散らかさないといけないのでしょうか?第2に、潜在的なパフォーマンスコストがあります。発生する場合など、この冗長なチェックが積み重なるようなもっと複雑なシナリオは想像できるでしょう。最後に、また最悪なことに、このコードはバグが起きることを招き寄せています。意図的かどうかに関わらずgetConfigurationDirectoriesがリストが空でないことをチェックすることを、やめてしまったらどうでしょうか?プログラマーはmainを更新するのを覚えていないかもしれません。ある日突然、「あり得ない」エラーが起こり得るだけではなく、起こりそうになるのです。冗長なチェックへのニーズは、型のついたシステムに穴を開けてしまいます。私たちがNothingになることはあり得ないと静的に証明することができれば、getConfigurationDirectoriesがリストの要素があることをチェックすることをやめても証明が無効になり、コンパイル時にエラーになるのです。書いてある通り、テストスイートの手動の検査によってバグを見つけなければなりません。forward)明らかに修正版のheadはいくつか改善の余地があります。賢なって欲しいところです。リストが空ではないとすでにチェックされていたなら、headは条件によらず、最初の要素を返すはずです。あり得ないケースを扱うことを強制する必要もありません。したらそれができるでしょうか?元々の(部分的な)headの型定義を見てみましょう。a前節では戻り値の約束を弱めることで部分関数を全域関数にする方法を説明しました。そんなことはしたくないので、もう一つの方法を取ることにします。引数の型(今回で言うと[a])を変えるのです。戻り値の型を弱める代わりに、入力の型を強めるのです。それによって、headが空のリストに対してよばれる可能性を完全に排除できます。そのためには、空ではないリストの型を表現する必要があります。幸運なことに、Data.List.NonEmptyのNonEmpty型がまさしくそれです。これは以下のように定義されています。aは、実際にはaと通常の(空である可能性のある)[a]のタプルであることに注目してください。これは最初の要素と残りの要素を分けて保管することで空ではないリストを便利にモデリングしています。これによってheadのを非常に簡単に実装できます。xさっきとは異なり、GHCはこの定義を文句なく受け入れます。全域関数であり、部分関数ではありません。この定義を使えば実装は以下のようにできます。configDirs)mainの中で冗長なチェックが一切なくなったことに注目してください!代わりに、getConfigurationDirectoriesの中でチェックがまさに1回だけ行われています。aが作り出されています。nonEmpty関数は以下のような型になっています。a)Maybe型はまだ姿を見せていますが、今回は、Nothingの場合をとても明確にすることができています。入力値のバリデーションを行っていたのと同じ場所です。FilePath型の値を手にすることができます。それはつまり、リストが空ではないという知識を残すことになります。(型システムによって!)headの結果の型を弱める代わりに、引数の型を強めることで前の節での問題を完全に取り除くことができました。コードには冗長なチェックはなく、何のパフォーマンスオーバーヘッドもありませんさらに、もしgetConfigurationDirectoriesがリストが空でないことのチェックを止めるように変更された場合、戻り値の型が必ず変わります。同時にmainの型検証が失敗して、プログラムを実行する前に問題を知ることができるのです。加えると、元のheadの振る舞いをnonEmptyでheadを実装することで、置き換えることができます。nonEmptyこの逆は成立しないことに注目してください。新しいバージョンのheadから古いものを得ることはできません。結局のところ、第2のアプローチの方がすべての点で優れています。parsing)もしかすると上の例はこの投稿のタイトルと何の関係があるのかと疑問に思っているかもしれません。結局、リストが空でないことをバリデーションする2つの方法を試してみただけで、パースについては触れてもいません。その解釈は間違っていませんが、もう一つの視点を提案したいと思います。私の考えではバリデーションとパースの違いは、ほぼ全て、どのように情報が保持されるかと言うことにあるのです。以下の2つの関数について考えてみてください。empty"これらの2つの関数はほぼ同じものです。与えられたリストが空ではないことをチェックしています。空ならエラーメッセージと共にプログラムを中断します。その違いはすべて戻り値の型にあります。validateNonEmptyは常に()を返します。その型には何の情報もありません。これによって型システムの中に、獲得した知識を保持することになります。これらの関数のどちらの同じことをチェックしていますが、parseNonEmptyは呼び出し側に学習した情報へアクセスできるようにしていますが、validateNonEmptyはただその情報を放り捨ててしまっています。これらの2つの関数は型システムの2つの異なる観点を見事に説明しています。validateNonEmptyは型検査に十分従っていますが、parseNonEmptyのみがその最大の恩恵を受けることができています。してparseNonEmptyの方が好ましいかわかれば、私が「バリデーションせずパースせよ」というスローガンで何を意味しているかを理解できるでしょう。それでもまだ、あなたはparseNonEmptyの名前に疑いを持っているかもしれません。それは本当になんでもパース(解析)しているのでしょうか?それともただ入力をバリデーションして結果を返しているだけなのでしょうか?パースとバリデーションの正確な定義は議論があるものである一方で、私はparseNonEmptyが正真正銘のパーサーであると信じています(中でも特にシンプルなものではありますが)。「パーサーとは何か」考えてみてください。本当にパーサーとはただのより構造化されていない入力を消費して、より構造化された出力を生み出すだけのただの関数にすぎません。原理的に、パーサーは部分関数です。ドメイン内の一部の値は範囲内のどの値にも対応しません。そのため、すべてのパーサーはいくらかの失敗という概念を持たざるを得ません。パーサーの入力はテキストですが、必ずそうでなければならないということでは決してありません。parseNonEmptyは完全にまあまあなパーサーです。リストを空ではないリストにパースして失敗する時にはエラーを吐いてプログラムを終了します。柔軟な定義によってパーサーは信じられないほど強力なツールになりました。入力値のチェックをちょうどプログラムと外界の境目でチェックできるようになり、1度これを行えば全く再チェックしなくて良くなったのです!Haskellerはこの力をよく知っています。彼らはたくさんの種類のパーサーを日頃から使っているのです。JSONをパースしてドメインの型として使えるようにするパーサーを提供します。コマンドライン引数のためにいくつかのパーサーの組み合わせを扱えるようにするライブラリです。いったデータベースのライブラリは外部のデータストアが持っているデータをパースする機構を持っています。servantエコシステムはHaskellのデータタイプをパスコンポーネントやクエリパラメーター、HTTPヘッダーなどからパースする仕組みで作られています。これらのすべてのライブラリに共通するテーマは外界とHaskellアプリケーションの境界上にあるものだということです。それらは和型や積型ではなく、Byteのストリームであるため、なんらかのパースを行う必要がありません。データに対処する前にパースを行うことは、多くの種類の重大なセキュリティの問題を含むバグを回避することに役立ちます。1つこのすべての最初にパースするアプローチに欠点があるとすれば、しばしば値がパースされるのが実際に使われるよりも大幅に前になってしまうことです。このことは動的型付言語ではパースと処理のロジックに一貫性をもたせることを少し難しくします。莫大なテストカバレッジがあればこれらの問題を防ぐことができますが、ほとんどの場合はそのテストのメンテナンスに非常に手間がかかります。静的型付システムでは、問題が驚くほどシンプルになります。上記のNonEmptyの例で示された通りです。パースと処理のロジックに一貫性がない場合、そのプログラムはコンパイルすることすら失敗します。validation)願くば、この時点で、あなたは少なくともパースの方がバリデーションよりも好ましいという考え方に感化されていて欲しいですが、まだ、疑いをひきずっているかもしれません。型システムが必要なチャックを強制するとしたら、バリデーションは本当にそんなに悪いものなのでしょうか。するとエラーの見え方は少し悪くなるかもしれませんが、少しの冗長なチェックはそんなに辛くないですよね?残念なことに、話はそうシンプルではありません。その場その場でのバリデーションは言語理論的セキュリティ領域におけるショットガンパーシングという現象につながります。Themでは著者たちが下記のような定義をしています。ショットガンパーシングはパースおよび入力値のバリデーションコードを処理コードと混合して全体に分散させるプログラミングのアンチパターンです。つがすべての「悪い」ケースをキャッチすることを期待します。cases.彼らは続けてそのようなバリデーション手法に内在する問題について説明しています。ショットガンパーシングは必然的にプログラムから不正な入力を処理せずに拒否する能力を奪います。入力のストリームの中で遅くなってから発見されたエラーは結果として処理ずみの不正な入力の一部になるでしょう。その結果プログラムの状態を正確に予測することが難しくなります。predict.言い換えると、最初にすべての入力をパースしていないプログラムは、有効な入力に対して対処しているときに、異なる部分が不正であると発見し、どんな処理が実行済みであろうとも、一貫性を維持するために、突然巻き戻さなければならなくなるリスクにあります。時々(RDBMSのトランザクション処理などでは)こういったことは可能ですが、一般的にはそうではありませんショットガンパーシングがバリデーションとどういう関係にあるかは直ちに明らかになることはないかもしれませんが、結局のところ、すべてのバリデーションを最初にやっておけばショットガンパーシングのリスクを軽減することができます。問題は、バリデーションベースのアプローチは全てが実際に最初にバリデーションされたのか、もしくはいわゆる「起こり得ない」ケースが実際に起こるのかを確かめることを極端に難しくする、または不可能にすることです。プログラムの全体は例外がどこでも発生させることが起こりうるだけではなく、むしろ定期的に起こる必要があると想定する必要があります。パースはこの問題を2つのフェーズに階層分けすることで回避しています。この場合、不正な入力による失敗は最初のフェーズでしか起こり得ません。実行の中での残りの失敗は比べて最小であり最大限の注意を持ってそれらをケアすることができます。practice)今の所このブログポストは何かのセールスピッチです。「そこの読者のあなた、パースしないわけにはいきません!」と言っているような感じです。私がちゃんと仕事ができたのであれば、少なくとも読者のうちの何人かはこの考え方を購入してくれたでしょう。あなたが「何を」「どうして」やらないといけないのか理解できたとしても、「どうやって」実現すればいいのかまだ自信がないかもしれません。私のアドバイスは「データ型に集中しなさい」です。キーバリューペアのタプルのリストを受け付ける関数を書くことを考えてみるとあなたは不意に重複したキーをどうしたらいいかわからないことに気づきます。する関数を書くことです。()しかし、このチェックは危ういものです。簡単に忘れ去れれてしまうでしょう。その戻り値が使われておらず、削除することも常に可能で、それでも型検査は通るからです。良い解決法はMapと言った重複したキーを許可しないデータ構造を選ぶことです。普段あなたがするように、先ほどの関数をタプルのリストではなく、Mapを受け付けるように調整して実装しましょう。一度このようにすれば、この新しい関数の呼び出し側ではタプルのリストを渡しているので型検査は失敗するでしょう。それが呼び出し側の引数の一部から与えられた値であったり、他の関数の戻り値の一部だった場合に、呼び出しの連鎖を辿ってリストからMapに変換し続けることができます。最終的には、その値が作られた最初の場所に辿り着くか、重複が実際に許されるべきところを発見するでしょう。現時点では、修正されたバージョンのcheckNoDuplicateKeysを差し込むことができます。v)これでもう重複チェックは削除することができません。プログラムが実際に処理するのに必要な処理になったからです!この想定上のシナリオは以下の2つのシンプルなアイディアを強調します。不正な状態を表現できないようなデータ構造を使ってください。可能な限りもっとも厳格なデータ構造をモデリングしてください。現在使っているエンコーディングで特定の可能性を排除するのがあまりにも難しいのであれば、関心のある属性をより簡単に表現できるエンコーディングを検討してください。リファクタリングを恐れないで。証明する責任を可能な限り押し上げてください、ただしその範囲まで。可能な限り早い段階でデータを可能な限り最も厳密な表現にしてください。システムの境界で、そのデータに対してあらゆるアクションが取られる前に。あるコードの枝葉がデータをより厳格な表現にすることを求めた場合、その枝葉が選ばれた時すぐにそのデータをより厳格な表現にパースしてください。直和型を賢明に使い、データ型が制御フローを反映し、適応するようにしてください。[3]言い換えれば、関数を書くときに、与えられたデータではなく、欲しいデータの表現をするようにしてください。するとその設計プロセスは、両者が中間のどこかで落ち合うまで、両方の側から働きかけるギャップを埋める活動になります。インタラクティブに設計の部品を調整することを恐れないでください。そのリファクタリングのプロセスで何か学ぶことがあるかもしれないので!ここには手に余るほどの追加のアドバイスがあります。順番は特にありません。データ型がコードに情報を与えるようにして、コードがデータ型をコントロールすることがないようにしてください。今書いているコードを関数が必要だからという理由だけでレコードのどこかにBoolをくっつける誘惑を回避してください。リファクタリングを恐れず、正しいデータの表現をしてください。型システムは変更が必要な箇所をすべて検知して、後で頭痛に苦しめられないようにしてくれます。m()を返す関数は強い疑いを持って扱ってください。ときには、命令型的な副作用があり、意味のある結果を返さないために、これらが本当の意味で必要になることはあります。主たる目的が例外を投げることであれば、もっとより良い方法があるはずです。複数パスでデータをパースすることを恐れないでください。ショットガンパーシングを避けるというのはただ、パースされていないデータに対して処理を行ってはいけないというだけのことで、入力値の一部が、他の入力値のパース方法を変えてはいけないということではありません。多くの有用なパーサーは文脈依存です。非正規化されたデータ型を使うのを避けてください、"特に"それがミュータブルな時は。いう、表現可能であることが自明な不正な状態を作り出します。Truth(信頼できる唯一の情報源)を懸命に求めましょう。非正規化されたデータの表現を抽象境界の後ろ側にとどめておいてください。非正規化されたデータがどうしても必要であれば、カプセル化を使って、小さい、信用できるモジュールが唯一その表現に一貫性があることに責任を持つようにさせてください。バリデーターを抽象データ型を使ってパーサーに「見える」ようにしてください。ときに、は整数が特定の範囲にあることを保証など、不正な状態を本当の意味で表現できないようにするのHaskellが提供するツールを考えるとただただ非現実的です。そのときにはスマートコンストラクタで抽象newtypeを使って、バリデーターをパーサーとして「偽装」します。いつもあなたがそうしているように、最もいい判断をしてください。持ち出してアプリケーション全体を書き換えるのは割に合わないでしょう。それらの状況を放射線物質かのようにみなし、適切な処理をしてください。すべての他のことに失敗したら、少なくとも誰かは問わず次にコードを修正する必要がある人のために不変条件をドキュメントとして残すためにコメントを残してください。reading)以上です。願わくば、このブログポストがHaskellの型システムの利点を活かすのにPhDが必要なく、GHCの最新で最高の輝しい新しい言語拡張を利用する必要もないことを、これがただただ素晴らしいことを通して証明できれば幸いです。ときに、Haskellを最大限に使う上で最も大きな障害になるのは、単にどのようなことができるのかを認識することです。残念なことに、Haskellの小さなコミュニティはデザインパターンと暗黙値になってしまったテクニックをドキュメントにするリソースが不足しています。このブログ記事に書かれているアイデアは新しいものではありません。実際、核心となるアイデアである「全域関数を書く」というのは、概念的には非常に単純です。それにもかかわらず、私がHaskellのコードを書く方法について、実践的で具体的な詳細を伝えるのは驚くほど難しいと感じています。抽象的な概念について長時間話すのは簡単ですが(その多くは非常に価値があります!)、プロセスに関して役立つ情報を伝えることはできていないように感じます。私の希望としては、これはその方向に向けた小さな一歩となることです。これらのアイデアについて別の視点からアクセスしやすい解説や、別の具体例を見たい場合は、ぜひ読んでみることをお勧めします。Proofs」もお勧めです。この論文では、ここで説明したものよりもさらに複雑な不変条件を型システムで表現するためのいくつかのテクニックが概説されています。最後に付け加えたいのは、このブログ記事で説明したようなリファクタリングを行うのは、必ずしも簡単ではないということです。私が示した例はシンプルですが、現実はしばしばそれほど単純ではありません。型駆動設計に慣れている人でも、特定の不変条件を型システムで表現するのは本当に難しいことがあります。自分が思い通りに問題を解決できなかったとしても、それを個人的な失敗だとは考えないでください!このブログ記事で述べた原則は、目指すべき理想と考えてください。絶対に守るべき厳格な要件ではありません。大切なのは、挑戦し続けることです。厳密に言うと、Haskellでは「ボトム」(どんな値にも存在し得る構造)を無視しています。これらは他の言語のnullとは違い、「実際の」値ではありません。無限ループや例外を発生させる計算のようなものです。慣用的なHaskellでは、通常これらを避けようとします。したがって、これらが存在しないと仮定して推論することにも価値があります。私の言葉を鵜呑みにしないでください。Correct」が、その理由を納得させてくれるでしょう。実はData.List.NonEmptyはheadをすでに提供していますが、ただ説明のために実装しています。時には、Dos攻撃を避けるために、ユーザー入力をパースする前に何らかの認可を行う必要がありますが、それで問題ありません。認可は比較的小さな範囲で行われるはずであり、システムの状態に大きな変更を引き起こすことはないでしょう。↩︎GitHubで編集を提案まじまっちょ筋肉エンジニアです。qiitaもやってました。qiita.com/majimacchoバッジを贈って著者を応援しようバッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。原文のメッセージは「パースしろ。25日前ご指摘ありがとうございます。語順は変えていますが、意味は変わっていないので翻訳としては適切な範囲内かと思います。validateという言葉は元の記事の方でもスローガン的に使われており、日本語としての語感を大事にしたため上記になっています。