読者です 読者をやめる 読者になる 読者になる

プログラミングの実験場

Haskell/Webアプリ/画像処理/可視化/ITによる生産性向上 など

HaskellでASTからのコード生成(1) Writer+Stateモナドによる構造化

前のポストでGADTを使って型付きの抽象構文木(AST)を表現する方法について書いたが、ここではそのASTから他の言語のコード生成する方法について調査・検討した結果を記す。@keigoiさんの記事(http://d.hatena.ne.jp/keigoi/20111206/haskell_tagless_dsl)に触発され、一部借用したものである。なお、今回のコード生成の方法論はASTの型付き・型なしの区別にかかわらず使える。

MatlabのDSL in Haskell

今回例として扱うのは、Matlab(行列計算言語・統合環境)の行列計算を表現するようなDSL。最小構成として、2次元行列の加減乗算と要素の取り出しを表現したい。(Matlabの文法の簡単な説明はこれなどを参照。 http://www.math.meiji.ac.jp/~mk/labo/text/matlab/node4.html)これだけの小さなものでも、

  • 変数の管理
  • let-sharing(別の記事で記述予定)

といった、コード生成のエッセンスが含まれている。

まず、構文木を表すデータ型Exp rはこのような感じになる。

import Data.Text (Text)
import qualified Data.Text as T

type Ident = Text

-- 2D matrix
data Mat2D r = Mat Ident | MatConst [[r]]

class Scalar a
instance Scalar Int
instance Scalar Integer
instance Scalar Double

data Exp r where
	Const :: r -> Exp r
	Add :: Exp r -> Exp r -> Exp r
	Sub :: Exp r -> Exp r -> Exp r
	Mul :: Exp r -> Exp r -> Exp r
	Abs :: (Scalar r) => Exp r -> Exp r
	Signum :: (Scalar r) => Exp r -> Exp r
	Index2D :: (Num r) => Exp Int -> Exp Int -> Exp (Mat2D r) -> Exp r

この構文木を使って、たとえば、reify :: Exp r -> Textという関数を定義して、

> reify $ Const (MatConst [[1,2,3],[4,5,6]])
[[1 2 3];[4 5 6]]
> reify $ Index2D 1 2 (MatConst [[1,2,3],[4,5,6]])
m = [[1 2 3];[4 5 6]];
m(1,2)

といった具合のコード生成をしたい。「コード生成できるもの」を表すReifiableクラスというものを作って、reify :: a -> TextをReifiableクラスのメソッドとする。ここでReifiableのインスタンスとなるのは、Exp r, Mat2D r, Int, Integer, Doubleである。例として以下にExp rとMat2D rのインスタンス定義を示す。ここまでの全ソースコードと実行例はここにある。

instance Reifiable (Exp r) where
	reify (Const exp) = reify exp
	reify (Add e1 e2) = T.concat [reify e1,"+",reify e2]
	reify (Index2D i j mat) = T.concat [reify mat,"(",reify i,",",reify j,")"]

instance (Show r) => Reifiable (Mat2D r) where
	reify (Mat ident) = ident
	reify (MatConst xss) = T.pack $ "[" ++ intercalate ";" (map (intercalate " " . map show) xss) ++ "]"

ちなみに、ここでは行列は二次元のみであるが、行列を2次元以外にも対応させて型安全にするには、ここ(http://www.haskell.org/haskellwiki/GHC/Type_families)にあるように、data family Mat dim rなどとして、dimにD1, D2, D3といった型のタグを付けてやれば良いと思われる。(ただしそのようなことをやると、型クラスをつけるときにだいぶ複雑になる。)

ASTは単なるHaskellの値であるので、mapやfoldなどを使ってASTを構築することもできる。

*Main> reify $ foldl1 (+) $ replicate 5 $ Const (MatConst [[1,2,3],[4,5,6]])
"[1 2 3;4 5 6]+[1 2 3;4 5 6]+[1 2 3;4 5 6]+[1 2 3;4 5 6]+[1 2 3;4 5 6]"

なかなか良い感じなのだが、実はこの実装では問題があり、Index2Dによる要素の取り出しがきちんと出力できない。
Index2D 1 2 (MatConst [[1,2,3],[4,5,6]])をreifyすると、[[1,2,3],[4,5,6]](1,2)となるのだが、Matlabのいけてない言語仕様により、このようには書けず、定数行列を一回変数に代入する必要がある!

モナドを利用した構造化

  • そういった、コード出力時に単純変換以上の何らかの操作をしたい場合には、状態管理を出力のロジックに組み入れる必要がある。→ Stateモナドを使う。
  • また、コードの出力も、単にTextを返すのではなく、ASTをたどっていく過程の任意の段階で出力をしたい。→ Writerモナドを使う。

State、Writerの両方を使いたいので、モナド変換子で2つのモナドを積み上げるのが基本のようだが、今回はRWSTというモナド変換子(Hackage、Shelarcyさんによる解説)を使う。RWSモナドはReader, Writer, Stateのすべてが組み込まれており、しかもMonadState(http://hackage.haskell.org/packages/archive/mtl/2.0.1.0/doc/html/Control-Monad-State-Class.html)などの型クラスのインスタンスになっているので、get/put, tellなどがliftせずに使えるようになっている便利なモナドである。IOも、後ほどのlet-sharingの件で必要となるので入れている。

type W a = RWST () Text CompilerState IO a

最初この巨大な型を見ると面食らうが、その意味するところは、型パラメータの左から順に、読み込む(Reader)ものは無く(())、Textを書き出すことができ、CompilerStateという状態を書き換えることができ、IOもliftIOを介して使える、というものである。書きだすもの(Text)と状態(CompilerState)は型が決まっており、get,put,tellなどの関数を使って、モナドの裏側で暗黙に操作される(これらの関数はみなW a型の値を返す。)。 型コンストラクタの最後に付くa型はdo記法の中で自由に替えられ、モナドのユーザーが明示的に扱うものであるというのは、IOなど他のモナドと同じである。

最初の、reifyがTextを返すバージョンの抜粋

class Reifiable a where
	reify :: a -> Text

instance Reifiable (Exp r) where
	reify (Const exp) = reify exp
	reify (Add e1 e2) = T.concat [reify e1,"+",reify e2]
	reify (Index2D i j mat) = T.concat [reify mat,"(",reify i,",",reify j,")"]

gen :: Exp r -> IO ()
gen = T.putStrLn . reify

reifyがモナドを返すように書き換えたバージョンの抜粋。

type W a = RWST () Text CompilerState IO a
class Reifiable a where
	reify :: a -> W Text

instance Reifiable (Exp r) where
	reify (Const exp) = reify exp
	reify (Add e1 e2) = do
		c1 <- reify e1
		c2 <- reify e2
		return $ T.concat [c1,"+",c2]
	reify (Index2D i j mat) = do
		ci <- reify i
		cj <- reify j
		m <- reify mat
		return $ T.concat [m,"(",ci,",",cj,")"]

data CompilerState = CompilerState { variableCount :: Int }

gen :: Exp r -> IO ()
gen e = do
	(txt,_,_) <- runRWST (reify e) () (CompilerState 0)
	T.putStrLn txt

まだ機能は古いバージョンと全く同じ。runRWSTを使ってモナドを走らせると、IO (Text, CompilerState,Text)を返すので、その結果を取り出せる。

先に述べたように、定数行列から要素を取り出すとき、いったん行列を変数に代入して、その変数から要素取り出しをしたい。
どのように設計すればよいかというと、

  • 以前と同様、reifyが返すのはその値を参照するのに必要なコード。つまり代入した変数の名前。
  • それに加えて、MatConstに対するreifyの定義の中で、tellを使って、値を準備するコード(つまり新しい変数への代入のコード)をWriterモナドに「出力」する。
instance (Show r) => Reifiable (Mat2D r) where
	reify (Mat ident) = return ident
	reify (MatConst xss) = do
		let code =  T.pack $ "[" ++ intercalate ";" (map (intercalate " " . map show) xss) ++ "]"
		var <- newName
		tell $ T.concat [var," = ",code,";\n"]
		return var

ここで、「出力」と鍵括弧にくくったのは、putStrLnのようにIOに出力するのではなく、あくまでWriterモナドをつかっているので、最後にモナドを走らせて「出力」を取り出す必要があるからである。newNameというのは、Wモナドが持つ状態(WモナドはMonadStateのインスタンスである)を利用して一意な名前を作る関数で、次のように定義できる。

data CompilerState = CompilerState { variableCount :: Int }

newName :: W Text
newName = do
	CompilerState count <- get
	put $ CompilerState (count + 1)
	return $ T.pack $ "v" ++ show (count+1)

最後に、Writerモナドに出力した内容と、reify自体の返り値をまとめてコード出力の結果とする。

gen :: Exp r -> IO ()
gen e = do
	(txt,_,assigned) <- runRWST (reify e) () def
	T.putStrLn $ T.append assigned txt

実行結果は以下のようになる。

*Main> let m1 = Const (MatConst [[1,2,3],[4,5,6]]) :: (Exp (Mat2D Int))
*Main> let m2 = Const (MatConst [[10,5,3],[1,4,2]]) :: (Exp (Mat2D Int))
*Main> gen $ Index2D (Index2D 1 2 m1) 1 m2
v1 = [1 2 3;4 5 6];
v2 = [10 5 3;1 4 2];
v2(v1(1,2),1)

良い感じに変数への代入ができている! コードをここに挙げておく。

変数の重複の排除

ここで、以下の様な式を考えてみる。

m1 :: Exp (Mat2D Int)
m1 = Const (MatConst [[1,2,3],[4,5,6]])

test4 :: Exp (Mat2D Int)
test4 = m1 + m1

これをreifyすると、

*Main> gen test4
v1 = [1 2 3;4 5 6];
v2 = [1 2 3;4 5 6];
v1+v2

となり、同じ内容であるはずのm1がv1,v2と複数回生成されている。これくらいの小さなデータならいいが、もしこのm1が重い計算の結果得られる値の場合このような重複は避けたい。なぜこういうことが起こるかというと、test4が簡約されるときに、m1 + m1という式の両側のm1が同じがどうかというのを構わずにHaskellのコンパイラが展開してしまうからである。この論文の3.1節に説明されている。これの重複を避けることには、Let-sharingという名前が付いている。一言で言えば、System.Mem.StableNameモジュールのmakeStableNameとhashStableNameという関数を使って、項の同一性判定をしてやれば良く、さほど難しくはなかった。別の記事で実装について記す予定。

参考になった記事

文献を調査中なので、参考になるものがあったら教えてほしいです。