領域特化言語

この章の目標

この章では多数の標準的な手法を使い、PureScriptにおける領域特化言語(またはDSL)の実装について探求していきます。

領域特化言語とは、特定の問題領域での開発に適した言語のことです。 構文及び機能は、その領域内の考え方を表現するに使われるコードの読みやすさを最大化すべく選択されます。 本書の中では、既に領域特化言語の例を幾つか見てきています。

  • 第11章で開発されたGameモナドと関連する動作は、テキストアドベンチャーゲーム開発という領域に対しての領域特化言語を構成しています。
  • 第13章で扱ったquickcheckパッケージは、生成的テスティングの領域に向けた領域特化言語です。 このコンビネータはテストの性質に対して特に表現力の高い記法を可能にします。

この章では、領域特化言語の実装において、幾つかの標準的な技法にについて構造的な手法を取ります。 この話題の完全な解説では決してありませんが、目的に合う実践的なDSLを構築するのに充分な知識は得られるでしょう。

ここでの実行例はHTML文書を作成するための領域特化言語です。 正しいHTML文書を記述するための型安全な言語を開発することが目的で、素朴な実装を徐々に改善しつつ進めていきます。

プロジェクトの準備

この章に付随するプロジェクトには新しい依存性が1つ追加されます。 これから使う道具の1つであるFreeモナドが定義されているfreeライブラリです。

このプロジェクトをPSCiを使って試していきます。

HTMLデータ型

このHTMLライブラリの最も基本的なバージョンは Data.DOM.Simpleモジュールで定義されています。このモジュールには次の型定義が含まれています。

newtype Element = Element
  { name         :: String
  , attribs      :: Array Attribute
  , content      :: Maybe (Array Content)
  }

data Content
  = TextContent String
  | ElementContent Element

newtype Attribute = Attribute
  { key          :: String
  , value        :: String
  }

Element型はHTMLの要素を表します。 各要素は要素名、属性の対の配列と、内容で構成されます。 内容のプロパティにはMaybe型を適切に使い、要素が開いている(他の要素やテキストを含む)か閉じているかを示します。

このライブラリの鍵となる機能は次の関数です。

render :: Element -> String

この関数はHTML要素をHTML文字列として出力します。 PSCiで明示的に適当な型の値を構築し、ライブラリのこのバージョンを試してみましょう。

$ spago repl

> import Prelude
> import Data.DOM.Simple
> import Data.Maybe
> import Effect.Console

> :paste
… log $ render $ Element
…   { name: "p"
…   , attribs: [
…       Attribute
…         { key: "class"
…         , value: "main"
…         }
…     ]
…   , content: Just [
…       TextContent "Hello World!"
…     ]
…   }
… ^D

<p class="main">Hello World!</p>
unit

現状のライブラリには幾つもの問題があります。

  • HTML文書の作成に手が掛かります。 全ての新しい要素に少なくとも1つのレコードと1つのデータ構築子が必要です。
  • 無効な文書を表現できてしまいます。
    • 開発者が要素名の入力を間違えるかもしれません
    • 開発者が属性を間違った要素に関連付けることができてしまいます
    • 開いた要素が正しい場合に開発者が閉じた要素を使えてしまいます

残りの章ではとある手法を用いてこれらの問題を解決し、このライブラリーをHTML文書を作成するために使える領域特化言語にしていきます。

スマート構築子

最初に導入する手法は単純ですがとても効果的です。 モジュールの使用者にデータの表現を露出する代わりに、モジュールエクスポートリストを使ってデータ構築子ElementContentAttributeを隠蔽します。 そして正しいことが分かっているデータを構築する、いわゆるスマート構築子だけをエクスポートします。

例を示しましょう。まず、HTML要素を作成するための便利な関数を提供します。

element :: String -> Array Attribute -> Maybe (Array Content) -> Element
element name attribs content = Element
  { name:      name
  , attribs:   attribs
  , content:   content
  }

次にHTML要素のためのスマート構築子を作成します。 この要素は利用者がelement関数を適用して作成できるようになってほしいものです。

a :: Array Attribute -> Array Content -> Element
a attribs content = element "a" attribs (Just content)

p :: Array Attribute -> Array Content -> Element
p attribs content = element "p" attribs (Just content)

img :: Array Attribute -> Element
img attribs = element "img" attribs Nothing

最後に、正しいデータ構造だけが構築されることがわかっているこれらの関数をエクスポートするように、モジュールエクスポートリストを更新します。

