Skip to content

Instantly share code, notes, and snippets.

@ykst
Created January 28, 2015 14:09
Show Gist options
  • Save ykst/b6d00c5fb2cc3df5c8a3 to your computer and use it in GitHub Desktop.
Save ykst/b6d00c5fb2cc3df5c8a3 to your computer and use it in GitHub Desktop.
TemplateHaskellによる文字列生成埋め込みDSL

TemplateHaskellによる文字列生成埋め込みDSL

型安全な汎用文字列生成マクロを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によるコンテキストへの介入で夢が広がりんぐです。

使用Hackage

StringからHaskell式を生成するために、haskell-src-metaパッケージを使用しました。

$ cabal install haskell-src-meta

参考

@ykst
Copy link
Author

ykst commented Jun 2, 2016

完全にText.Shakespeare.Textの再発明だが、当時は単純にその存在を知らなかったという。。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment