Haskell: Harrothチュートリアルを読みながら実装したForthパーサ

Pugsの勉強をしようと考えている方は、以下のリソースを読むと面白いかもしれません。

Harrothチュートリアルでは、Forthというプログラミング言語のパーサをHaskellで実装しながら、パーサについての理解が深められるように書かれています。しかし残念な点が2点あります。第一に、説明で出てくるコードと実際のソースコードに若干の違いがあり、読者の理解を妨げていることです。*1第二に、パーサの核になる部分のコードが欠落していることです。よく書けているチュートリアルなだけに、本当に残念でした。

あと少しのピースがあれば、完成しそうだったので、代わりにそのピースを作ってみました。何回も試行錯誤してパーサが完成した時は、本当に感動しました。

そのパーサのコードは以下の通りです。

% vim MyForth.hs
{-
MyForth - An Implementation of Forth using Haskell (parser only)

This code is based on the Harroth tutorial. You can find that tutorial:
http://www.perlcabal.org/~nothingmuch/harrorth/doc/

* Usage
    % ghci MyForth.hs
    *MyForth> parseTest forthProgram "2 foo : bar 4 blah ; ding"
    [Push 2,Invoke "foo",NewWord "bar" [Push 4,Invoke "blah"],Invoke "ding"]
-}

module MyForth where

import Text.ParserCombinators.Parsec

-- AST
type Forth = [Exp]

data Exp
    = Push Literal
    | Invoke Word
    | NewWord Word Forth
    deriving Show

type Literal = Integer
type Word    = String

-- Parser
forthProgram :: Parser Forth
forthProgram = do
    ast <- forth
    eof
    return ast

forth :: Parser Forth
forth = sepEndBy forthExp sep

forthExp :: Parser Exp
forthExp =   do {lit <- literal; return $ lit}
         <|> do {nw  <- newWord; return $ nw}
         <|> do {w   <- word   ; return $ w}

literal :: Parser Exp
literal = do
    lit <- many1 digit
    return $ Push (read lit)

newWord :: Parser Exp
newWord = do
    char ':'
    maybeSep
    name <- wordName
    sep
    body <- forth
    maybeSep
    char ';'
    return $ NewWord name body

word :: Parser Exp
word = do
    name <- wordName
    return $ Invoke name

wordName :: Parser String
wordName = do
    name <- many1 letter
    return name

sep :: Parser ()
sep = skipMany1 space

maybeSep :: Parser ()
maybeSep = skipMany space

一番困ったのは、forthExp関数の定義がどこにも書かれていなかったことです。ヒントになったのは、以下の説明とParsecのマニュアルです。それらを読んで何とか復元できました。

`forthExp` is of type `Parser Exp`, meaning that it returns `Exp`s in the parser monad. Recall that an `Exp` is either a definition of a word, a literal, or a word. `forthExp` uses Parsec's `<|>` operator to denote alternatives.

http://www.perlcabal.org/~nothingmuch/harrorth/doc/03.kwid より

ghciで実行してやると、きちんと動作していることがわかります。ちなみにparseTest関数はパーサをテストするための関数で、Parsecで定義されています。

% ghci MyForth.hs
*MyForth> parseTest forthProgram "2 foo : bar 4 blah ; ding"
[Push 2,Invoke "foo",NewWord "bar" [Push 4,Invoke "blah"],Invoke "ding"]

少しずつですが、Haskellの感覚がわかってきたような気がします。頑張るぞ。

*1:最終形体のソースコードだけじゃなくて、rehaul前の古いソースコードも残っていれば良かったのですが...