dataToExpQ
Haskell Advent Calendar 2012二日目です。
一日目から飛ばしてきましたね。負けずに頑張らなければなりませんね。
QuasiQuotes書くときにdataToExpQ便利ですよね。
便利だけどこんさんが何言っているかわかんねえ!という人向けの記事です。
間違ってたらツッコミください。
(以下ではGHC7.4.1を用いています)
何故QuasiQuotesなのか
僕らエンジニアが扱うものは基本何らかのシンタックスに則って書かれています。
つまりHaskellerはConfigファイルやSQL文字列、もしくはある言語ファイルなどを目の前にして、
「このファイルパーズして"Haskellのデータ構造"として欲しいなあ」とつぶやく訳です。
文字列から値生成ならまだランタイムに出来ますが、型の生成や型クラスの生成などとなるとコンパイルタイムにやるしかありません。
というわけでまあQuasiQuotesを使うわけです。
QuasiQuotes作成
このQuasiQuotes、定義は以下の様になっているわけですが、
data QuasiQuoter = QuasiQuoter { quoteExp :: String -> Q Exp, quotePat :: String -> Q Pat, quoteType :: String -> Q Type, quoteDec :: String -> Q [Dec] }
つまりはString -> Q Expといった関数作れば良い訳ですね。
で、パーザが必要になるのまではまあいいんですが、ExpQ(Q Expのalias)を自分で構築するの面倒ですよね。
GHCでは、「あるデータ型の生成」に対象を絞ることで簡単にQuasiQuotesを構築する方法が提供されています。
それがdataToExpQ系列の関数です。ここではdataToExpQをとりあげます。
dataToExpQとは
dataToExpQはLanguage.Haskell.TH.Quotesで提供されています。
QuasiQuoterを作る際のイメージを簡略化して図で表すと、
+--------+ parser +---------------+ dataToExpQ +--------+ | String |------->| (Data a) => a |----------->| ExpQ | +--------+ +---------------+ +--------+
こんな感じです。parserでStringからあるデータ(Dataのインスタンス)を生成します。
そのデータからExpQを生成するのがdataToExpQです。結果(String -> ExpQ)の関数が出来るため、
それをQuasiQuoterにぶちこみます。
具体的に型を見てみます。
Prelude> import Language.Haskell.TH.Quote as Quote Prelude Quote> :type Quote.dataToExpQ dataToExpQ :: Data a => (forall b. Data b => b -> Maybe (Language.Haskell.TH.Syntax.Q Language.Haskell.TH.Syntax.Exp)) -> a -> Language.Haskell.TH.Syntax.Q Language.Haskell.TH.Syntax.Exp
ややこしそうに見えますが、1引数適当に渡して、fullQualifiedな部分を落とすと分かり易くなります
(手で簡略化してます)。
Prelude Quote> :type Quote.dataToExpQ $ \_ -> Nothing dataToExpQ $ \_ -> Nothing :: Data a => a -> ExpQ
つまり、Data a => a を ExpQ に変換するからdataToExpQという名前がついていると。そのままですね。
この変換過程にユーザが口を挟むためのものがさっき渡した引数という訳です。
\_ -> Nothing (const Nothingでもいいですが)を渡した場合は特別なことは何もしません。
ではDataとは何でしょう。
Scrap Your Boilerplate
Generic Programmingという概念があって、それのHaskell版です。ひどい説明ですね。
Scrap Your Boilerplate(syb)という論文とライブラリを以てHaskell(GHC)に取り込まれ
ました。
GHCの拡張、DeriveDataTypeableを有効にすると、deriving節でTyepable, Dataが指定出来る様になります。
Typeableは自身の型表現を返せる型のクラスであり、型安全なcastを提供します。
Dataは一般化されたfold(gfold)を持つ型のクラスであり、Generic Programmingを提供します。
// TODO: syb例を書こうと思ったけど面倒になってきた
dataToExpQを適当に使ってみる
dataToExpQを使ってみましょう。
XXXという文字列からYYYというデータ型ほしいなあ、という欲求部分ですが、
例題としてXXXを数値リテラルと和と積のみの数式、YYYをExprとします。
あ、文字列のパーズ部分は省略します。
{-# LANGUAGE TemplateHaskell #-} module Main where import Language.Haskell.TH import Language.Haskell.TH.Quote import Data.Generics.Aliases import Expr -- GHC stage restrictionのためモジュール分けた。後述。 -- 以下ではexpressionをExpQに変換した後、その場に直ぐspliceしている main = do putStrLn "---- do nothing" print $((dataToExpQ $ const Nothing) expression) putStrLn "---- 3 to 8" print $((dataToExpQ $ Nothing `mkQ` lit3tolit8) expression) putStrLn "---- Add to Mul" print $((dataToExpQ $ Nothing `mkQ` addToMul) expression) putStrLn "---- succ and addToMul" print $((dataToExpQ convertExpr) expression)
以下がExprです。
{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE TemplateHaskell #-} module Expr where import Language.Haskell.TH import Language.Haskell.TH.Quote import Data.Generics -- 数式パーズして得られるデータ型(derivingでDataのインスタンスにしてある) data Expr = Lit MyInt | Add Expr Expr | Mul Expr Expr deriving (Typeable, Data, Show) -- データ型ひとつではつまらないのでもう一つ定義 newtype MyInt = MyInt Int deriving ( Typeable , Data , Show -- 表示用 , Num -- 数値リテラルとして使用する(Cf.GeneralizedNewtypeDeriving) , Enum -- succの使用 ) -- パーズ後得た値の例 expression :: Expr expression = Add (Lit 3) (Add (Lit 4) (Mul (Lit 5) (Add (Lit 6) (Lit 7)))) -- ... snip ...
dataToExpQに渡す部分を見てみます。まず木構造の葉の変換。
-- 3 を 8 に変換(葉の変換) lit3tolit8 :: MyInt -> Maybe ExpQ lit3tolit8 (MyInt 3) = Just [| MyInt 8 |] lit3tolit8 _ = Nothing
葉の所で現れるMyIntだけを考慮して変換ルールを書いてしまいます。
これをmkQを使ってクエリ化(mkQのQはQモナドではなく、Queryを意味しています)し、dataToExpQに食わせます。
main = do -- ... snip ... print $((dataToExpQ $ Nothing `mkQ` lit3tolit8) expression) -- ... snip ...
mkQは定義見ると分かりますが、動作は要するにmaybeです。第2引数試して駄目なら第一引数返します。
つまりlit3tolit8が使えない(MyIntでない)ならNothingを返します。
ここで僕らはGeneric Programmingをしていることを思い出して下さい。
dataToExpQは変換対象の値(expression)にDataのインスタンスを要求しているため、
Dataの親クラスであるTypeableも要求しており、対象の値の型表現をチェック出来るわけです。
次に内部ノードの変換です。
-- AddをMulに変換(枝の変換) addToMul :: Expr -> Maybe ExpQ addToMul (Add x y) = Just [| Mul $((dataToExpQ $ Nothing `mkQ` addToMul) x) $((dataToExpQ $ Nothing `mkQ` addToMul) y) |] addToMul _ = Nothing
ここでもやはりExprのみを考慮して書きます。
自身を再帰しています。Generic Programmingとはいえ、
dataToExpQがMaybe ExpQを要求してしまっているので、
自分で末端まで変換して上げないといけない気がします..
ということで再帰しています。
最後にクエリの合成です。mkQの後にextQでクエリを連結させていきます。
-- succMyIntとaddToMulの合成、と言いたいが.. convertExpr :: Data a => a -> Maybe ExpQ convertExpr = Nothing `mkQ` succMyInt' `extQ` addToMul' where -- AddをMulに変換(convertExprを呼び出してしまっている) addToMul' :: Expr -> Maybe ExpQ addToMul' (Add x y) = Just [| Mul $((dataToExpQ convertExpr) x) $((dataToExpQ convertExpr) y) |] addToMul' _ = Nothing -- MyIntの部分をインクリメント succMyInt' :: MyInt -> Maybe ExpQ succMyInt' (MyInt i) = Just $ appE (conE 'MyInt) (litE (integerL $ fromIntegral (succ i)))
succMyInt'とaddToMul'の単純な合成が出来ればいいのですが、
やはりdataToExpQがMyabe ExpQを要求しているので単純に合成とは行かなそうです。
しようがないのでaddToMul'の内部でconvertExprを呼び出しています。
さて以上の様に、パーズした結果を自由に変更しつつExpQに持ち上げることが出来るとわかりました。
本来ならばこれを応用してAntiQuote(反クオート)を行います。
まあそのへん適当にやってください。
反クオート以外にも色々出来るかもしれません。
sybは実行時に型表現をチェックしているために動作が遅いのですが、
やっていることはQモナドの生成であるため、コンパイル時間が遅くなる程度ですみます。
Generic ProgrammingとTemplate Haskellは相性いいですね。
あ、今思いついたけど、データ色々変更したいならdataToExpQに渡す前に自分でGeneric Programmingすればいいですねあほでしたね僕。
色々台無しになりましたが終わります。
GHC stage restriction
Template Haskellを書いていると以下のようなエラーに度々出会います。
/some/path/to/haskell/file.hs:xx:yy: GHC stage restriction: hoi' is used in a top-level splice or annotation, and must be imported, not defined locally In the first argument of `foo', namely hoi' In the expression: foo hoi' In the first argument of `print', namely `$(foo hoi')'
無限ループ処理が面倒なことによる制限らしいのですが、対処法は単純です。
エラーメッセージで書いてある様にモジュールローカルで定義をせずに、外部モジュールで定義するか、
怒られている対象の関数を関数化せずに手動でインライン化するかすればいいです。
ここからはTemplate Haskell内部知らないので予想でしかないのですが、GHCはTemplate Haskellを扱う際(恐らく-XTemplateHaskellあたりをフラグとして)、
ExpQなどTemplate Haskell用のQモナドを扱う関数とその他の関数を区別して取り扱うようなので(考えてみればそりゃそうだ、という感じですが)
開発者側でもマクロに関わる関数と普通の関数に気をつけて記述する必要があるのだと思います。
ちなみにHaskellと同じような型付きマクロを持っているHaxe(2.10)でも同様の制限がありました。
コンパイルが止まらなくなるよりはマシだろ、ということですかね。
// TODO: リンクとかはる