はてな使ったら負けだと思っている deriving Haskell このページをアンテナに追加 RSSフィード

 | 

2010-12-10

準クォートでもてかわゆるふわメタプログラミング!

|  準クォートでもてかわゆるふわメタプログラミング! - はてな使ったら負けだと思っている deriving Haskell を含むブックマーク はてなブックマーク -  準クォートでもてかわゆるふわメタプログラミング! - はてな使ったら負けだと思っている deriving Haskell

(この記事は Haskell Advent Calendar jp 2010 参加記事です。)

皆さん、パーサ、書いてますか?……書いてる。それはよかった。では皆さん、Template Haskell、使ってますか?……使っている。それは素晴しい!

それでは皆さん、準クォート、使ってますか?……え?使っていない!?それは実に勿体ない! パーサ書いてて Template Haskell も使っているのに準クォートを使わないのは、えーと、そう!三角関数と複素数を知っているのに極座標を知らない様なもの ですよ!


……と云う訳で、準クォートです。

Template Haskell 知らないよ!と云う人は、はてなブックマーク - PFI セミナーで Template Haskell と Haskell の総称プログラミングについて話してきました - はてな使ったら負けだと思っている deriving Haskell - haskell の前半部分に目を通しておくと良いでしょう。


準クォートとはリーダマクロのことで、簡単に説明してしまえば「超お手軽俺俺 EDSL 作成機能」です。

この記事では、二つの例を挙げて、準クォートの簡単な使い方について説明したいと思います。

それぞれの例の全体は GitHub のプロジェクトページ から手に入ります。以下の記事では重要なところだけ抜粋してあります。

Printf の例(関数の合成)

最初の例は printf です。

ソースを見てみる前に、実行例を示しておきます。

$ ghci -XQuasiQuotes
Prelude> :l printf.hs
[1 of 1] Compiling Printf           ( printf.hs, interpreted )
Ok, modules loaded: Printf.
*Printf> [$printf| 1 + %d = %d |] 2 (1+2)
" 1 + 1 = 2 "
*Printf> [$printf|Hello, %s!|] "QuasiQuotes"    
"Hello, QuasiQuotes!"

御覧の様に、定義された準クォートを使用するには QuasiQuotes 言語拡張が必要になります。

[$ident|mokeke|] などと書くと、ident に束縛されている QuasiQuoter に "mokeke" が渡されて、その結果がその場に splice されます。

では、ソースを見ていきましょう。

