多くのプログラミング言語が用途を限定しない汎用の言語として設計されているのに対して、領域特化言語(ドメイン固有言語, domain-specific language, DSL)は何らかの目的に特化して設計された言語のことをいいます。領域特化言語を導入する利点のひとつは、プログラムの各部分の役割が明確になり、ソフトウェア全体の設計がよりわかりやすくなるというものがあります。たとえば、ゲームソフトウェアではゲームの描画とゲームの状態更新は完全に分離されているべきです。ゲームで一時的に処理が重くなり、描画を何フレームかスキップして速度を維持したいとしましょう。もし状態更新と描画がきちんと分離されていなければ、描画だけをうまくスキップするということはできなくなってしまいます。もし描画専用の領域特化言語を導入すると、ソフトウェアのコードをその内容に沿って完全に分離するのを強制し、それに違反するようなコードをコンパイル時に防ぐことができます。
Freeモナドを使うと、一定の手順にしたがって命令群を定義するだけで、簡単にそのような領域特化言語を構成する事ができます。しかも、あくまでその領域特化言語は元のプログラミング言語の上で構成されているので、構文そのものは元のプログラミング言語そのままですし、純粋な計算については何ら制限なく自由に使うことができます。しかも、その領域特化言語で書かれたプログラムはデータとして扱うことができ、その解釈を動的に切り替えるようなことも可能です。プログラムをデータとして動的に操作できるというとまるでLisp/Schemeのようですが、Lispのような動的な型付けで何でもありのリストとは異なり、Freeモナドなら静的型付けの上で型安全な言語を定義することができます。動的に命令列を構成し、その解釈も動的に自由に切り替えられる柔軟さと、誤った命令をコンパイル時に検出できる堅牢さを同時に達成できるわけです。
領域特化言語を使ってみる
まずは領域特化言語を使ったコードがどのようになるのかを見てみましょう。何か仮想的な命令体系を想定して説明するのもいいですが、既知のAPIのほうが理解も早いだろうということと、現実のAPIを使うことで実用性を匂わせておきたいという意図で、HTML5のCanvasグラフィックスAPIに基づいた領域特化言語を考えてみます。これとほぼ同様のものがpurescript-free-canvasで実装されています。
最近筆者はPureScriptばかりいじっているのでPureScriptの文脈で説明をしていきますが、もちろん他の言語でも同様のことができるはずです。構文もHaskellとほとんど同じなので、雰囲気で理解できると思います。
このテキストで作る領域特化言語には、以下のようなCanvas APIに対応する3つの命令があります。これらは実のところただのPureScriptの関数に過ぎません。
setFillStyle :: String -> Graphics Unit
measureText :: String -> Graphics TextMetrics
fillText :: String -> Number -> Number -> Graphics Unit
Graphics a
がモナドなので、いつものようにdo記法でだらだらと命令を並べて描画の内容を記述することができます。
paint :: Graphics Unit
paint = do
let text = "Lorem ipsum dolor sit amet"
metrics <- measureText text
setFillStyle "red"
fillText text (100.0 - metrics.width) 20.0
このGraphics a
はキャンバスグラフィックス専用のモナドですから、例えば意図しないDOM操作をうっかりねじ込んでしまうというようなことは起こりえません。確実にコンパイルエラーになります。
paint :: Graphics Unit
paint = do
let text = "Lorem ipsum dolor sit amet"
metrics <- measureText text
appendChild parent child -- コンパイルエラー!勝手にDOMをいじるんじゃない!
setFillStyle "red"
fillText text (100.0 - metrics.width) 20.0
DOM操作のような別の作用を描画の部分に埋め込むことは禁止されますが、純粋な計算であれば状態に一切悪影響を与えませんから、この領域特化言語内で自由に利用可能です。100.0 - metrics.width
のような数値計算には何の制限もありません。独自の領域特化言語といっても、PureScript本来の表現力が失われることはありません。
ここで各命令の引数にグラフィックスコンテキストを与えていないことにも注目してください。JavaScriptだと
context.measureText(text)
というようにコンテキストも指定しなければなりませんが、ここでは
measureText text
だけの指定で呼び出せています。どのコンテキストに対して描画命令を出すかという構造も分離されているのです。コンテキストを渡さなくて済むのは、単にコーディングの手間が省けるという意味でも便利です。念の為に言っておきますが、決してグラフィックスコンテキストをグローバルに共有しているわけではありません。
また、すでにHaskellやPureScriptを使っている方には言うまでもないようなことですが、同じ命令を繰り返したければいつものfor
やfor_
を再利用することができます。領域特化言語専用のfor
を改めて定義する必要はありません。if-then-else
やwhen
もいつもどおりです。
for_ (range 1 3) \i -> do
fillText text (100.0 - metrics.width) (20.0 * toNumber i)
この一連の描画命令を実際に実行するのは、runGraphics
という関数を呼び出すだけなので簡単です。グラフィックコンテキストを明示的に渡すのはここの一度だけです。
runGraphics paint context
Freeモナドを使うと、こういった領域特化言語を刺し身の上にタンポポを載せるような単純作業で定義することができます。
Freeモナドを使って領域特化言語を作る
ライブラリの準備
PureScriptではpurescript-freeというモジュールがあって、Freeモナドが一連のライブラリとして提供されています。Haskellだとekmett/freeがありますし、Scalaならscalazがよさそうです。その他の言語でももちろん可能ですが、HaskellやPureScriptでさえ結構手順が面倒なので、強力な型システムやモナド用の糖衣構文がない言語ではなかなか苦しいと思います。
Freeモナドを自力で定義してもそれほど大変ではありません。自力で定義したいかたは、記事末尾に挙げた参考文献を御覧ください。
命令セット
まずはその領域特化言語にどんな命令を用意するか検討します。説明のためだけにCanvasRenderingContext2DのすべてのAPIを実装するわけにはいかないので、以下の3つの命令だけ用意することにしましょう。
interface CanvasRenderingContext2D {
fillStyle: String;
measureText(String text): TextMetrics;
fillText(text: String, x: Number, y: Number): void
}
これに対応する、領域特化言語でない、直接の作用を起こすほうのAPIの定義は以下のようになります。これは後で解釈関数というのを実装するときに、低レベルなAPIとして使います。
_setFillStyle :: forall eff . String -> Context2D -> Eff (canvas :: Canvas | eff) Unit
_measureText :: forall eff . String -> Context2D -> Eff (canvas :: Canvas | eff) TextMetrics
_fillText :: forall eff . String -> Number -> Number -> Context2D -> Eff (canvas :: Canvas | eff) Unit
データ型
それでは実際に領域特化言語を作っていきます。最初に、先ほどの命令群に対応するデータ型GraphicsF
を定義します。代数的データ型を使い、各命令が受け取る引数をフィールドとして持つようなコンストラクタを与えます。
data GraphicsF more = SetFillStyle String more
| MeasureText String (TextMetrics -> more)
| FillText String Number Number more
このとき、幾つかポイントがあるので注意してください。
- このデータ型には型変数
more
を持たせます。この謎の型変数の意味についてはFreeモナドそのものの定義について調べてみるとわかりますが、このテキストでは気にしないことにします。 - 実行した結果を返さないような命令の場合は、それぞれのコンストラクタの最後のフィールドとして
more
を付け加えます。例えば、SetFillStyle
はこの命令の結果のようなものはないので、そのままmore
を付け加えます。 - 実行した結果を返すような命令の場合は、その命令が返す値の型から
more
へ写す関数をフィールドの最後に付け加えます。例えば、MeasureText
は命令を実行した結果としてTextMetrics
を返しますが、コンストラクタのフィールドの最後にTextMetrics -> more
を付け加えます。
なお、ここではグラフィックスコンテキストと分離したいので、Context2D
はフィールドに加えていません。
Functorのインスタンス
また、Free
と組み合わせるにはGraphicsF
にFunctor
のインスタンスが(ひとまずは)必要です。ここがちょっとめんどうくさいですが、それぞれのコンストラクタの最後の追加のフィールドのところに関数f
を噛ませるだけです。
instance functor_GraphicsF :: Functor GraphicsF where
fmap f (SetFillStyle style more) = SetFillStyle style (f more)
fmap f (MeasureText text more) = MeasureText text (f <<< more)
fmap f (FillText text x y more) = FillText text x y (f more)
HaskellだとFunctorを自動で導出できるので簡単ですが、PureScriptにはインスタンスの導出がありません。どうせ単純作業なので、ここでは頑張って自力で定義しますが、実は後述するようにFunctorのインスタンスを自分で書かずに作るテクニックがあります。
型の別名
型名としてはFree GraphicsF a
というようにFree
にGraphicsF
を埋め込んだ形で使うことが多いので、これを別名としておくと便利です。それに、このライブラリを使う側から見ればそれが内部的にFree
を使っているかどうかは興味のないことなので、隠してしまうという意味もあります。
type Graphics a = Free GraphicsF a
type
(型の別名)だと中身がFree
だというのがモロ見えになってしまうので、newtype
で新しい型を与えて中身を隠してあげるほうが好きな人もいるでしょう。
newtype Graphics a = Graphics (Free GraphicsF a)
たぶん実用上は後者のほうがいいと思いますが、ここでは簡単のために前者のtype
のほうにしておきます。
命令
Freeモナドのライブラリには次のような関数liftF
が定義されています。
liftF :: forall f a. f a -> Free f a
liftF
は任意のf a
をFree f a
という値に『持ち上げる』(lift)関数で、つまりGraphicsF a
をFree GraphicsF a
に変えることができます。つぎにGraphicsF
の各コンストラクタをこのliftF
で持ち上げ、実際のFreeモナドの値を返すような関数にします。各コンストラクタの最後のフィールドはunit
やid
で埋めておきます。
setFillStyle :: String -> Graphics Unit
setFillStyle s = liftF $ SetFillStyle s unit
measureText :: String -> Graphics TextMetrics
measureText s = liftF $ MeasureText s id
fillText :: String -> Number -> Number -> Graphics Unit
fillText s x y = liftF $ FillText s x y unit
ここもめんどうくさいボイラープレートなのですが、PureScriptにはTemplate Haskellのような邪悪なメタプログラミングの仕組みがないので、善の魔法使いである我々はひたすら脳が退化しそうな単純作業に没頭します。命令が3つくらいなら大丈夫ですが、命令の数が多いとここでげんなりしてきます。
これが終われば、命令列を領域特化言語として構成することができるようになりますが、まだこの命令列を実際に実行する機能は与えられていません。
解釈関数
最後に、データとして与えられた命令群を実際に解釈して別の何かへと変える関数を実装します。ここでは関数runGraphics
を定義してGraphics
をPureScriptのネイティブな作用の型Eff eff a
に変換して実際に実行可能にします。Free f a
の値を変換するには、*本来なら(後述)*次のようなfoldFree
という関数を使います。
foldFree :: forall f m a. (Functor f, MonadRec m) => NaturalTransformation f m -> Free f a -> m a
ここでFunctor f
の制約があるため。GraphicsF
にFunctor
のインスタンスが必要だったわけです。
runFreeM
の最初の引数に渡す関数interpret
をrunGraphics
の内部で定義しますが、interpret
ではGraphicsF
のフィールドからデータを取り出して実際の作用を起こすための関数にせっせ移し替え、あとは型の辻褄を合わせるためにconst a <$>
やk <$>
みたいな式を機械的にくっつけます。interpret
の実装も単調な作業です。
runGraphics :: forall a eff. Context2D -> Graphics a -> Eff (canvas :: Canvas | eff) a
runGraphics ctx = foldFree interpret
where
interpret :: Natural GraphicsF (Eff (canvas :: Canvas | eff))
interpret (SetFillStyle s a) = const a <$> _setFillStyle s ctx
interpret (MeasureText s k) = k <$> _measureText s ctx
interpret (FillText s x y a) = const a <$> _fillText s x y ctx
Natural
という謎の型コンストラクタが出てきていますが、これは次のような関数の型の別名です。
type Natural f g = forall a. f a -> g a
こういうのを自然変換(Natural Transformation)というそうで、型が示している通りinterpret
は関手GraphicsF
を関手Eff (canvas :: Canvas | eff)
に変換しているということを意味しています。関数型プログラミングの界隈では圏論の言葉を使って肝試しをするのが流行っていますが、幽霊の正体を見てみたら枯れ尾花だったりすることもよくあるわけで、ここでビビって帰ってしまったら負けです。圏論なんてよくわかりませんが、とにかく型とにらめっこして型パズルを解きさえすればこっちのものです。
作業はこれで完了です。それなりの作業量はあるものの、一旦理解してしまえばここまでの手順はまったくの単純作業です。時給800円くらいで領域特化言語を定義するアルバイト(未経験可)とかありそうです。
ここではEff eff a
に変換して実際に描画する翻訳関数だけを定義しましたが、これとは全然別の複数の翻訳関数を与えることもできます。たとえば、各命令の呼び出しをロギングしたりとか、あるいはリモートのターゲットに対しての描画命令に変換したりできるかもしれません。活かすも殺すも解釈関数しだいです。
CoyonedaでFunctorを作る
先ほどの過程では、GraphicsF
のFunctor
インスタンスを自力で定義しなければならないという面倒くさい手順がありました。Haskellだとインスタンスの自動導出でこれを避ける事ができますが、PureScriptではそうはいきません。ところが、インスタンスの導出がなくてもうまい回避策があって、Coyoneda
というデータ型を使うと、どんなデータ型からでも勝手にFunctor
が構成できてしまうのです。Coyoneda
の値を作るのは簡単で、次のliftCoyoneda
という関数を適用するだけです。
liftCoyoneda :: forall f a. f a -> Coyoneda f a
f
に何の制約もありません。どんな値f a
でもliftCoyoneda
であっさりとCoyoneda f a
の値にすることができてしまいます。そして、
instance functorCoyoneda :: Functor (Coyoneda f)
というように、f
に何の制約もなくこのCoyoneda
はFunctor
なのです。こわい。
それでは、今度はFree
に直接GraphicsF
を埋め込むのではなく、代わりにCoyoneda GraphicsF
を埋め込んでみます。
type Graphics a = Free (Coyoneda GraphicsF) a
それぞれの命令の定義も、間にliftCoyoneda
を挟んで持ち上げるだけ。
setFillStyle :: String -> Graphics Unit
setFillStyle s = liftF $ liftCoyoneda $ SetFillStyle s unit
measureText :: String -> Graphics TextMetrics
measureText s = liftF $ liftCoyoneda $ MeasureText s id
fillText :: String -> Number -> Number -> Graphics Unit
fillText s x y = liftF $ liftCoyoneda $ FillText s x y unit
runGraphics
も、解釈関数interpret
をliftCoyonedaTF
で持ち上げるだけ。よっこいしょ。
runGraphics :: forall a eff. Context2D -> Graphics a -> Eff (canvas :: Canvas | eff) a
runGraphics ctx = runFreeM $ liftCoyonedaTF interpret
where
interpret :: Natural GraphicsF (Eff (canvas :: Canvas | eff))
interpret (SetFillStyle s a) = const a <$> _setFillStyle s ctx
interpret (MeasureText s k) = k <$> _measureText s ctx
interpret (FillText s x y a) = const a <$> _fillText s x y ctx
Coyoneda
を絡めるだけで、GraphicsF
にFunctor
インスタンスを与えずに領域特化言語を定義できてしまいました。
Functor
でないただのデータ型でFunctor
を構成できるとかどう見ても超常現象ですし、おそらくCoyonedaは何か強力な魔術の類だと思うのですが、残念ながら筆者の魔力が不足しているためこれ以上のことはわかりませんでした。ふしぎ!
purescript-freeの実装
Haskellのekmett/freeのほうでは、Free f a
のモナドインスタンスは
instance Functor f => Monad (Free f)
というようにf
にFunctor f
の制約がかかっていて、GraphicsF a
のような型はFunctor
のインスタンスがないとFree GraphicsF a
がモナドになりません。これが本来のシンプルなFreeモナドなのですが、実はpurescript-freeの実装では最初からCoyoneda
に相当する構造が埋め込まれているため、GraphicsF a
にFunctor
のインスタンスは必要ありません。先程はfoldFree
にFunctor f
の制約がかかっているというようなことを言いましたが、それは古いバージョンの話で、現在では以下のようにFunctor f
の制約はなくなっています。
foldFree :: forall f m a. (MonadRec m) => NaturalTransformation f m -> Free f a -> m a
同様にFree f
のモナドインスタンスのほうもFunctor f
の制約がなくなっています。
instance freeMonad :: Monad (Free f)
こうなっているのはどうも最適化の都合のようです。というわけで、purescript-freeを使う場合は明示的にCoyonedaでこよこよする必要はありません。
さいごに
特定の問題領域に特化した言語を作るのは、ソフトウェアの堅牢さを保証する最も強力な手法のうちのひとつだと思います。そして、Free
を使えば領域特化言語を作るのは決して難しくはありません。
単にコードの一部に制約を加えたいということであれば、PureScriptでは他にもExtensible Effectsで制御する方法があります。Freeモナドでも複数の種類の作用を混ぜる方法がありますが、他の作用と混ぜたいという場合はExtensible Effectsのほうが便利なときも多いでしょうし、解釈を動的に切り替えたいという場合はやはりFreeモナドを選ぶことになるでしょう。この辺りは目的に応じて使い分けることになると思います。
説明の多くはWhy free monads matter - Haskell for allを参考にしています。Freeモナドの中身にあまり興味がなくて、とにかく領域特化言語を定義するために道具として使えれば何でもいいやという人もいるでしょうし、Freeモナドがそもそもどのように定義されているのかという説明はこのテキストでは意図的にすべてばっさり省きました。"Why free monads matter"では、そもそもFreeモナドそのものはどのように定義されているのか、どのような考え方で導出するのかとか、FreeモナドはFunctorの不動点なんだとか、Freeモナドで並列処理っぽいものを実装してみるというような面白い話題が数多く紹介されています。興味のある人は是非読んでみることをおすすめします。
ところで、ここで作った言語というのはあくまでPureScriptの言語の構文の範疇にありますが、こう言ったものは内部領域特化言語(internal domain-specific language, 内部DSL)と呼ばれているようです。領域特化言語というと、筆者としてはCSSやGLSLみたいに、母体となる言語とは互換性のない全然別の言語というイメージなのですが、こういうCSSのような元の言語と互換性のないものは外部DSL(external domain-specific language)とというようです。でも内部領域特化言語といってもどうせ単なるモナドですし、これを領域特化言語なんて大仰な名前で呼んでいいものかという気もします。だっていつものPureScriptのソースコードなんですから。そんなに大袈裟なことはしていません。
キャンペーンおよび追加情報
- FRP特集:話題のファンクショナル・リアクティブ・プログラミングを体験したいかたに、purescript-halogenがおすすめ!purescript-freeも内部で使われています。
この文章を読んだ人はこんな文章も読んでいます
-
Gabriel Gonzalez, Why free monads matter - Haskell for all
-
Edward Kmett, Monads for Free
-
Phil Freeman, Stack Safety for Free
-
Phil Freeman, PureScript by Example/14. Domain-Specific Languages
-
Extensible Effects in Scala 日本語!Scala!
-
A simple crud DSL with an interpreter written as cofree commonad in Purescript