module Data.DOM.Smart
  ( Element
  , Attribute(..)
  , Content(..)

  , a
  , p
  , img

  , render
  ) where

モジュールエクスポートリストはモジュール名の直後の括弧内に書きます。 各モジュールのエクスポートは次の3種類の何れかになります。

  • 値(ないし関数)。その値の名前により指定されます。
  • 型クラス。クラス名により指定されます。
  • 型構築子とそれに紐付くデータ構築子。 型名とそれに続くエクスポートされるデータ構築子の括弧で囲まれたリストで指定されます。

ここでは、Elementをエクスポートしていますが、データ構築子はエクスポートしていません。 もしデータ構築子をエクスポートすると、使用者が不正なHTML要素を構築できてしまいます。

AttributeContent型についてはデータ構築子を全てエクスポートしています(エクスポートリストの記号 ..で示されています)。 すぐ後で、これらの型にもスマート構築子の手法を適用していきます。

既にライブラリに幾つもの大きな改良が加わっていることに注目です。

  • 不正な名前を持つHTML要素は表現できません(勿論ライブラリが提供する要素名に制限されています)。
  • 閉じた要素は構築するときに内容を含められません。

Content型にとても簡単にこの手法を適用できます。 単にエクスポートリストからContent型のデータ構築子を取り除き、次のスマート構築子を提供します。

text :: String -> Content
text = TextContent

elem :: Element -> Content
elem = ElementContent

Attribute型にも同じ手法を適用してみましょう。 まず、属性のための汎用のスマート構築子を用意します。 以下は最初の試行です。

attribute :: String -> String -> Attribute
attribute key value = Attribute
  { key: key
  , value: value
  }

infix 4 attribute as :=

この定義では元のElement型と同じ問題に直面しています。 存在しなかったり、名前が間違って入力された属性を表現できます。 この問題を解決するために、属性名を表すnewtypeを作成します。

newtype AttributeKey = AttributeKey String

これを使えば演算子を次のように変更できます。

attribute :: AttributeKey -> String -> Attribute
attribute (AttributeKey key) value = Attribute
  { key: key
  , value: value
  }

AttributeKeyデータ構築子をエクスポートしなければ、明示的にエクスポートされた次のような関数を使う以外に、使用者が型 AttributeKeyの値を構築する方法はありません。 以下に幾つかの例を示します。

href :: AttributeKey
href = AttributeKey "href"

_class :: AttributeKey
_class = AttributeKey "class"

src :: AttributeKey
src = AttributeKey "src"

width :: AttributeKey
width = AttributeKey "width"

height :: AttributeKey
height = AttributeKey "height"

新しいモジュールの最終的なエクスポートリストは次のようになります。 最早どのデータ構築子も直接エクスポートしていない点に注目です。

module Data.DOM.Smart
  ( Element
  , Attribute
  , Content
  , AttributeKey

  , a
  , p
  , img

  , href
  , _class
  , src
  , width
  , height

  , attribute, (:=)
  , text
  , elem

  , render
  ) where

PSCiでこの新しいモジュールを試してみると、既にコードの簡潔さにおいて大幅な向上が見て取れます。

$ spago repl

> import Prelude
> import Data.DOM.Smart
> import Effect.Console
> log $ render $ p [ _class := "main" ] [ text "Hello World!" ]

<p class="main">Hello World!</p>
unit

しかし、基盤をなすデータ表現は全く変更されなかったので、render関数を変更する必要はなかったことにも注目してください。 これはスマート構築子による手法の利点のひとつです。 外部APIの使用者によって認識される表現から、モジュールの内部データ表現を分離できるのです。

演習

  1. (簡単)Data.DOM.Smartモジュールで renderを使った新しいHTML文書の作成を試してみましょう。

  2. (普通)checkeddisabledといったHTML属性は値を要求せず、空の属性として書き出せます。

    <input disabled>
    

    空の属性を扱えるように Attributeの表現を変更してください。 要素に空の属性を追加するためのattributeまたは:=の代わりに使える関数を記述してください。

幻影型

次の手法の動機付けとして、以下のコードを考えます。

> log $ render $ img
    [ src    := "cat.jpg"
    , width  := "foo"
    , height := "bar"
    ]

<img src="cat.jpg" width="foo" height="bar" />
unit

