e-mon

備忘録

HaskellでSchemeを書く#2

構文解析その2

前回は、基本的な関数の表現などをまとめた。 今回も続けて構文解析について進めていく。 既に10時間は経過している気がするけど、48時間で終わるのか!

何はともあれ、まずは以下の定義から。

data LispVal = Atom String
             | List [LispVal]
             | DottedList [LispVal] LispVal
             | Number Integer
             | String String
             | Bool Bool

これは代数的データ型と呼ばれるものらしく、恐らくEither型も代数的データ型なのかな?|で接続された、例えばAtomみたいな部分がコンストラクタのタグ名で、その後に続いているのがそのコンストラクタに格納可能であるデータ型、ということだった。

代数的データ型とはなんぞや?ということでwikiで調べてみると、以下のような素晴らしい例が出てきた。

Prelude> data Node = Leaf Integer | Branch Node Node deriving (Show)
Prelude> Leaf 1
Leaf 1
Prelude> Branch (Branch (Leaf 1) (Leaf 2)) (Branch (Leaf 3) (Leaf 4))
Branch (Branch (Leaf 1) (Leaf 2)) (Branch (Leaf 3) (Leaf 4))
Prelude> 

なんかBNF記法的な何かだと理解した。いろんな表現の集合。いずれかのコンストラクタで表されているもの。

問題は、それぞれが何を意味しているか。NumberStringBoolはいいとして、問題はそれ以外の3つだ。

Schemeでの表現は、全てS式と呼ばれるもので、そのS式を構成しているのが、リストと、リスト以外のモノを指すアトム(参考:Schemeのお勉強 (S式と評価,式と文,リテラル))。Listは、Haskellでのデータ型表現が[LispVal]とLispValのリストになっているので、 そういうことなんだろう。

Schemeでは、リストとはcons(construct語源?)セルによって構成されるらしい。 (wiki:Cons)

(1 . 2)が最小単位のコンスセルで、ドット対の S式、ということだ・・・また知らない単語が出てきてしまったので調べると、まあ単純にこの形式のデータのことをそう呼ぶらしい。コンスセルでドット対じゃあないものが存在するということか?と思ったけどそういうことでもないらしい。じゃあコンスセルでいいやん。コンスセルの表現方法としてLispではドット対という、と。まあそういうことらしい。

ふと疑問に思ったのが、このLispValをリストとして持つと、リストの中の型がコンストラクタによってぐちゃぐちゃになっちゃうんじゃ?と思ったんだけど、リストには'純リスト'と'純でないリスト'というのがあるらしく、純リストは、並べられたデータの型を制限しないリストのことらしい。凄い。

そんなこんなで、次がDottedList [LispVal] LispValなんだけども、これってDottedList List LispValという書き方じゃダメなんだろうか。ダメだった。type constructor でも classでもないからダメよ、というふうに見える。

Prelude> data LispVal = Atom String | List [LispVal] | DottedList List LispVal

<interactive>:118:58:
    Not in scope: type constructor or class `List'
    A data constructor of that name is in scope; did you mean -XDataKinds?

この代数的データ型にはそれぞれ呼び方があるらしく、data 型コンストラクタ(type constuctor) = 値コンストラクタ(data constructor) データ型という並びになっているらしい(参考:Haskellのコンストラクタとデータ型)。値コンストラクタはさだめられたデータ型を返す関数なんだって。

DottedListは、ドット対で表現されたリストのことかな?だから、Listのほうは、(1 2 3 4)みたいなやつのことか。

残るはAtomだけども、こいつは何を指しているんだろう。式そのものを一時的に文字列で受け取るためのものだろうか。

文字列をパース

少し疑問は残ったものの次へ。次は、文字列として表現されるものをパースする。ここも難所だった・・。

parseString :: Parser LispVal
parseString = do char '"'
                 x <- many (noneOf "\"")
                 char '"'
                 return $ String x

2行目のdoは各行を適切に>>>>=で接続しますよーってなことらしい。そんでcharは、

Prelude Text.ParserCombinators.Parsec> :t char
char
  :: Text.Parsec.Prim.Stream s m Char =>
     Char -> Text.Parsec.Prim.ParsecT s u m Char

こんな風に定義されてて、与えられたChar型の文字とマッチしたらポインタを一個進めて文字列を次に渡す、とかなのかなぁ。この辺、関数の定義がわからない分具体的になにしてるのかわからん。manyは次のnoneOfと併せて、"が出てこない間の文字列にマッチさせる。最終的にreturnするのだけれど、ここでreturnが必要な理由は、manyが返してくるデータ型はStringなので、この関数のデータ型であるParse LispValとしてまとめあげるためにreturnしているらしい。まだ深淵に触れたくないのでさらっと見てみると、

class Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  return :: a -> m a

あるaをモナドとして返してくれる関数?らしい。LispValはParserモナドじゃないから、全体としてParserモナドのアクションになるようにしましょうね、ということかしら。

Atomのパース

ついに出ました、謎のAtom

parseAtom :: Parser LispVal
parseAtom = do first <- letter <|> symbol
               rest <- many (letter <|> digit <|> symbol)
               let atom = first:rest
               return $ case atom of
                          "#t" -> Bool True
                          "#f" -> Bool False
                          _    -> Atom atom

<|>が目新しいくらい?letterは大文字か小文字の英字をパースしてくれるやつで、これは1文字目が英字もしくはsymbolに指定した記号始まりで、その後に続く文字列が、英字、記号、数字のいずれかであることを判定、つまりはアトムであるかを見ているわけだな?で、真偽値だったらBoolコンストラクタTrue or False として格納してそれ以外はAtomとして格納すると。だいぶ読めるようになってきたぞ?

数字のパーサ

数字のパーサは見た目がとてもシンプル。ただやってることは難しい。

parseNumber :: Parser LispVal
parseNumber = liftM (Number . read) $ many1 digit

これは(Number . read) という、String -> LispValというふうに合成された関数を、many1 digit関数から返される値に適用したいんだけども、many1 digitParse Stringという、Monad Stringな値を持ってしまっているので、そのままでは適用できないと。となると、String -> LispValの関数を、Monad String -> Monad LispValという関数にしたい・・・・・!ということでそれを担うのがliftMということですね・・。実際に、

Prelude Control.Monad> :t (Number . read)
(Number . read) :: String -> LispVal
Prelude Control.Monad> :t liftM (Number . read)
liftM(Number . read) :: Monad m => m String -> m LispVal

こんな感じになる。

あとはこれらのパーサを組み合わせてひとまずは完了!!