世界を動かす技術を、日本語で。

パーサーコンビネータ

2026年10月18日原文(t3.at)

概要

  • Parser型 は、文字列を解析して値と残りの文字列を返す関数型
  • pSeq や pMap などの関数で複雑なパーサーを合成可能
  • 解析の流れは「残り文字列」を次のパーサーに順次渡す方式
  • Fold や Unfold の考え方で、値の集約や展開も表現
  • pingコマンド出力 のような複雑なテキストも、パーサーで構造化可能

Parser型とパーサー合成の基本

  • Parser<T>型 は、(input: string) => [T, string] という関数型
    • 入力文字列を受け取り、 T型の値 と 残り文字列 のペアを返却
  • 例:pInteger("10 20 30") → [10, " 20 30"]
    • "10"を数値として抽出し、残りを返す
  • パーサーの合成 は、「残り文字列」を次のパーサーに渡すことで実現
    • 例:pHello → pIgnoreWhitespace → pInteger の順で解析
      • "Hello 100" → ["Hello", " 100"] → [" ", "100"] → [100, ""]
  • pSeq 関数で複数パーサーを直列合成
    • 例:pSeq(pHello, pIgnoreWhitespace, pInteger) は [string, null, number] のタプルを返す
    • const pHello100 = pSeq(...) のように合成可能
  • pMap でパース結果を任意のオブジェクト型などに変換
    • 例:pGreeting["Hello 100["Hello", null, 100]{ greeting: "Hello", who: 100 }pMap", ""]

パーサー実行例と流れ

  • "Hello 100" を pHello100 で解析
    • pHello100("Hello 100") → ["Hello", null, 100], ""
  • "10;20;30;" を pSeq(pNumber, pSemicolon) で逐次解析
    • 1回目: "10;[10, ";"]", "20;30;"
    • 2回目: "20;[20, ";"]", "30;"
    • 3回目: "30;[30, ";"]", ""
  • 各パーサーが 残り文字列 を次のパーサーに渡し、最終的に全体を解析

Fold・Unfoldによる値の集約と展開

  • Fold: リストの値を集約
    • 例:list.reduce((acc, n) => acc + n)
    • ["Hello", " ", 100] → "Hello 100"
  • Unfold: 値を構造化データに展開
    • 例:pSeq(...)("Hello 100") → ["Hello", " ", 100]

pingコマンド出力のパース例

  • pingコマンドの出力をパーサーで解析し、構造化データに変換
    • header、packets、statistics などのオブジェクトとして抽出
  • 例:
    • header: { targetIp: "1.1.1.1", ... }
    • packets: [{ byteCount: 64, from: "1.1.1.1", seq: 1, ... }, ...]
    • statistics: { packetsTransmitted: 8, packetsReceived: 7, ... }

まとめ

  • Parser型 を使うことで、文字列から構造化データへの変換を柔軟かつ再利用可能に設計
  • パーサー合成 ・ マッピング ・ Fold/Unfold の発想で、複雑なテキスト解析やデータ変換にも対応可能

Hackerたちの意見

あんまり良いスタートタイプじゃないね。type Parser = (input: string) => [T, string] ってことは、文字列を受け取ってTを返すってことだよね。パーサーはTを返すことがあるかも。この記事は「部分パーサー」って概念から始まって、そこから他の(非部分?)パーサーが作れるのかな?それとも、部分パーサーの本当の力が発揮されるのはここからで、チェーンを新しいパーサーとしてまとめるってこと?いや、区別はないみたい。部分パーサーを組み合わせると、結局部分パーサーになるし、フルパーサー(それが何を意味するかは別として)を組み合わせるとフルパーサーになる。皮肉なことに、「部分」の適切な定義がここにあるね:関数がすべての入力に対して明確に定義されてない。ある入力ではTが返されて、他の入力ではエラーが出るかも、誰が知ってるか…。

└

