3 ポイント 投稿者 GN⁺ 2024-07-23 | 1件のコメント | WhatsAppで共有

パースし、検証するな

型駆動設計の本質

  • 型駆動設計(type-driven design)を説明するシンプルなスローガン: パースし、検証するな
  • このスローガンは、型システムを活用してコードの安全性と正確性を高める方法を意味する

可能性の領域

  • 静的型システムは、特定の関数が実装可能かどうかを簡単に判断できるようにしてくれる
  • 例: foo :: Integer -> Void は実装不可能(Void 型は値を持てない)
  • 例: head :: [a] -> a 関数は、リストが空の場合には定義されない

部分関数を全関数に変える

期待値の管理
  • head 関数はリストが空のときに値を返せないため、Maybe 型を使って Nothing を返せるようにする
  • しかし、これは利用時の煩雑さを招くことがある
期待を伝える
  • NonEmpty 型を使って空でないリストを表現することで、head 関数が常に値を返すことを保証できる
  • NonEmpty 型を使えば不要なチェックを取り除き、型システムによってエラーをコンパイル時に捉えられる

パースの力

  • パースと検証の違いは、情報をどのように保持するかにある
  • validateNonEmpty 関数はリストが空でないことを検証するが、その情報を保持しない
  • parseNonEmpty 関数はリストが空でないことを検証し、NonEmpty 型としてその情報を保持する

検証の危険性

  • 検証ベースのアプローチは、「shotgun parsing」と呼ばれる問題を引き起こす可能性がある
  • これは、プログラムが入力の一部を処理したあとで、残りの入力が無効だと判明する状況を招きうる
  • パースはプログラムを二段階に分け、第一段階で入力の妥当性を確認し、第二段階で妥当な入力だけを処理するようにする

実践におけるパース

  • データ型に集中し、関数の型シグネチャを可能な限り具体的にする
  • 不正な状態を表現できないデータ構造を使い、可能な限り早くデータを具体的な表現へ変換する
  • データ型がコードを導くようにし、コードがデータ型を支配しないようにする
  • m () を返す関数は慎重に使うべきである
  • 何度にも分けてデータをパースすることを恐れるべきではない
  • データの非正規化された表現を避け、必要な場合はカプセル化によって管理する
  • バリデータをパーサのように見せる抽象データ型を使うべきである

要約、振り返り、関連する読み物

  • Haskell の型システムを最大限に活用することは難しくなく、最新の言語拡張を使う必要もない
  • 中核となるアイデアは「全関数を書くこと」であり、これは単純だが実践は難しいことがある
  • 関連する読み物として、Matt Parson のブログ記事「Type Safety Back and Forth」と、Matt Noonan の論文「Ghosts of Departed Proofs」が勧められている

GN⁺ のまとめ

  • この記事は、Haskell の型システムを活用してコードの安全性と正確性を高める方法を説明している
  • パースと検証の違いを理解し、パースによって入力の妥当性を確認することが重要だと強調している
  • 型システムを活用し、不正な状態を表現できないデータ構造を使い、可能な限り早くデータを具体的な表現へ変換することが重要である
  • 関連する読み物として、Matt Parson のブログ記事と Matt Noonan の論文が勧められている

1件のコメント

 
GN⁺ 2024-07-23
Hacker Newsのコメント
  • この助言と記事はとても有益

  • 静的型付き関数型言語を使わない人にとっても有用

  • このアイデアはパラダイムを超越している

  • 80〜90年代のオブジェクト指向の文献にも似た概念が見られる。たとえば Design by Contract

  • TypeScript はランタイムで型を絞り込む形で書かれることが多い

  • Design by Contract は Clojure の spec に影響を与えたのかもしれない(Clojure は動的言語)

  • 基本的には、これは前提と保証に関する話(要求と提供)

  • 前提が確認され保証が成り立てば、プログラムの他の部分で重複した前提を再確認する必要はない

  • コード中で、すでに保証されている性質が再確認されているのを見ると混乱しやすく、コードの理解や改善を難しくする

  • このパターンは現代の C# でもうまく機能し、スペース節約の効果もある

    • 例:
      if(!Whatever.TryParse<Thingy>(input, out var output)) output = some-sane-default;
      
    • 例:
      if(!Whatever.TryParse<Thingy>(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
      
    • カーネルモードドライバーでは後者は使わないことを勧める
  • 強力な型システムを活用してエラーケースを表現不可能にするのがよく、それはソフトウェアバグの削減に役立つ

  • 問題を考え、設計に従うにはより時間がかかるが、多くの場合その時間には価値がある

  • 「Parse, don’t validate」というスローガンは型ベース設計をうまく要約している

  • 個人的には「常に単一のコンストラクタでのみバリデーションを行う」のがよい。そうすれば無効なオブジェクトがそもそも存在しなくなる

  • オブジェクトを変更するには、同じコンストラクタを再度呼び出して新しい状態を構成する形で実装すべき

  • qmail のセクション 5 を思い出す。そこには「パースするな」と「良いインターフェースとユーザーインターフェースがある」という話が含まれている

  • 中規模のプログラミング授業を教えるなら、学生にこの提案を比較対照するエッセイを書かせるだろう。それぞれの提案には学ぶべき点があり、最初は矛盾しているように見えるかもしれない

  • 関連資料: Richard Feldman の "Making Impossible States Impossible"

  • 過去の議論:

  • Crowdstrike に伝達済み

  • 2000年代半ばの XML ブームのときの誰かのコメントを思い出す。多くの組織が XML を選んだ理由は、XML がパーサーを提供してくれるからだというもの

  • パーサーを書くのは難しくもなく楽しいのに、人々がパーサーを書きたがらない理由が理解できない

  • これは Protocol Buffers の "required" キーワードが大きな失敗だったという意見と反対のことなのだろうかと気になる

  • 柔軟で未検証のパースと、検証済みのパース機能の両方を備えるのが最善だろう