型安全な汎用文字列生成マクロをquasi quoteで実装してみます。 機能として次のような物を持たせます。
- 変数を取れる
- 内部でHaskell式を実行出来る
{-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-}
module Language.SExpr (s) where
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import qualified Language.Haskell.Meta.Parse as MP (parseExp)
import Data.List (intercalate)
import Data.Maybe (catMaybes)
import Text.Parsec
import Text.Parsec.Combinator (manyTill)
import Text.Parsec.String (Parser)
import Control.Applicative hiding ((<|>), many, optionMaybe, optional)
data SExpr = SList [SItem]
data SItem = SString String | SExp String | SVar
sExprP :: Parser SExpr
sExprP = SList <$> many sItemP
where
sItemP :: Parser SItem
sItemP =
SString <$> many1 (noneOf "[" <|> try (char '[' <* notFollowedBy (char '=')))
<|> (string "[=" >>
(SVar <$ char ']'
<|> SExp <$> (manyTill anyChar (try (string "=]")))))
parseSExpr :: FilePath -> String -> SExpr
parseSExpr path = either (error . show) id . parse (sExprP <* eof) path
buildExp :: SExpr -> Q Exp
buildExp (SList items) =
mapM toExpM items >>= pure . unzip >>= \(names, exps) ->
lamE (map varP $ catMaybes names) (foldr appE [|""|] exps)
toExpM :: SItem -> Q (Maybe Name, Q Exp)
toExpM item = case item of
SString str -> pure (Nothing, [| (++) str |])
SExp command -> pure (Nothing, [| (++) $(either undefined pure (MP.parseExp command)) |])
SVar -> newName "x" >>= \name -> pure (Just name, [| (++) $(varE name) |])
showLoc :: Loc -> String
showLoc loc = intercalate ":" [loc_filename loc, show line, show col]
where (line, col) = loc_start loc
s :: QuasiQuoter
s = QuasiQuoter {
quoteExp = (\input -> location >>= \loc -> buildExp (parseSExpr (showLoc loc) input)),
quotePat = undefined,
quoteType = undefined,
quoteDec = undefined
}
[=]
がString引数を取るプレースホルダ、[= <exp> =]
が式の埋め込みです。
<exp>
はStringを返す(大体)任意のHaskell式ですが、quasi quoteをネストする事は出来ません。
> [s|foo[=]|] "bar"
"foobar"
> [s|int [=] = [=];|] "x" "1"
"int x = 1;"
> [s|var xs = [= show (take 4 [1..]) =]|]
"var xs = [1,2,3,4]"
なんだかrubyっぽいですが、コンパイル時に型検査が行われます。
非常にシンプルですが、やっている事としては、
- パーサーでquasi quote内の文字列を構文木に変換 (
parseSExpr
) - 構文木をExpに変換すると同時に変数を収集 (
toExpM
) - 無名関数コードを生成 (
buildExp
) - 式コンテキストのQuasiQuoterを定義 (
s
)
というもので、これを基本に色々な応用技が出来る感じです。 パーサーによるシンタックスの注入とanti quoteによるコンテキストへの介入で夢が広がりんぐです。
StringからHaskell式を生成するために、haskell-src-metaパッケージを使用しました。
$ cabal install haskell-src-meta
-
準クォートでもてかわゆるふわメタプログラミング!: http://haskell.g.hatena.ne.jp/mr_konn/20101210/quasiquotes 式の構築の仕組みはこちらからほぼそのまま借用しました。
-
GHC Template Haskell: http://www.haskell.org/ghc/docs/latest/html/users_guide/template-haskell.html
完全にText.Shakespeare.Textの再発明だが、当時は単純にその存在を知らなかったという。。