はてな使ったら負けだと思っている 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

2010-09-07

本家 instant-generics に deriving 用マクロが登場した模様です!!!

| 00:10 |  本家 instant-generics に deriving 用マクロが登場した模様です!!! - はてな使ったら負けだと思っている deriving Haskell を含むブックマーク はてなブックマーク -  本家 instant-generics に deriving 用マクロが登場した模様です!!! - はてな使ったら負けだと思っている deriving Haskell

本日 instant-generics-0.2 が公開されましたが公開されましたね。ドキュメントはまだ生成されていないようですが、モジュール名から色々推測出来そうですね。

Modules
Generics
Generics.Instant
Generics.Instant.Base
Generics.Instant.Functions
Generics.Instant.Functions.Empty
Generics.Instant.Functions.Eq
Generics.Instant.Functions.Show
Generics.Instant.Instances
Generics.Instant.TH

ん……?( ゚д゚)

Generics.Instant.Functions.Show
Generics.Instant.Instances
Generics.Instant.TH

(つд⊂)ゴシゴシ

Generics.Instant.TH

  _, ._

(;゚ Д゚)


Generics.Instant.TH

本家がマクロ機能を提供しちまったぜ!!!!

deriving-IG 用済み!!!!!!!!!!!!


……と云う訳で、derive するのは本家提供のモジュールを使うのが良いでしょう。Fixity の情報を TH で取れないと云ったのは嘘で、普通に取れる様だったので、その辺りの扱いも本家の方が数段良いです。

ただ、構築子に対応するダミー型の名前を指定出来るのは derive-IG だけなので、そこはまあ、アドバンテージがあると云って良いかもしれません。


まあ、ちょっと前まで リポジトリの方にも TH は無かった様なので、俺が開発陣の尻尾に火を点けてやったぜ……!!と思うことにします。

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

2010-09-06

derive-IG-0.1.1 を公開しました。

|  derive-IG-0.1.1 を公開しました。 - はてな使ったら負けだと思っている deriving Haskell を含むブックマーク はてなブックマーク -  derive-IG-0.1.1 を公開しました。 - はてな使ったら負けだと思っている deriving Haskell

instant-generics の Representable クラス及び Constructor クラスを derive する為のライブラリ、derive-IGを公開しました。

http://hackage.haskell.org/package/derive-IG-0.1.1

ソースは GitHubにあるのでどうぞ。

昨日の日記に書いたものの体裁を整えたもので、shelarcy さんの助言に基づいて幾つか機能拡張・改良をしています。

こんな感じで使います。

