Home About Contact
Parser Combinator , TypeScript , Deno

「改善版2024)Kotlin でパーサーコンビネータを実装する HtmlWriter の導入」のコードを TypeScript にする(その3)

その2の続きです。 今まではパーサーが letter, zeroOrMore しか用意していなかったので、 HelloWrold! などという中途半端な文字列をパースする例で説明していた。 今回は普通に Hello, World! 文字列をパースできるように、 one, and および seq パーサーを追加します。

環境

$ deno -version
deno 1.44.1

one パーサー

parser.ts に one パーサーを追加します。 ほとんど letter パーサーと同じですが、 letter がアルファベットの任意の1文字にマッチするパーサーに対して、 この one パーサーは指定した1文字にマッチします。

const one = <T>(toSomething: ToSomething<T>, moji: Moji): Parser<T>=> {
    const re = /[a-zA-Z]/

    const p = (ms: Moji[]): HtmlWriter<T> => {
        if( ms.length<1 ){
            return ngHtmlWriter(ms)
        } else {
            const m = head(ms)
            if( m.c == moji.c ){
                return okHtmlWriter(tail(ms), [toSomething(m)])
            } else {
                return ngHtmlWriter(ms)
            }
        }
    }

    return p
}

たとえば、コンマ1文字にマッチする commaParser の例:

const comma: Moji = { c: ',' }
const commaParser = one<HtmlBlock>(toJustHtmlBlock, comma)

and パーサー

次に and パーサーを用意します。

const and = <T>(p1: Parser<T>, p2: Parser<T>): Parser<T>=> {
    return (ms: Moji[]): HtmlWriter<T> => {
        const w1 = p1(ms)
        if( w1.r.ok ){
            const w2 = p2(w1.ms)
            if( w2.r.ok ) {
                return okHtmlWriter(w2.ms, w1.r.xs.concat(w2.r.xs))
            } else {
                return ngHtmlWriter(w1.ms)
            }
        } else {
            return ngHtmlWriter(w1.ms)
        }
    }
}

たとえば、コンマが2回連続する文字列にマッチするパーサーであれば、次のようにコードします。

const p = and<HtmlBlock>( commaParser, commaParser )

試しに one と and を使う

この段階で Hello, World! 文字列をパースするパーサーを書いてみます。

const word = zeroOrMore( letter<HtmlBlock>(toJustHtmlBlock) )
const comma = one<HtmlBlock>( toJustHtmlBlock, {c: ','} )
const space = one<HtmlBlock>( toJustHtmlBlock, {c: ' '} )
const exclamation = one<HtmlBlock>( toJustHtmlBlock, {c: '!'} )

const p = and( and( word, comma ), space )

この p パーサーで Hello, World! 文字列をパースすると次のようになります。

$ deno run --check main.ts
{
  ms: [
    { c: "W" },
    { c: "o" },
    { c: "r" },
    { c: "l" },
    { c: "d" },
    { c: "!" }
  ],
  r: {
    ok: true,
    xs: [
      { m: { c: "H" } },
      { m: { c: "e" } },
      { m: { c: "l" } },
      { m: { c: "l" } },
      { m: { c: "o" } },
      { m: { c: "," } },
      { m: { c: " " } }
    ]
  },
  parse: [Function: parse]
}

r.xs にパースできた Moji[] が出力されています。 Hello と コンマとスペースまでパースできていることがわかります。 このまま最後までパースできるパーサーを書くとこうなります。

const p = and( and( and( and( word, comma ), space ), word), exclamation)

これで機能するにはするのですが、とても読みづらい上に もし変更が入ったら、もう書きかえる気にもなりません。

そこで seq パーサーを導入します。 もし seq パーサーがあれば、このパーサーは次のように書くことができます。

const p = seq( [word, comma, space, word, exclamation] )

seq パーサー

const seq = <T>(parsers: Parser<T>[]): Parser<T> => {
    if( parsers.length<1 ){
        return (ms: Moji[]): HtmlWriter<T> => {
            return okHtmlWriter<T>(ms, [])
        }
    } else if( parsers.length==1 ){
        return parsers[0]
    } else {
        const initValue = parsers[0]
        return parsers.slice(1).reduce( (acc, p)=> {
            return and(acc, p)
        }, initValue )
    }
}

与えられたパーサーが0個の場合と1個の場合は特別に対処しています。 2個以上のパーサーがきた場合は、 and を利用してパーサーを組み立てます。

one, and, seq をエクスポート

ここまでで parser.ts に one, and, seq のパーサーを追加したので、これをエクスポートに追記します。

// parser.ts

export type { Moji, Parser, ParseResult, HtmlWriter, ToSomething }
export { toMojiList, parseResult, htmlWriter, letter, one, and, seq, zeroOrMore }

main.ts で使う

// main.ts

import { Moji, toMojiList } from "./moji.ts"
import { Parser, ParseResult, HtmlWriter, ToSomething, parseResult, htmlWriter, letter, one, seq, zeroOrMore, and } from "./parser.ts"

type JustHtmlBlock = { m: Moji }
type NothingHtmlBlock = {}
type HtmlBlock = JustHtmlBlock | NothingHtmlBlock


const text = "Hello, World!"

const toJustHtmlBlock: ToSomething<HtmlBlock> = (m: Moji): HtmlBlock => {
    return { m: m }
}

const word = zeroOrMore( letter<HtmlBlock>(toJustHtmlBlock) )
const comma = one<HtmlBlock>( toJustHtmlBlock, {c: ','} )
const space = one<HtmlBlock>( toJustHtmlBlock, {c: ' '} )
const exclamation = one<HtmlBlock>( toJustHtmlBlock, {c: '!'} )

//const p = and( and( word, comma ), space )
//const p = and( and( and( and( word, comma ), space ), word), exclamation)

const p = seq<HtmlBlock>( [word, comma, space, word, exclamation] )

const mojiList = toMojiList(text)
const initParseResult = parseResult<HtmlBlock>(true, [])
const result = htmlWriter(mojiList, initParseResult).parse( p )
console.log(result)

実行してみます。

$ deno run --check main.ts
Check file:///home/moca/1021-parser/parser-ts/step2/index.ts
{
  ms: [],
  r: {
    ok: true,
    xs: [
      { m: { c: "H" } },
      { m: { c: "e" } },
      { m: { c: "l" } },
      { m: { c: "l" } },
      { m: { c: "o" } },
      { m: { c: "," } },
      { m: { c: " " } },
      { m: { c: "W" } },
      { m: { c: "o" } },
      { m: { c: "r" } },
      { m: { c: "l" } },
      { m: { c: "d" } },
      { m: { c: "!" } }
    ]
  },
  parse: [Function: parse]
}

意図通り最後までパースできました。