無駄と文化

実用的ブログ

Haskell でλ計算のインタプリタを作っていこう ~ その3. プリティプリンタ

f:id:todays_mitsui:20171008025323p:plain


引き続き Mogul という名前のλ計算インタプリタを作っていこうと思います。
前回、パーザを書いたのでソースコードから抽象構文木を生成することができるようになりました。さっそく抽象構文木をいじくり回して何かしらの処理を実装したいところですが、今回はその前の下準備です。

ソースコード全体は GitHub に置いておきます。

github.com


抽象構文木は読みづらい

当たり前なんですが、素の抽象構文木は読みづらいです。例えば ^f x y.``fyx の抽象構文木はこれ。

Ident "f":^ (Ident "x" :^ (Ident "y" :^ (Var (Ident "f") :$ Var (Ident"y")):$ Var (Ident "x")))

実際、内部的にこのようなデータが扱われているとはいっても、デバッグ時にこれをそのまま読みたくない感じはじますよね。
というわけで前回、パーザを書いて文字列から抽象構文木を作ったので、今回は逆をやりましょう。プリティプリンタで抽象構文木を文字列に変換します。


プリティプリンタの要件

と云ってもそこまで難しいことをするつもりはありません。とにかく 文字列化 ができればいいだけです。 Mogul で扱っているいろいろなデータ型を良い感じに文字列化する pp :: a -> String を実装します。

関数 pp に求められる数少ない要件は以下の通り。

  1. Expr, Func など様々なデータ型に対して動くようにする
  2. 文字列化した後、再度 抽象構文木にパースすることが可能な、正しい Mogul コードを生成する
  3. 余計なスペースを省いてコードの密度を高く保つ

というわけでやっていきましょう。


PPrintable クラス

関数 pp で様々なデータ型を文字列化したいので、型クラス PPrintable を定義して pp をそのクラスのメソッドにします。

class PPrintable a where
    prepara :: a -> [Token]
    pp      :: a -> String
    pp = render . prepara

この PPrintable クラス、なんだが Show クラスに似ていますよね。実際、役割はほぼ同じです。
ghci などでカジュアルにデータ型を表示するために show の動作はそのまま残しておきたいので、あえて Show インスタンスの独自定義はせずに別のクラスを用意します。


もう一つの特徴として prepara :: a -> [Token] によって一度データ型をトークンの列に変換している点が挙げられます。
Mogul の Expr 型は二分木に似た構造ですが、それをまず平坦な Token の列に変換します。そうすることで隣のトークンを読んで適切な位置にスペースを挿入することがやりやすくなります。

Token 型の定義はこんな感じ、

data Token = Backquote         -- "`"
           | Lambda            -- "^"
           | Dot               -- "."
           | Equal             -- "="
           | Symbol Text       -- 英小文字1文字からなるシンボル
           | LargeSymbol Text  -- 英数字2文字以上からなるシンボル
           | EOL               -- 行端
  deriving (Eq)

instance Show Token where
    show Backquote       = "`"
    show Lambda          = "^"
    show Dot             = "."
    show Equal           = "="
    show (Symbol s)      = unpack s
    show (LargeSymbol s) = unpack s
    show EOL             = "\n"


さて、 PPrintable クラスのメソッドの中でも pp にはデフォルトの定義があります。

pp = render . prepara

まず prepara :: PPrintable a => a -> [Token] を使ってトークンの列に変換して、その後に render :: [Token] -> String を使って文字列に変換します。
render の実装は以下のとおり、

render :: [Token] -> String
render []       = ""
render (s@(LargeSymbol _) : ps@(LargeSymbol _ : _))
                = show s <> " " <> render ps
render (s@(Symbol _) : ps@(LargeSymbol _ : _))
                = show s <> " " <> render ps
render (p : ps) = show p <> render ps

LargeSymbolLargeSymbol が並んでいるとき、または SymbolLargeSymbol が並んでいるときに限り間に1つのスペースを挿入しながら文字列として結合します。
それ以外の場合はただトークンを文字列に変換しながら並べていくだけです。


PPrintable クラスのインスタンス宣言

ではそれぞれのデータ型を PPrintable クラスのインスタンスにしていきます。対象となるのは Ident, Expr, Func, (Ident, Func), Context です。

{-# LANGUAGE FlexibleInstances #-}

instance PPrintable Ident where
    prepara = (:[]) . symbol

instance PPrintable Expr where
    prepara = flatten []

instance PPrintable Func where
    prepara = prepara . body

instance PPrintable (Ident, Func) where
    prepara (v, f) = symbol v : Equal : prepara f

instance PPrintable Context where
    prepara = Data.Map.foldrWithKey (\v f cont -> prepara (v, f) <> (EOL : cont)) []

PPrintable (Ident, Func) に対するインスタンス宣言をする際には FlexibleInstances 拡張 を有効にする必要があります。

補助関数 symbol, body, flatten の定義は以下の通り、

-- | symbol (Ident "x"  ) => Symbol "x"
-- | symbol (Ident "FOO") => LargeSymbol "FOO"
symbol :: Ident -> Token
symbol v@(Ident x)
    | isUniIdent v = Symbol x
    | otherwise    = LargeSymbol x

-- | body (Func [Ident "x", Ident "y"] (Var (Ident "y") :$ Var (Ident "x")))
-- |   => Ident "x" :^ Ident "y" :^ Var (Ident "y") :$ Var (Ident "x")
body :: Func -> Expr
body (Func vs e) = foldr (:^) e vs

flatten :: [Token] -> Expr -> [Token]
flatten acc (e :$ e') = Backquote : flatten (flatten acc e') e
flatten acc (x :^ e)  = Lambda : symbol x : Dot : flatten acc e
flatten acc (Var x)   = symbol x : acc

大きな木構造を平坦なリストに変換するという意味で flatten は特に重要です。第1引数はただのアキュムレータなので余り気にする必要はありません。 flatten [] :: Expr -> [Token] をトークンの列を得る目的で使っています。
Expr 以外のデータ型は再帰も含まないシンプルなものなので、よしなにトークン列に変換してあげれば大丈夫でしょう。


使ってみる

という訳で pp を使っていくつかの式を文字列化してみます。

ghci> let Right e = parse expr "" "^xyz.``xz`yz"
ghci> e
Ident "x" :^ (Ident "y" :^ (Ident "z" :^ (Var (Ident "x") :$ Var (Ident "z")) :$ (Var (Ident "y") :$ Var (Ident "z"))))
ghci> pp e
"^x.^y.^z.``xz`yz"

ghci> Right e = parse expr "" "``x `FOO BAR y"
gchi> e
(Var (Ident "x") :$ (Var (Ident "FOO") :$ Var (Ident "BAR"))) :$ Var (Ident "y")
ghci> pp e
"``x`FOO BARy"

ghci> let Right e = parse def "" "````Jxyzw = ``xy``xwz"
ghci> e
(Ident "J",Func {args = [Ident "x",Ident "y",Ident "z",Ident "w"], bareExpr = (Var (Ident "x") :$ Var (Ident "y")) :$ ((Var (Ident "x") :$ Var (Ident "w")) :$ Var (Ident "z"))})
ghci> pp e
"J=^x.^y.^z.^w.``xy``xwz"

pp のおかげで確実に可読性が向上いていますね。良い感じです。


まとめ

というわけで可読性確保のために、云うほど pretty でもないプリティプリンタを書きました。
次回は Expr 型に ド・ブラン・インデックス の概念を取り入れてみようかと思います。


私からは以上です。