{-# LANGUAGE TemplateHaskell #-}
module Printf (printf) where
import Language.Haskell.TH
import Language.Haskell.TH.Quote

printf :: QuasiQuoter
printf = QuasiQuoter { quoteExp = buildFun . parsePrintf }

因みに、QuasiQuoter は Language.Haskell.TH.Quotes で以下の様に定義されています。

data QuasiQuoter = QuasiQuoter { quoteExp :: String -> ExpQ, quotePat :: String -> PatQ }

つまり、準クォートの本体は唯のパーザの組に過ぎないのです。今回の例ではパターンは使わないので、quotePat に値を束縛していないだけです。

今回では、文字列を一旦中間の状態である[Printf]に変換して、その後フォーマットに対応する関数の構文木に変換しています。

こんな感じに:

-- | Printf の書式を表わす型
data Printf = Lit Char
            | String
            | Dec
            | Float
            | Show
            | Char
              deriving (Show, Eq)

-- | 文字列をパースして中間形式の Printf 型のリストにする関数
parsePrintf :: String -> [Printf]
parsePrintf = ...

-- | Printf 型のリストを関数に変換する関数
buildFun :: [Printf] -> ExpQ
buildFun = ...

こんな感じです。簡単でしょ?

JSON (代数的データ型と反クォート)

前節では Printf の簡単な例を見ましたが、式クォートしか使っていないので、

formatter = [$printf|%s is %d years old.|]

の代わりに

formatter = $(buildFun . parsePrintf $ "%s is %d years old.")

と書いても同じ効果が得られます。何が違うの?これだったら普通のTH使うのと同じじゃないか!

また、一々中間表現を構文木に手で変換する関数を書いているのがちょっと面倒です。なんとか成らないの?……それが成るんですよおくさん!

と云う訳で、もっと QuasiQuote の旨みがわかる例が JSONの例 です。


では、まず使用例から。

$ ghci -XQuasiQuotes       
GHCi, version 6.12.3: http://www.haskell.org/ghc/  :? for help
Prelude> :l json.hs 
[1 of 1] Compiling Main             ( json.hs, interpreted )
Ok, modules loaded: Main.
*Main> [$js| 12 |]
JSNumber 12
*Main> let twelve = 12; name = "mr_konn" in [$js| {ident: `twelve, name: `name} |]
JSObject [("ident",JSNumber 12),("name",JSString "mr_konn")]
*Main> let isNull [$js| null |]  = True; isNull _ = False 
*Main> isNull [$js| "hoge" |]
False
*Main> isNull JSNull
True

概ね前回と変わりがない様ですが、準クォートを用いたパターンマッチを行っていますし、

*Main> let twelve = 12; name = "mr_konn" in [$js| {ident: `twelve, name: `name} |] 

と云うちょっと気になる部分があります。この部分こそが、QuasiQuotes の極意とも云える「反クォート」を使っているところです。

反クォートと云うのは、普通にデータを構文木にそのまま変換するだけではなくて、そこに外部の関数や計算式を埋め込んだりする処理のことです。

この例では、 twelve、nameといった外部の値を、JSON のデータの中に埋め込んでいます。

反クォートの構文はパーサを書く人間が自由に決めてよくて、たとえば [$js| {ident: #{twelve}, name: #{name}} |] みたいな感じにすることも出来ます。


では、ソースを見ていきましょう。

{-# LANGUAGE TemplateHaskell, DeriveDataTypeable, OverlappingInstances #-}
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Data.Generics

今回は、Data.Generics パッケージを import しています。これは、後で触れる『簡単に構文木に変換するジュモン』の為の準備です。

それとの組み合わせのため、DeriveDataTypeable 言語拡張もオンにする必要があります。後の言語拡張は今回の例題固有のものなのでどうでもいいです。


data JSON = JSNull                    -- ^ 空
          | JSNumber Int              -- ^ 数
          | JSString String           -- ^ 文字列
          | JSArray  [JSON]           -- ^ 配列
          | JSObject [(String, JSON)] -- ^ オブジェクト
          | JSQuote  String           -- ^ アンチクォート
            deriving (Show, Eq, Data, Typeable)

class IsJSON a where
  toJSON :: a -> JSON

JSONの値を表わすデータ型と、JSONに変換出来ることを表わす IsJSON 型クラスの定義です。

JSONの定義の一番最後の JSQuote String が、今回の目玉である準クォートのためのコンストラクタです。Data, Typeable を derive オマジナイです。


json :: Parsec String () JSON
json = lexeme (jsquote <|> jsnull <|> try jsnumber <|> try jsstring <|> try jsary <|> jsobj)
jsquote  = JSQuote <$> (symbol "`" *> ident)
...

しばらくずらずらっとパーザの定義が続きます。急拵えのパーザなので、時々パーズに失敗しますが、ゆるふわーな心で許してください。


では、いよいよ 準クォートの心臓部です。

js :: QuasiQuoter
js = QuasiQuoter { quoteExp = parseExp, quotePat = parsePat }

parseQa :: (JSON -> Q a) -> String -> Q a
parseQa jsonToA str = do
  loc <- location
  let pos = uncurry (newPos $ loc_filename loc) (loc_start loc)
      ans = parse (setPosition pos >> (json <* many space)) (loc_filename loc) str
      jsv = either (error.show) id ans
  jsonToA jsv

parseExp :: String -> ExpQ
parseExp = parseQa (dataToExpQ $ const Nothing `extQ` antiQuoteE)

parsePat :: String -> PatQ
parsePat = parseQa (dataToPatQ $ const Nothing `extQ` antiQuoteP)

parseQa は、JSON からなにがしかの構文木に変換する関数を取って、文字列からその構文木へのパーザを返す関数です。式もパターンも途中までは殆んど同じ動作なので、その共通部分を括り出した関数ですね。

parseExp, parsePat では dataToExpQ だとか dataToPatQ だとかミョーな名前の関数が出て来ていますが、これこそが散々触れていた『データ型を構文木に変換するためのオマジナイ』です。正確には syb を使った総称関数の一種なんですが、まあそんなことどうでもいいです。

dataToExpQ (或いは PatQ) は、反クォート処理を行う補助関数を取って、データを構文木に変換する関数です。このデータ型は Data クラスインスタンスではないといけません。

言葉で説明するとわかりづらいですが、上の使い方を見て頂ければ大体のところはわかると思います。補助関数は、何か特別な処理をした場合は Just を、特に何もせずそのまま変換して良いときは Nothing を返すことになっています。

それでは、実際に反クォートを行っている部分を見てみましょう。

antiQuoteE :: JSON -> Maybe ExpQ
antiQuoteE (JSQuote nm) = Just $ appE (varE 'toJSON) (varE $ mkName nm)
antiQuoteE _ = Nothing

antiQuoteP :: JSON -> Maybe PatQ
antiQuoteP (JSQuote nm) = Just (varP $ mkName nm)
antiQuoteP _ = Nothing

式構文木用の反クォート処理では、JSQuote "hoge" なら hoge と云う名前の変数をスコープから探してきて、toJSON を適用してJSON型に変換してその場に埋め込む、と云うことをしています。一方、パターンクォートの方ではただ単に変数名の指定のために反クォートを使っている、と云う訳です。

JSQuote はあくまで準クォートの内部処理の為に定義したコンストラクタなので、このコードをライブラリとして公開する時には、 多分モジュール外にエクスポートしない方が良いでしょう。


GHC-7.0.1 以降の準クォート

と云う訳で、ここまで駆け足で準クォートを解説してきましたが、そろそろお時間の様です。

今回は GHC-6.12.3 に準拠した準クォートの解説でしたが、GHC-7系統からは大幅に機能が強化されます。

  • 式・パターンに加えて、新たに宣言・型のクォート機能が追加された
    • qq = QuasiQuote { quoteExp = hoge, quotePat = fuga, quoteType = moge, quoteDec = piyo }
    • などとして使う。
  • 準クォートの $ が省略可能に
    • [printf|%s must go!|] とか [json| {ident: `id} |] とか書ける様になります

これからも益々様々な活用が出来そうですね!やりました!


Happy QuasiQuoting!

トラックバック - http://haskell.g.hatena.ne.jp/mr_konn/20101210
 |