このエントリーは 改善版2024)kotlin でパーサーコンビネータを実装する【追伸】 の続きです。
この方法を使ってパースした場合、 最後にHTMLを出力する段階で HtmlBlock をまとめるコードがあるのですが、 やっていることは同じ種類の HtmlBlock を一つにしているだけです。 しかし、 HtmlBlock を sealed class として用意しているので、それを まとめる だけの操作をするのにもこれだけのコードを書く必要があります。
このように:
val createHtmlBlock: (HtmlBlock, String)->HtmlBlock = { htmlBlock, value->
when(htmlBlock){
is HtmlBlock.Just -> HtmlBlock.Just(value)
is HtmlBlock.Italic -> HtmlBlock.Italic(value)
is HtmlBlock.Bold -> HtmlBlock.Bold(value)
is HtmlBlock.Nothing-> HtmlBlock.Nothing
}
}
... listOf(createHtmlBlock(accLast,(accLast.value + htmlBlock.value))) ...
HtmlBlock の種類が増えたら面倒すぎる。
直感的に言えば、二つの HtmlBlock を(同じ種類だったら)その内容を足し合わせることができる演算子を HtmlBlock に定義できればよい。
そこで sealed クラスとして HtmlBlock を表現するのをあきらめて普通のデータクラスにして、 代わりに BlockKind という enum class を使うことにしてみた。 このように:
enum class BlockKind { JUST, ITALIC, BOLD, NOTHING }
data class HtmlBlock(val kind: BlockKind, val value: String)
さらに、このHtmlBlock の2つを引数にとって同じ種類だったら内容を一つにまとめる関数 concatTwoHtmlBlocks を作成:
val concatTwoHtmlBlocks: (HtmlBlock, HtmlBlock)->List<HtmlBlock> = { a, b->
if( a.kind == b.kind ){
listOf(HtmlBlock(a.kind, a.value + b.value))
} else {
listOf(a,b)
}
}
簡単に扱えるように `+` を infix で定義:
infix fun HtmlBlock.`+`(htmlBlock1: HtmlBlock): List<HtmlBlock> {
val htmlBlock0 = this
return concatTwoHtmlBlocks(htmlBlock0, htmlBlock1)
}
種類が異なる場合は足さないので、単なる + 演算でもないのだが、 演算子名でよいものが思いつかなかったので。
これなら次のように HtmlBlock をまとめる処理が記述できる。
val a = HtmlBlock(BlockKind.JUST, "a")
val b = HtmlBlock(BlockKind.JUST, "b")
val c = HtmlBlock(BlockKind.ITALIC, "c")
println( a `+` b ) // => [HtmlBlock(kind=JUST, value=ab)]
println( a `+` c ) // => [HtmlBlock(kind=JUST, value=a), HtmlBlock(kind=ITALIC, value=c)]
そもそもここまでやるくらいだったら HtmlBlock の value を val ではなく var にして value を変更可能にしてしまえば 済むという話もなくはない。 var にすることで data class にはできなくなるが。 そのほか、この方法は事前に BlockKind が確定していなければいけないため、 パーサー部分だけをライブラリにしたい場合は、困るだろう。 その場合は AbstractHtmlBlock などを導入すればなんとかなるだろう。(割愛)
これを使ってパーサー部分を記述すれば、こうなります:
val text = "Hello, **World**! Hello, *Again*!"
val toNothing: ToHtmlBlock = { HtmlBlock(BlockKind.NOTHING, "") }
val toItalic: ToHtmlBlock = { HtmlBlock(BlockKind.ITALIC, it) }
val toBold: ToHtmlBlock = { HtmlBlock(BlockKind.BOLD, it) }
val toJust: ToHtmlBlock = { HtmlBlock(BlockKind.JUST, it) }
val pItalicStart: Parser = pWord("*", toNothing)
val pItalicStop: Parser = pWord("*", toNothing)
val pBoldStart: Parser = pWord("**", toNothing)
val pBoldStop: Parser = pWord("**", toNothing)
val pItalic =
pItalicStart and
zeroOrMore(pNone("*", toItalic)) and
pItalicStop
val pBold =
pBoldStart and
zeroOrMore(pNone("**", toBold)) and
pBoldStop
val pJust = pAnyone( toJust )
val p = zeroOrMore(pBold or pItalic or pJust)
val parseResult = p(text, listOf())
println( toHtml(parseResult) )
sealed クラス使わなくてもいいのかなこれで。
完成したコードを掲載します。
// main.kts
enum class BlockKind { JUST, ITALIC, BOLD, NOTHING }
data class HtmlBlock(val kind: BlockKind, val value: String)
typealias ToHtmlBlock = (String)->HtmlBlock
data class ParseResult(
val ok: Boolean,
val next: String,
val htmlBlocks: List<HtmlBlock>)
typealias Parser = (String, List<HtmlBlock>)->ParseResult
val concatTwoHtmlBlocks: (HtmlBlock, HtmlBlock)->List<HtmlBlock> = { a, b->
if( a.kind == b.kind ){
listOf(HtmlBlock(a.kind, a.value + b.value))
} else {
listOf(a,b)
}
}
infix fun HtmlBlock.`+`(htmlBlock1: HtmlBlock): List<HtmlBlock> {
val htmlBlock0 = this
return concatTwoHtmlBlocks(htmlBlock0, htmlBlock1)
}
val toNGParseResult: (String)->ParseResult = { next->
ParseResult(false, text, listOf())
}
val pWord: (String, ToHtmlBlock)-> Parser = { word, toHtmlBlock->
val p: Parser = { text, htmlBlocks->
val invalid = ( text.length < word.length )
if( !invalid && text.substring(0, word.length)==word ){
ParseResult(
true,
text.substring(word.length),
htmlBlocks + listOf(toHtmlBlock(word)))
} else {
toNGParseResult(text)
}
}
p
}
infix fun Parser.and(parser1: Parser): Parser {
val parser0 = this
val p: Parser = { text, htmlBlocks->
val parseResult0 = parser0(text, htmlBlocks)
if( parseResult0.ok ){
val parseResult1 = parser1(parseResult0.next, parseResult0.htmlBlocks)
if( parseResult1.ok ){
ParseResult(true, parseResult1.next, parseResult1.htmlBlocks)
} else {
toNGParseResult(text)
}
} else {
toNGParseResult(text)
}
}
return p
}
infix fun Parser.or(parser1: Parser): Parser {
val parser0 = this
val p: Parser = { text, htmlBlocks->
val parseResult0 = parser0(text, htmlBlocks)
val parseResult1 = parser1(text, htmlBlocks)
if( parseResult0.ok ){
parseResult0
} else if( parseResult1.ok ){
parseResult1
} else {
toNGParseResult(text)
}
}
return p
}
val zeroOrMore: (Parser)->Parser = { parser->
val p: Parser = { text, htmlBlocks->
val parseResult = parser(text, htmlBlocks)
if( !parseResult.ok ){
// パースが失敗した場合:
ParseResult(true, text, htmlBlocks)
} else {
// パースが成功した場合:
zeroOrMore(parser)(parseResult.next, parseResult.htmlBlocks)
}
}
p
}
val pAnyone: (ToHtmlBlock)->Parser = { toHtmlBlock->
val p: Parser = { text, htmlBlocks->
if( text.length>0 ){
ParseResult(
true,
text.substring(1),
htmlBlocks + listOf(toHtmlBlock(text[0].toString())))
} else {
toNGParseResult(text)
}
}
p
}
val pNone: (String, ToHtmlBlock)->Parser = { token, toHtmlBlock->
val p: Parser = { text, htmlBlocks->
if( text.startsWith(token) ){
toNGParseResult(text)
} else {
if( text.length>0 ){
ParseResult(
true,
text.substring(1),
htmlBlocks + listOf(toHtmlBlock(text[0].toString())))
} else {
toNGParseResult(text)
}
}
}
p
}
val fixHtmlBlocks: (List<HtmlBlock>)->List<HtmlBlock> = { htmlBlocks->
htmlBlocks.fold(listOf<HtmlBlock>()) { acc, htmlBlock->
if( acc.size==0 ){
acc + listOf(htmlBlock)
} else {
val accLast = acc.last()
acc.dropLast(1) + (accLast `+` htmlBlock)
}
}
}
val toHtml: (ParseResult)->String = { r->
if( r.ok ){
fixHtmlBlocks(r.htmlBlocks).map {
when(it.kind){
BlockKind.JUST -> it.value
BlockKind.ITALIC -> "<i>${it.value}</i>"
BlockKind.BOLD -> "<b>${it.value}</b>"
BlockKind.NOTHING -> ""
}
}.joinToString("")
} else {
""
}
}
val text = "Hello, **World**! Hello, *Again*!"
val toNothing: ToHtmlBlock = { HtmlBlock(BlockKind.NOTHING, "") }
val toItalic: ToHtmlBlock = { HtmlBlock(BlockKind.ITALIC, it) }
val toBold: ToHtmlBlock = { HtmlBlock(BlockKind.BOLD, it) }
val toJust: ToHtmlBlock = { HtmlBlock(BlockKind.JUST, it) }
val pItalicStart: Parser = pWord("*", toNothing)
val pItalicStop: Parser = pWord("*", toNothing)
val pBoldStart: Parser = pWord("**", toNothing)
val pBoldStop: Parser = pWord("**", toNothing)
val pItalic =
pItalicStart and
zeroOrMore(pNone("*", toItalic)) and
pItalicStop
val pBold =
pBoldStart and
zeroOrMore(pNone("**", toBold)) and
pBoldStop
val pJust = pAnyone( toJust )
val p = zeroOrMore(pBold or pItalic or pJust)
val parseResult = p(text, listOf())
println( toHtml(parseResult) )
以上です。