ここでの問題は、 width属性とheight属性に文字列値を提供しているということです。 ここで与えることができるのはピクセル単位ないしパーセントの数値だけであるべきです。

AttributeKey型にいわゆる 幻影型 (phantom type) 引数を導入すると、この問題を解決できます。

newtype AttributeKey a = AttributeKey String

定義の右辺に対応する型 aの値が存在しないので、この型変数 a幻影型と呼ばれています。 この型 aはコンパイル時に追加の情報を提供するためだけに存在しています。 型AttributeKey aの任意の値は実行時には単なる文字列ですが、コンパイル時はその値の型により、このキーに関連する値で求められる型がわかります。

attribute関数の型を次のように変更すれば、AttributeKeyの新しい形式を考慮するようにできます。

attribute :: forall a. IsValue a => AttributeKey a -> a -> Attribute
attribute (AttributeKey key) value = Attribute
  { key: key
  , value: toValue value
  }

ここで、幻影型の引数 aは、属性キーと属性値が照応する型を持っていることを確認するために使われます。 使用者は AttributeKey aの型の値を直接作成できないので(ライブラリで提供されている定数を介してのみ得られます)、全ての属性が構築により正しくなります。

なお、IsValue制約はキーに関連付けられた値の型が何であれその値を文字列に変換し、生成したHTML内に出力できることを保証します。 IsValue型クラスは次のように定義されています。

class IsValue a where
  toValue :: a -> String

StringInt型についての型クラスインスタンスも提供しておきます。

instance IsValue String where
  toValue = id

instance IsValue Int where
  toValue = show

また、これらの型が新しい型変数を反映するように、 AttributeKey定数を更新しなければいけません。

href :: AttributeKey String
href = AttributeKey "href"

_class :: AttributeKey String
_class = AttributeKey "class"

src :: AttributeKey String
src = AttributeKey "src"

width :: AttributeKey Int
width = AttributeKey "width"

height :: AttributeKey Int
height = AttributeKey "height"

これで、不正なHTML文書を表現することが不可能になっていることがわかります。 また、widthheight属性を表現するのに文字列ではなく数を使うことが強制されていることがわかります。

> import Prelude
> import Data.DOM.Phantom
> import Effect.Console

> :paste
… log $ render $ img
…   [ src    := "cat.jpg"
…   , width  := 100
…   , height := 200
…   ]
… ^D

<img src="cat.jpg" width="100" height="200" />
unit

演習

  1. (簡単)ピクセルまたはパーセントの何れかの長さを表すデータ型を作成してください。 その型についてIsValueのインスタンスを書いてください。 この新しい型を使うようにwidthheight属性を変更してください。

  2. (難しい)真偽値truefalse用の最上位の表現を定義することで、幻影型を使ってAttributeKeydisabledcheckedのような空の属性を表現しているかどうかをエンコードできます。

    data True
    data False
    

    幻影型を使って、使用者が attribute演算子を空の属性に対して使うことを防ぐように、前の演習の解答を変更してください。

Freeモナド

APIに施す最後の変更では、Content型をモナドにしてdo記法を使えるようにするために、Freeモナドと呼ばれる構造を使っていきます。 これによって入れ子になった要素がわかりやすくなるような形式でHTML文書を構造化できます。 以下の代わりに……

p [ _class := "main" ]
  [ elem $ img
      [ src    := "cat.jpg"
      , width  := 100
      , height := 200
      ]
  , text "A cat"
  ]

このように書くことができるようになります。

p [ _class := "main" ] $ do
  elem $ img
    [ src    := "cat.jpg"
    , width  := 100
    , height := 200
    ]
  text "A cat"

しかし、do記法だけがFreeモナドの恩恵ではありません。 Freeモナドがあれば、モナドの動作の表現をその解釈から分離し、同じ動作に複数の解釈を持たせることさえできます。

FreeモナドはfreeライブラリのControl.Monad.Freeモジュールで定義されています。 PSCiを使うと、次のようにFreeモナドについての基本的な情報を見ることができます。

> import Control.Monad.Free

> :kind Free
(Type -> Type) -> Type -> Type

Freeの種は、引数として型構築子を取り、別の型構築子を返すことを示しています。 実はなんと、Freeモナドを使えば任意のFunctorMonadにできるのです。

モナドの動作の表現の定義から始めます。 これには対応したい各モナド動作について、1つのデータ構築子を持つFunctorを作成する必要があります。 今回の場合、2つのモナドの動作はelemtextになります。 Content型を次のように変更するだけでできます。