良い入力だね。TypeScriptで作業してるから、部分パーサーは常に例外を投げることができるよね(確かパーシモンライブラリもそうだったと思う)。あんまり技術的なことには深入りしないことにしたよ!

└

コンビネータは関数的だから、必ず何かを返すんだ。でも、何も返さなくてもいいなら、Tはどんな型でもOK。ユニットみたいな何かを返せばいいよ(つまり、値が何もないシングルトン型ね)。

└

部分パーサーを組み合わせると、結局部分パーサーになる。もし部分というのがパーサーが失敗する可能性を意味するなら、必ずしもそうとは限らないよ。optional(elem)やrepeat(elem)は、elemがエラーで入力を消費しないならトータルだし、少なくともパーサーコンビネータの合理的なセマンティクスではね(ただし、repeat(elem)でelemが空文字列にマッチするのはプログラマーのエラーと見なすことが多いけど)。

すごくクールなビジュアルガイドだね!ここで、フリッツ・ルーアーのパーサーについての小さな詩を紹介するのがいいと思う。これ、コンピュータサイエンスの最初の学期からずっと覚えてるやつ。「パーサーは、文字列から物のペアのリストへの関数だよ!」 https://people.willamette.edu/~fruehr/haskell/seuss.html

└

それを投稿しようとしてたところだった!でも、Googleが自分でドクター・スース風のパーサーコンビネーターの例を作り始めちゃった。あと、CSの最初の学期にこんなことに触れてたらよかったな。

なんでこれが1ページに収まらないのか理解できない。スライドショー形式だと新しい情報が入るたびに前の「章」に戻るのがすごく難しい。検索もできないし。デジタルメディアには存在しない制限を、変な美的理由で無駄に再導入してる感じがする。

└

最初は読むのを続けるのがちょっと億劫だったけど、頑張ってよかった!でも、どう進めばいいのかわからないのがイライラするよね。スクロールと進むのを行ったり来たりしなきゃいけないのが面倒だし…。でも、素晴らしいコンテンツがあるのに、ユーザー体験がそれを妨げてる気がする。

└

君の意見はわかるけど、俺は好きだな!各セグメントを分けることで、読みやすく感じるんだよね。理想的な世界では、こういう記事は「スライドショー」と「シングルページ」ビューを切り替えられるようにしてほしいな。だって、同じコンテンツがどちらでも使えるから。

クールなガイドだけど、パケットメタデータを任意のキー・バリューのペアとしてパースするチャンスを逃しちゃったね。例えば、64バイト from 1.1.1.1: icmp_seq=3 ttl=52 time=47.1 ms のコロン以降はキー・バリューのデータだから、pKeyVal()の一般的なパーサーを入れれば、アプローチのエレガントさがもっと際立つと思うよ。

最初のアドレスはIPアドレスとしてパースしない方がいいかも、ホスト名の可能性もあるからね。細かいことに思えるかもしれないけど、複数の種類の構造を期待する時にどうするかを示すチャンスを作るんだ。記事は、本文に割かれたスペースが限られていたせいで、ちょっと読みづらかった。短い章があったのに、何ページもスクロールしなきゃいけなかったよ。

このサイト、めっちゃクールだし、このテーマも大好き!でも、ちょっと小さい画面でテストしてみた方がいいかも。興味のあるコンテンツに対する画面スペースが限られてるし、右上の半分は余白が多いし、スクロール可能なコンテンツは実質的に画面の1/3しか使ってない。左側もまた1/3くらい。あなたのウェブサイト、あなたのビジョンはわかるし、UIで何か新しいことを試みてるのも尊重するけど、もしここでの目標がアイデアを伝えることなら、読者としてはもっとコンテンツにスペースが欲しいな。特定の投稿を見るためにリンクをクリックしたのに、他の投稿にはあんまり興味がないんだよね(少なくとも、読みたい投稿を読んでからじゃないと)。ちなみに、今はMacBook Airを使ってるよ。

Hacker Newsで議論の続きを見る