LoginSignup
55
52

More than 5 years have passed since last update.

Freeモナドで領域特化言語を作るとプリティミューテーション

Last updated at Posted at 2015-09-04

多くのプログラミング言語が用途を限定しない汎用の言語として設計されているのに対して、領域特化言語(ドメイン固有言語, 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だと

JavaScript
context.measureText(text)

というようにコンテキストも指定しなければなりませんが、ここでは

measureText text

だけの指定で呼び出せています。どのコンテキストに対して描画命令を出すかという構造も分離されているのです。コンテキストを渡さなくて済むのは、単にコーディングの手間が省けるという意味でも便利です。念の為に言っておきますが、決してグラフィックスコンテキストをグローバルに共有しているわけではありません。

また、すでにHaskellやPureScriptを使っている方には言うまでもないようなことですが、同じ命令を繰り返したければいつものforfor_を再利用することができます。領域特化言語専用のforを改めて定義する必要はありません。if-then-elsewhenもいつもどおりです。

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つの命令だけ用意することにしましょう。

TypeScript
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と組み合わせるにはGraphicsFFunctorのインスタンスが(ひとまずは)必要です。ここがちょっとめんどうくさいですが、それぞれのコンストラクタの最後の追加のフィールドのところに関数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というようにFreeGraphicsFを埋め込んだ形で使うことが多いので、これを別名としておくと便利です。それに、このライブラリを使う側から見ればそれが内部的に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 aFree f aという値に『持ち上げる』(lift)関数で、つまりGraphicsF aFree GraphicsF aに変えることができます。つぎにGraphicsFの各コンストラクタをこのliftFで持ち上げ、実際のFreeモナドの値を返すような関数にします。各コンストラクタの最後のフィールドはunitidで埋めておきます。

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の制約があるため。GraphicsFFunctorのインスタンスが必要だったわけです。

runFreeMの最初の引数に渡す関数interpretrunGraphicsの内部で定義しますが、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を作る

先ほどの過程では、GraphicsFFunctorインスタンスを自力で定義しなければならないという面倒くさい手順がありました。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に何の制約もなくこのCoyonedaFunctorなのです。こわい。

それでは、今度は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も、解釈関数interpretliftCoyonedaTFで持ち上げるだけ。よっこいしょ。

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を絡めるだけで、GraphicsFFunctorインスタンスを与えずに領域特化言語を定義できてしまいました。

Functorでないただのデータ型でFunctorを構成できるとかどう見ても超常現象ですし、おそらくCoyonedaは何か強力な魔術の類だと思うのですが、残念ながら筆者の魔力が不足しているためこれ以上のことはわかりませんでした。ふしぎ!

purescript-freeの実装

Haskellのekmett/freeのほうでは、Free f aのモナドインスタンスは

instance Functor f => Monad (Free f)

というようにfFunctor fの制約がかかっていて、GraphicsF aのような型はFunctorのインスタンスがないとFree GraphicsF aがモナドになりません。これが本来のシンプルなFreeモナドなのですが、実はpurescript-freeの実装では最初からCoyonedaに相当する構造が埋め込まれているため、GraphicsF aFunctorのインスタンスは必要ありません。先程はfoldFreeFunctor 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も内部で使われています。

この文章を読んだ人はこんな文章も読んでいます

最も参考になったカスタマーレビュー

55
52
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
52