data ContentF a
  = TextContent String a
  | ElementContent Element a

instance Functor ContentF where
  map f (TextContent s x) = TextContent s (f x)
  map f (ElementContent e x) = ElementContent e (f x)

ここで、このContentF型構築子は以前のContentデータ型とよく似ています。 しかし、ここでは型引数aを取り、それぞれのデータ構築子は型aの値を追加の引数として取るように変更されています。 Functorインスタンスでは、単に各データ構築子で型aの値に関数fを適用します。

これにより、新しいContentモナドをFreeモナド用の型同義語として定義できます。 これは最初の型引数として ContentF型構築子を使うことで構築されます。

type Content = Free ContentF

型同義語の代わりにnewtypeを使用して、使用者に対してライブラリの内部表現を露出することを避けられます。 Contentデータ構築子を隠すことで、提供しているモナドの動作だけを使うことを使用者に制限しています。

ContentFFunctorなので、 Free ContentF用のMonadインスタンスが自動的に手に入ります。

Contentの新しい型引数を考慮するようにElementデータ型を僅かに変更する必要があります。 モナドの計算の戻り値の型が Unitであることだけが必要です。

newtype Element = Element
  { name         :: String
  , attribs      :: Array Attribute
  , content      :: Maybe (Content Unit)
  }

また、Contentモナドについての新しいモナドの動作になるよう、elemtext関数を変更する必要があります。 これにはControl.Monad.Freeモジュールで提供されている liftF関数が使えます。 以下がその型です。

liftF :: forall f a. f a -> Free f a

liftFにより、何らかの型aについて、型f aの値からFreeモナドの動作を構築できるようになります。 今回の場合、ContentF型構築子のデータ構築子をそのまま使えます。

text :: String -> Content Unit
text s = liftF $ TextContent s unit

elem :: Element -> Content Unit
elem e = liftF $ ElementContent e unit

他にも同じようなコードの変更はありますが、興味深い変更は render関数にあります。ここでは、このFreeモナドを 解釈 しなければいけません。

モナドの解釈

Control.Monad.Freeモジュールでは、Freeモナドで計算を解釈するための多数の関数が提供されています。

runFree
  :: forall f a
   . Functor f
  => (f (Free f a) -> Free f a)
  -> Free f a
  -> a

runFreeM
  :: forall f m a
   . (Functor f, MonadRec m)
  => (f (Free f a) -> m (Free f a))
  -> Free f a
  -> m a

runFree関数は、純粋な結果を計算するために使用されます。 runFreeM関数があればFreeモナドの動作を解釈するためにモナドが使えます。

補足:厳密には、より強いMonadRec制約を満たすモナドmに制限されています。 実際、らmは安全な末尾再帰モナドに対応してため、スタックオーバーフローを心配する必要はありません。

まず、動作を解釈できるモナドを選ばなければなりません。 Writer Stringモナドを使って、結果のHTML文字列を累算することにします。

新しいrenderメソッドが開始すると、補助関数 renderElementに移譲し、execWriterを使ってWriterモナドで計算します。

render :: Element -> String
render = execWriter <<< renderElement

renderElementはwhereブロックで定義されます。

  where
    renderElement :: Element -> Writer String Unit
    renderElement (Element e) = do

renderElementの定義は直感的で、複数の小さな文字列を累算するためにWriterモナドのtell動作を使っています。

      tell "<"
      tell e.name
      for_ e.attribs $ \x -> do
        tell " "
        renderAttribute x
      renderContent e.content

次に、renderAttribute関数を定義します。 こちらも同じくらい単純です。

    where
      renderAttribute :: Attribute -> Writer String Unit
      renderAttribute (Attribute x) = do
        tell x.key
        tell "=\""
        tell x.value
        tell "\""

renderContent関数は、もっと興味深いものです。 ここではrunFreeM関数を使い、Freeモナドの内部で計算を解釈しています。 計算は補助関数 renderContentItemに移譲しています。

      renderContent :: Maybe (Content Unit) -> Writer String Unit
      renderContent Nothing = tell " />"
      renderContent (Just content) = do
        tell ">"
        runFreeM renderContentItem content
        tell "</"
        tell e.name
        tell ">"