{-# LANGUAGE TypeFamilies, TypeOperators #-}
import Generics.Instant
import Generics.Instant.Derive      -- derive-IG の関数

data HList a b = a :- b | HNull

-- Either に Representable と Constructor を deriving
$(derive ''Either)

-- 三つ組 に Representable と Constructor を deriving
-- derive をつかうと、(,,) に対応する型が GHC_Types_(,,)_(,,)とかになってしまうので
-- 構築子に対応する型名前を個別に指定している
$(deriveWith ''(,,) [Just "Triple_Triple_"])

-- HNull は自動生成される Main_HList_HNull のままで問題ないので
-- (:-) に対してだけ新しく型名を指定している。
-- 構築子の定義順に、型名指定をすればよい。末尾のNothingは省略可。
$(deriveWith ''HList [Just "Main_HList_HCons", Nothing]

... some nice, great code ...

上では$()で包んで書きましたが、6.12.x系統では他の構文と曖昧にならなければ、splice は省略出来るので

derive ''Either

と書く事が出来ると思います。

また、何らかの事情で Constructor / Representable どちらか一方のみ derive したいといった場合の為に、

  • deriveRep
  • deriveRepWith
  • deriveCon
  • deriveConWith

と云った関数も提供しております。

演算子も一応処理出来る様にはなっていますが、 reify で結合性を取ってこれないので、今のところは演算子か否かの判定しか行なっていないと思ってください。なので、型演算子については Constructor を自前で定義して、 Representable は derive、といった使い方が良いかと思います。

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

2010-09-05

instant-generics を derive するマクロを書いた

|  instant-generics を derive するマクロを書いた - はてな使ったら負けだと思っている deriving Haskell を含むブックマーク はてなブックマーク -  instant-generics を derive するマクロを書いた - はてな使ったら負けだと思っている deriving Haskell

昨日の資料にもあった様に、 instant-generics は標準で derive する為の機構を用意していません。

そこで、自前で derive するための機構を用意しました。

GitHubからどうぞ。

演算子に対応させて、SYBへの依存を抹消出来たらHackage に登録しようと思います。まあ、SYBが走っているのはコンパイル時だけなので、実行する分には速度のロスにはなっていないとは思いますが、IGのユーティリティなのに syb つかうのもなぁ……と思ったので。

今日書いたばかりなのでどっか壊れてるかもしれませんが、今のところきちんと動いている様に見えます。やりましたね。examples をみて頂ければわかる様に、もう Representable も Constructor も基本的には書かなくて良くなって、とても天国になると思います。やったね!

昨日のスライドの補足

|  昨日のスライドの補足 - はてな使ったら負けだと思っている deriving Haskell を含むブックマーク はてなブックマーク -  昨日のスライドの補足 - はてな使ったら負けだと思っている deriving Haskell

昨日のスライドの内容に関してshelarcy さんから幾つかご指摘を頂いたので、補足訂正をばしたいと思います。

スライドもあとで修正を入れるかと。

  • SYB が遅いのは再帰関数になっていて、Inline化されないからと云うのもある
    • IG や Smash はコンパイル時に型を舐めることで再帰関数を出来るだけ排除することが出来ている
  • cast の成否で適用・不適用を判断するのは Typeable でも出来る
    • スライドを書いたときの意図としては、『cast の成否に基いて、"子要素を畳み込み"出来る』と云う意味合いで書こうとしたと思われますが、何だか意味ふめいな説明になってしまっていた……
トラックバック - http://haskell.g.hatena.ne.jp/mr_konn/20100905

2010-09-04

PFI セミナーで Template Haskell と Haskell の総称プログラミングについて話してきました

|  PFI セミナーで Template Haskell と Haskell の総称プログラミングについて話してきました - はてな使ったら負けだと思っている deriving Haskell を含むブックマーク はてなブックマーク -  PFI セミナーで Template Haskell と Haskell の総称プログラミングについて話してきました - はてな使ったら負けだと思っている deriving Haskell

Metaprogramming in Haskell と云うタイトルで、PFIセミナーで話させて頂きました。

恐らく、Template Haskell や SYB、Generics、instant-generics についてのまとまった資料としては、日本語ではほとんど唯一であると思うので公開します。



20ページ削って50ページにしたんですが、30分と云う制限を遥かに越えて喋ってしまいました……。

前半のTHのところは、コンパイルタイム運命とかアホなことをやったので大盛り上がりでしたが、後半はちょっとマニアック過ぎて置いてけぼりにしてしまいました。反省……

Keynoteファイルはこちら。本編で使ったサンプルはGitHubにあります。

PDF版も用意しました。Keynote だとそんなに重くないんですが、 PDF は 14 MBと重めなので注意してください。

Slide Share 版は以下です。


Template Haskell については以前纏めたスライドがありますが、今回は 準クォートについての記述を追加してあります。準クォートと云うのはLISPのリーダマクロに対応するもので、パーサさえ書ければ簡単にDSLやオレオレパターンマッチを実現出来るものです。本当に便利なので、みんな使うと良いです。


Haskell での総称プログラミングについては、三つの手法を取り上げています。

です。他にも沢山手法があるのですが、代表的なものと云うことでこの三つにしました。SYB-with-Class や Uniplate、Smash については、巻末で少しだけ言及しています。

まさかこんなに Haskell で総称プログラミングの手法があるとは思いませんでした。それぞれ長短があるので、機会によって使い分けていきたいですね。

あと思ったのが、なんで instant-generics は Uniplate や SYB-With-Class の様なTHによる deriving 機能を提供していないのだろうと云うことで、時間があったら探ってみたいです。

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