renderContentItemの型は runFreeMの型シグネチャから推測できます。 関手 fは型構築子 ContentFで、モナド mは解釈している計算のモナド、つまり Writer Stringです。 これにより renderContentItemは次の型シグネチャだとわかります。

      renderContentItem :: ContentF (Content Unit) -> Writer String (Content Unit)

ContentFの2つのデータ構築子でパターン照合すればこの関数を実装できます。

      renderContentItem (TextContent s rest) = do
        tell s
        pure rest
      renderContentItem (ElementContent e rest) = do
        renderElement e
        pure rest

それぞれの場合において、式restは型Content Unitを持っており、解釈された計算の残りを表しています。 rest動作を返すことでそれぞれの場合を完成できます。

できました。 PSCiで、次のようにすれば新しいモナドのAPIを試すことができます。

> import Prelude
> import Data.DOM.Free
> import Effect.Console

> :paste
… log $ render $ p [] $ do
…   elem $ img [ src := "cat.jpg" ]
…   text "A cat"
… ^D

<p><img src="cat.jpg" />A cat</p>
unit

演習

  1. (普通)ContentF型に新しいデータ構築子を追加して、生成されたHTMLにコメントを出力する新しい動作commentに対応してください。 liftFを使ってこの新しい動作を実装してください。 新しい構築子を適切に解釈するように、解釈renderContentItemを更新してください。

言語の拡張

全動作が型Unitの何かを返すようなモナドは、さほど興味深いものではありません。 実際のところ、概ね良くなったと思われる構文は別として、このモナドはMonoid以上の機能を何ら追加していません。

非自明な結果を返す新しいモナド動作でこの言語を拡張することで、Freeモナドを構築する威力をお見せしましょう。

アンカーを使用して文書の様々な節へのハイパーリンクが含まれているHTML文書を生成したいとします。 手作業でアンカーの名前を生成して文書中で少なくとも2回それらを含めれば、これは達成できます。 1つはアンカーの定義自身に、もう1つは各ハイパーリンクにあります。 しかし、この手法には基本的な問題が幾つかあります。

  • 開発者が一意なアンカー名の生成をし損なうかもしれません。
  • 開発者がアンカー名を1つ以上の箇所で打ち間違うかもしれません。

開発者が誤ちを犯すことを防ぐために、アンカー名を表す新しい型を導入し、新しい一意な名前を生成するためのモナド動作を提供できます。

最初の工程は名前の型を新しく追加することです。

newtype Name = Name String

runName :: Name -> String
runName (Name n) = n

繰り返しになりますが、NameStringのnewtypeとして定義しているものの、モジュールのエクスポートリスト内でデータ構築子をエクスポートしないように注意する必要があります。

次に、属性値にNameを使えるよう、新しい型にIsValue型クラスのインスタンスを定義します。

instance IsValue Name where
  toValue (Name n) = n

また、次のように a要素に現れるハイパーリンク用の新しいデータ型を定義します。

data Href
  = URLHref String
  | AnchorHref Name

instance IsValue Href where
  toValue (URLHref url) = url
  toValue (AnchorHref (Name nm)) = "#" <> nm

この新しい型により、href属性の型の値を変更して、利用者にこの新しい Href型の使用を強制できます。 また、新しいname属性も作成でき、要素をアンカーに変換するのに使えます。

href :: AttributeKey Href
href = AttributeKey "href"

name :: AttributeKey Name
name = AttributeKey "name"

残っている問題は、現在モジュールの使用者が新しい名前を生成する方法がないということです。 Contentモナドでこの機能を提供できます。まず、 ContentF型構築子に新しいデータ構築子を追加する必要があります。

data ContentF a
  = TextContent String a
  | ElementContent Element a
  | NewName (Name -> a)

NewNameデータ構築子は型Nameの値を返す動作に対応しています。 データ構築子の引数としてNameを要求するのではなく、型Name -> a関数を提供するように使用者に要求していることに注意してください。 型a計算の残りを表していることを思い出すと、この関数は、型Nameの値が返されたあとで、計算を継続する方法を提供しているのだとわかります。

新しいデータ構築子を考慮するよう、次のようにContentF用のFunctorインスタンスを更新する必要もあります。

instance Functor ContentF where
  map f (TextContent s x) = TextContent s (f x)
  map f (ElementContent e x) = ElementContent e (f x)
  map f (NewName k) = NewName (f <<< k)

これで、以前と同じようにliftF関数を使って新しい動作を構築できます。

newName :: Content Name
newName = liftF $ NewName id

id関数を継続として提供していることに注意してください。 つまり型Nameの結果を変更せずに返しています。

最後に、新しい動作を解釈させるように解釈関数を更新する必要があります。 以前は計算を解釈するためにWriter Stringモナドを使っていましたが、このモナドは新しい名前を生成できないので、何か他のものに切り替えなければなりません。 WriterTモナド変換子をStateモナドと一緒に使うと、必要な作用を組み合わせられます。 型注釈が短く保たれるよう、この解釈モナドを型同義語として定義できます。

type Interp = WriterT String (State Int)

ここで、Int型の状態は増加していくカウンタとして振舞い、一意な名前を生成するのに使われます。

WriterWriterTモナドはそれらの動作を抽象化するのに同じ型クラスの構成要素を使うので、どの動作も変更する必要がありません。 必要なのは、Writer Stringへの参照全てをInterpで置き換えることだけです。 しかし、計算に使われる制御子は変更する必要があります。 単なるexecWriterの代わりに、evalStateも使う必要があります。

render :: Element -> String
render e = evalState (execWriterT (renderElement e)) 0

また、新しい NewNameデータ構築子を解釈するために、 renderContentItemに新しい場合を追加しなければいけません。

renderContentItem (NewName k) = do
  n <- get
  let fresh = Name $ "name" <> show n
  put $ n + 1
  pure (k fresh)

ここで、型 Name -> Content aの継続 kが与えられているので、型 Content aの解釈を構築しなければいけません。 この解釈は単純です。 getを使って状態を読み、その状態を使って一意な名前を生成し、それから putで状態に1だけ足すのです。 最後に、継続にこの新しい名前を渡して、計算を完了します。

以上をもって、この新しい機能をPSCiで試せます。 これにはContentモナドの内部で一意な名前を生成し、要素の名前とハイパーリンクのリンク先の両方として使います。

> import Prelude
> import Data.DOM.Name
> import Effect.Console

> :paste
… render $ p [ ] $ do
…   top <- newName
…   elem $ a [ name := top ] $
…     text "Top"
…   elem $ a [ href := AnchorHref top ] $
…     text "Back to top"
… ^D

<p><a name="name0">Top</a><a href="#name0">Back to top</a></p>
unit

複数回のnewNameの呼び出しの結果が、実際に一意な名前になっていることも確かめられます。

演習

  1. (普通)使用者から Element型を隠蔽すると、更にAPIを簡素にできます。 次の手順に従って、これらの変更を加えてください。

    • pimgのような(返る型がElementの)関数をelem動作と結合して、型Content Unitを返す新しい動作を作ってください。
    • Elementの代わりに型Content Unitの引数を受け付けるようにrender関数を変更してください。
  2. (普通)型同義語の代わりにnewtypeを使ってContentモナドの実装を隠してください。 newtype用のデータ構築子はエクスポートすべきではありません。

  3. (難しい)ContentF型を変更して以下の新しい動作に対応してください。

    isMobile :: Content Boolean
    

    この動作は、この文書がモバイルデバイス上での表示のために描画されているかどうかを示す真偽値を返します。

    手掛かりask動作とReaderTモナド変換子を使って、この動作を解釈してください。 あるいは、RWSモナドを使うほうが好みの人もいるかもしれません。

まとめ

この章では、幾つかの標準的な技術を使って、素朴な実装を段階的に改善することにより、HTML文書を作成するための領域特化言語を開発しました。

  • スマート構築子を使ってデータ表現の詳細を隠し、利用者には構築により正しい文書だけを作ることを許しました。
  • 独自に定義された中置2引数演算子を使い、言語の構文を改善しました。
  • 幻影型を使ってデータの型の中に追加の情報を折り込みました。 これにより利用者が誤った型の属性値を与えることを防いでいます。
  • Freeモナドを使って内容の集まりの配列表現をdo記法に対応したモナドな表現に変えました。 それからこの表現を新しいモナド動作に対応するよう拡張し、標準的なモナド変換子を使ってモナドの計算を解釈しました。

これらの手法は全て、使用者が間違いを犯すのを防いだり領域特化言語の構文を改良したりするために、PureScriptのモジュールと型システムを活用しています。

関数型プログラミング言語による領域特化言語の実装は活発に研究されている分野です。 それでも、幾つかの単純な技法に対して役に立つ導入を提供し、表現力豊かな型を持つ言語で作業することの威力を示すことができていれば幸いです。