実例によるPureScript

ウェブのための関数型プログラミング

Phil Freeman, "PureScript by Example - Functional Programming for the Web"

9 キャンバスグラフィックス

9.1 この章の目標

この章のコード例では、PureScriptでHTML5のCanvas APIを使用して2Dグラフィックスを生成するpurescript-canvasパッケージに焦点をあててコードを拡張していきます。

9.2 プロジェクトの準備

このモジュールのプロジェクトでは、以下のBowerの依存関係が新しく追加されています。

この章のソースコードは、それぞれにmainメソッドが定義されている複数のモ​​ジュールへと分割されています。この章の節の内容はそれぞれ異なるファイルで実装されており、それぞれの節で対応するファイルのmainメソッドを実行できるように、Gruntビルドターゲットを変更することでMainモジュールが変更できるようになっています。

HTMLファイル html/index.htmlには、各例で使用される単一のcanvas要素、およびコンパイルされたPureScriptコードを読み込むscript要素が含まれています。各節のコードをテストするには、ブラウザでこのHTMLファイルを開いてください。

9.3 単純な図形

Rectangle.pursファイルにはキャンバスの中心に青い四角形をひとつ描画するという簡単な例が含まれています。このモジュールは、Control.Monad.Effモジュールと、Canvas APIを扱うためのEffモナドのアクションが定義されているGraphics.Canvasモジュールをインポートします。

他のモジュールでも同様ですが、mainアクションは最初にgetCanvasElementByIdアクションを使ってCanvasオブジェクトへの参照を取得しています。また、getContext2Dアクションを使ってキャンバスの2Dレンダリングコンテキストを参照しています。

main = do
  canvas <- getCanvasElementById "canvas"
  ctx <- getContext2D canvas

これらのアクションの型は psciを使うかドキュメントを見ると確認できます。

getCanvasElementById :: forall eff. String -> Eff (canvas :: Canvas | eff) CanvasElement

getContext2D :: forall eff. CanvasElement -> Eff (canvas :: Canvas | eff) Context2D

CanvasElementContext2DGraphics.Canvasモジュールで定義されている型です。このモジュールでは、モジュール内のすべてのアクションで使用されているCanvas作用も定義されています。

グラフィックスコンテキストctxは、キャンバスの状態を管理し、プリミティブな図形を描画したり、スタイルや色を設定したり、座標変換を適用するためのメソッドを提供しています。

ctxの取得に続けて、setFillStyleアクションを使って塗りのスタイルを青一色の塗りつぶしに設定しています。

  setFillStyle "#0000FF" ctx

setFillStyleアクションがグラフィックスコンテキストを引数として取っていることに注意してください。これはGraphics.Canvasで共通のパターンです。

最後に、fillPathアクションを使用して矩形を塗りつぶしています。fillPathは次のような型を持っています。

fillPath :: forall eff a. Context2D -> 
                          Eff (canvas :: Canvas | eff) a -> 
                          Eff (canvas :: Canvas | eff) a

fillPathはグラフィックスコンテキストとレンダリングするパスを構築する別のアクションを引数にとります。パスはrectアクションを使うと構築することができます。rectはグラフィックスコンテキストと矩形の位置及びサイズを格納するレコードを引数にとります。

  fillPath ctx $ rect ctx
    { x: 250
    , y: 250
    , w: 100
    , h: 100
    }

この長方形のコード例をビルドしましょう。

$ grunt rectangle

それではhtml/index.htmlファイルを開き、このコードによってキャンバスの中央に青い四角形が描画されていることを確認してみましょう。

9.4 行多相を利用する

パスを描画する方法は他にもあります。arc関数は円弧を描画します。moveTo関数、 lineTo関数、closePath関数は細かい線分を組み合わせることでパスを描画します。

Shapes.pursファイルでは長方形と円弧セグメント、三角形の、3つの図形を描画しています。

rect関数は引数としてレコードをとることを見てきました。実際には、長方形のプロパティは型同義語で定義されています。

type Rectangle = { x :: Number
                 , y :: Number
                 , w :: Number
                 , h :: Number 
                 }

xyプロパティは左上隅の位置を表しており、whのプロパティはそれぞれ幅と高さを表しています。

arc関数に以下のような型を持つレコードを渡して呼び出すと、円弧を描画することができます。

type Arc = { x     :: Number
           , y     :: Number
           , r     :: Number
           , start :: Number
           , end   :: Number
           }

ここで、xyプロパティは弧の中心、rは半径、startendは弧の両端の角度を弧度法で表しています。

たとえば、次のコードは中心(300、300)、半径50の円弧を塗りつぶします。

  fillPath ctx $ arc ctx
    { x      : 300
    , y      : 300
    , r      : 50
    , start  : Math.pi * 5 / 8
    , end    : Math.pi * 2
    }

Number型のxyというプロパティがRectangleレコード型とArcレコード型の両方に含まれていることに注意してください。どちらの場合でもこの組は点を表しています。これは、いずれのレコード型にも適用できる、行多相な関数を書くことができることを意味します。

たとえば、 Shapesモジュールではxyのプロパティを変更し図形を並行移動するtranslate関数を定義されています。

translate :: forall r. Number -> Number ->
              { x :: Number, y :: Number | r } ->
              { x :: Number, y :: Number | r }
translate dx dy shape = shape
  { x = shape.x + dx
  , y = shape.y + dy
  }

この行多相型に注目してください。これはtrianglexyというプロパティと、それに加えて他の任意のプロパティを持ったどんなレコードでも受け入れるということを言っています。xフィールドとyフィールドは更新されますが、残りのフィールドは変更されません。

これはレコード更新構文の例です。shape { ... }という式は、shapeを元にして、括弧の中で指定されたように値が更新されたフィールドを持つ新たなレコードを作ります。波括弧の中の式はレコードリテラルのようなコロンではなく、等号でラベルと式を区切って書くことに意してください。

Shapesの例からわかるように、translate関数はRectangleレコードとArcレコード双方に対して使うことができます。

Shapeの例で描画される3つめの型は線分ごとのパスです。対応するコードは次のようになります。

  setFillStyle "#FF0000" ctx

  fillPath ctx $ do
    moveTo ctx 300 260
    lineTo ctx 260 340
    lineTo ctx 340 340
    closePath ctx

ここでは3つの関数が使われています。

このコード片を実行すると、二等辺三角形を塗りつぶされます。

shapesターゲットを使ってこの例をビルドしましょう。

$ grunt shapes

そしてもう一度html/index.htmlを開き、結果を確認して下さい。キャンバスに3つの異なる型が描画されるはずです。

演習

  1. (簡単) これまでの例のそれぞれについて、strokePath関数やsetStrokeStyle関数を使ってみましょう。

  2. (簡単) 関数の引数の内部でdo記法ブロックを使うと、fillPath関数とstrokePath関数で共通のスタイルを持つ複雑なパスを描画することができます。同じfillPath呼び出しで隣り合った2つの矩形を描画するように、Rectangleのコード例を変更してみてください。線分と円弧を組み合わせてを、円の扇形を描画してみてください。

  3. (やや難しい) 次のような2次元の点を表すレコードが与えられたとします。

    type Point = { x :: Number, y :: Number }

    多数の点からなる閉じたパスを描く関数renderPath書いてください。

    renderPath :: forall eff. Context2D -> [Point] -> 
                                           Eff (canvas :: Canvas | eff) Context2D

    次のような関数を考えます。

    f :: Number -> Point

    この関数は引数として1から0の間のNumberをとり、Pointを返します。renderPath関数を利用して関数fのグラフを描くアクションを書いてください。そのアクションは有限個の点をfからサンプリングすることによって近似しなければなりません。

    関数fを変更し、異なるパスが描画されることを確かめてください。

9.5 無作為に円を描く

Random.pursファイルには2種類の異なる副作用が混在したEffモナドを使う例が含まれています。この例では無作為に生成された円をキャンバスに100個描画します。

mainアクションはこれまでのようにグラフィックスコンテキストへの参照を取得し、ストロークと塗りつぶしスタイルを設定します。

  setFillStyle "#FF0000" ctx
  setStrokeStyle "#000000" ctx

次のコードではforEアクションを使って0から100までの整数について繰り返しをしています。

  forE 1 100 $ \_ -> do

それぞれの繰り返しでは、do記法ブロックは3つの乱数を生成することから始まっています。

    x <- random
    y <- random
    r <- random

これらの数は0から1の間に無作為に分布しています。これらはそれぞれx座標、y座標、半径rを表しています。

次のコードでこれらの変数に基づいてArcを作成します。

    let path = arc ctx
         { x     : x * 600
         , y     : y * 600
         , r     : r * 50
         , start : 0
         , end   : Math.pi * 2
         }

そして最後に、現在のスタイルに従って円弧の塗りつぶしと線描が行われます。

    fillPath ctx path
    strokePath ctx path

    return unit

forEに渡された関数が正しい型を持つようにするため、最後の行は必要であることに注意してください。

Gruntfileのrandomターゲットを使用して、この例をビルドします。

$ grunt random

html/index.htmlを開いて、結果を確認してみましょう。

9.6 座標変換

キャンバスは簡単な図形を描画するだけのものではありません。キャンバスは変換行列を扱うことができ、図形は描画の前に形状を変形してから描画されます。図形は平行移動、回転、拡大縮小、および斜め変形することができます。

purescript-canvasライブラリではこれらの変換を以下の関数で提供しています。

translate :: forall eff. TranslateTransform -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D
rotate    :: forall eff. Number             -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D
scale     :: forall eff. ScaleTransform     -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D
transform :: forall eff. Transform          -> Context2D
                                            -> Eff (canvas :: Canvas | eff) Context2D

translateアクションはTranslateTransformレコードのプロパティで指定した大きさだけ平行移動を行います。

rotateアクションは最初の引数で指定されたラジアンの値に応じて原点を中心とした回転を行います。

scaleアクションは原点を中心として拡大縮小します。ScaleTransformレコードはX軸とy軸に沿った拡大率を指定するのに使います。

最後のtransformはこの4つのうちで最も一般的なアクションです。このアクションは行列に従ってアフィン変換を行います。

これらのアクションが呼び出された後に描画される図形は、自動的に適切な座標変換が適用されます。

実際には、これらの関数のそれぞれの作用は、コンテキストの現在の変換行列に対して変換行列を右から乗算していきます。つまり、もしある作用の変換をしていくと、その作用は実際には逆順に適用されていきます。次のような座標変換のアクションを考えてみましょう。

transformations ctx = do
  translate { translateX: 10, translateY } ctx
  scale { scaleX: 2, scaleY: 2 } ctx
  rotate (Math.pi / 2) ctx
  
  renderScene

このアクションの作用では、まずシーンが回転され、それから拡大縮小され、最後に平行移動されます。     

9.7 コンテキストの保存

一般的な使い方としては、変換を適用してシーンの一部をレンダリングし、それからその変換を元に戻します。

Canvas APIにはキャンバスの状態のスタックを操作するsaverestoreメソッドが備わっています。purescript-canvasではこの機能を次のような関数でラップしています。

save    :: forall eff. Context2D -> Eff (canvas :: Canvas | eff) Context2D
restore :: forall eff. Context2D -> Eff (canvas :: Canvas | eff) Context2D

saveアクションは現在のコンテキストの状態(現在の変換行列や描画スタイル)をスタックにプッシュし、restoreアクションはスタックの一番上の状態をポップし、コンテキストの状態を復元します。

これらのアクションにより、現在の状態を保存し、いろいろなスタイルや変換を適用し、プリミティブを描画し、最後に元の変換と状態を復元することが可能になります。例えば、次の関数はいくつかのキャンバスアクションを実行しますが、その前に回転を適用し、そのあとに変換を復元します。

rotated ctx render = do
  save ctx
  rotate Math.pi ctx
  render
  restore ctx

こういったよくある使いかたの高階関数を利用した抽象化として、purescript-canvasライブラリでは元のコンテキスト状態を維持しながらいくつかのキャンバスアクションを実行するwithContext関数が提供されています。

withContext :: forall eff a. Context2D -> 
                             Eff (canvas :: Canvas | eff) a ->
                             Eff (canvas :: Canvas | eff) a          

withContextを使うと、先ほどのrotated関数を次のように書き換えることができます。

rotated ctx render = withContext ctx $ do
  rotate Math.pi ctx
  render

9.8 大域的な変更可能状態

この節ではpurescript-refsパッケージを使ってEffモナドの別の作用について実演してみます。

Control.Monad.Eff.Refモジュールでは大域的に変更可能な参照のための型構築子、および関連する作用を提供します。

> :i Control.Monad.Eff.Ref

> :k RefVal
* -> *

> :k Ref
!

RefVal aの値は型a値を保持する変更可能な領域への参照で、前の章で見たSTRef h aによく似ています。その違いは、ST作用はrunSTを用いて除去することができますが、Ref作用はハンドラを提供しないということです。STは安全に局所的な状態変更を追跡するために使用されますが、Refは大域的な状態変更を追跡するために使用されます。そのため、Refは慎重に使用する必要があります。

Refs.pursファイルにはcanvas要素上のマウスクリックを追跡するのに Ref作用を使用する例が含まれています。

このコー​​ドでは最初にnewRefアクションを使って値0で初期化された領域への新しい参照を作成しています。

  clickCount <- newRef 0

クリックイベントハンドラの内部では、modifyRefアクションを使用してクリック数を更新しています。

    modifyRef clickCount (\count -> count + 1)

readRefアクションは新しいクリック数を読み取るために使われています。

    count <- readRef clickCount

render関数では、クリック数に応じて変換を矩形に適用しています。

    withContext ctx $ do
      let scaleX = Math.sin (count * Math.pi / 4) + 1.5
      let scaleY = Math.sin (count * Math.pi / 6) + 1.5
      
      translate { translateX:  300, translateY:  300 } ctx
      rotate (count * Math.pi / 18) ctx
      scale { scaleX: scaleX, scaleY: scaleY } ctx
      translate { translateX: -100, translateY: -100 } ctx

      fillPath ctx $ rect ctx
        { x: 0
        , y: 0
        , w: 200
        , h: 200
        }

このアクションでは元の変換を維持するためにwithContextを使用しており、それから続く変換を順に適用しています(変換が下から上に適用されることを思い出してください)。

このコード例をビルドしてみましょう。

$ grunt refs

html/index.htmlファイルを開いてみましょう。何度かキャンバスをクリックすると、キャンバスの中心の周りを回転する緑の四角形が表示されるはずです。

演習

  1. (簡単) パスの線描と塗りつぶしを同時に行う高階関数を書いてください。その関数を使用して Random.purs例を書きなおしてください。

  2. (やや難しい) Random作用とDOM作用を使用して、マウスがクリックされたときにキャンバスに無作為な位置、色、半径の円を描画するアプリケーションを作成してください。

  3. (やや難しい) シーンを指定された座標を中心に回転する関数を書いてください。ヒント:最初にシーンを 原点まで平行移動しましょう。

9.9 L-Systems

この章の最後の例として、purescript-canvasパッケージを使用してL-systems(Lindenmayer systems)を描画する関数を記述します。

L-Systemsはアルファベット、つまり初期状態となるアルファベットの文字列と、生成規則の集合で定義されています。各生成規則は、アルファベットの文字をとり、それを置き換える文字の配列を返します。この処理は文字の初期配列から始まり、複数回繰り返されます。

もしアルファベットの各文字がキャンバス上で実行される命令と対応付けられていれば、その指示に順番に従うことでL-Systemsを描画することができます。

たとえば、アルファベットが文字 L (左回転)、 R(右回転)、F(前進)で構成されていたとします。また、次のような生成規則を定義します。

L -> L
R -> R
F -> FLFRRFLF

配列 "FRRFRRFRR" から始めて処理を繰り返すと、次のような経過を辿ります。

FRRFRRFRR
FLFRRFLFRRFLFRRFLFRRFLFRRFLFRR
FLFRRFLFLFLFRRFLFRRFLFRRFLFLFLFRRFLFRRFLFRRFLF...

この命令群に対応する線分パスをプロットすると、コッホ曲線と呼ばれる曲線に近似します。反復回数を増やすと、曲線の解像度が増加していきます。

それでは型と関数の言語へとこれを翻訳してみましょう。

アルファベットの選択肢は型の選択肢によって表すことができます。今回の例では、以下のような型で定義することができます。

data Alphabet = L | R | F

このデータ型では、アルファベットの各文字ごとに1つずつデータ構築子が定義が定義されています。

文字の初期配列はどのように表したらいいでしょうか。単なるアルファベットの配列でいいでしょう。これをSentenceと呼ぶことにします。

type Sentence = [Alphabet]

initial :: Sentence
initial = [F, R, R, F, R, R, F, R, R]

生成規則はAlphabetからSentenceへの関数として表すことができます。

productions :: Alphabet -> Sentence
productions L = [L]
productions R = [R]
productions F = [F, L, F, R, R, F, L, F]

これはまさに上記の仕様をそのまま書き写したものです。

これで、この形式の仕様を受け取りキャンバスに描画する関数lsystemを実装することができます。lsystemはどのような型を持っているべきでしょうか。この関数は初期状態initialと生成規則productionsのような値だけでなく、アルファベットの文字をキャンバスに描画する関数を引数に取る必要があります。

lsystemの型の最初の大まかな設計としては、次のようになるかもしれません。

forall eff. Sentence ->
            (Alphabet -> Sentence) ->
            (Alphabet -> Eff (canvas :: Canvas | eff) Unit) ->
            Number ->
            Eff (canvas :: Canvas | eff) Unit

最初の2つの引数の型は、値initialproductionsに対応しています。

3番目の引数は、アルファベットの文字を取り、キャンバス上のいくつかのアクションを実行することによって翻訳する関数を表します。この例では、文字 Lは左回転、文字 Rで右回転、文字Fは前進を意味します。

最後の引数は、実行したい生成規則の繰り返し回数を表す数です。

最初に気づくことは、現在のlsystem関数はAlphabet型だけで機能しますが、どんなアルファベットについても機能すべきですから、この型はもっと一般化されるべきです。それでは、量子化された型変数aについて、AlphabetSentenceaで置き換えましょう。

forall a eff. [a] ->
              (a -> [a]) ->
              (a -> Eff (canvas :: Canvas | eff) Unit) ->
              Number ->
              Eff (canvas :: Canvas | eff) Unit

次に気付くこととしては、「左回転」と「右回転」のような命令を実装するためには、いくつかの状態を管理する必要があります。具体的に言えば、その時点でパスが向いている方向を状態として持たなければなりません。計算を通じて状態を関数に渡すように変更する必要があります。ここでもlsystem関数は状態がどんな型でも動作しなければなりませんから、型変数sを使用してそれを表しています。

sを追加する必要があるのは3箇所で、次のようになります。

forall a s eff. [a] ->
                (a -> [a]) ->
                (s -> a -> Eff (canvas :: Canvas | eff) s) ->
                Number ->
                s -> 
                Eff (canvas :: Canvas | eff) s

まず追加の引数の型としてlsystemに型sが追加されています。この引数はL-Systemの初期状態を表しています。

sは引数にも現れますが、翻訳関数(lsystemの第3引数)の返り値の型としても現れます。翻訳関数は今のところ、引数としてL-Systemの現在の状態を受け取り、返り値として更新された新しい状態を返します。

この例の場合では、次のような型を使って状態を表す型を定義することができます。

type State =
  { x :: Number
  , y :: Number
  , theta :: Number
  }

プロパティxyはパスの現在の位置を表しており、プロパティthetaは現在の向きを表しており、ラジアンで表された水平線に対するパスの角度です。

システムの初期状態としては次のようなものが考えられます。

initialState :: State
initialState = { x: 120, y: 200, theta: 0 }

それでは、lsystem関数を実装してみます。定義はとても単純であることがわかるでしょう。

lsystemは第4引数の値(型Number)に応じて再帰するのが良さそうです。再帰の各ステップでは、生成規則に従って状態が更新され、現在の文が変化していきます。このことを念頭に置きつつ、まずは関数の引数の名前を導入して、補助関数に処理を移譲することから始めましょう。

lsystem :: forall a s eff. [a] ->
                           (a -> [a]) ->
                           (s -> a -> Eff (canvas :: Canvas | eff) s) ->
                           Number ->
                           s -> 
                           Eff (canvas :: Canvas | eff) s
lsystem init prod interpret n state = go init n
  where

go関数は第2引数に応じて再帰することで動きます。nがゼロであるときとnがゼロでないときの2つの場合で分岐します。

nがゼロの場合では再帰は完了し、解釈関数に応じて現在の文を解釈します。ここでは引数として与えられている型[a]の文、型sの状態、型s -> a -> Eff (canvas :: Canvas | eff) sの関数を参照することができます。これらの引数の型を考えると、以前定義したfoldMの呼び出しにちょうど対応していることがわかります。foldMpurescript-controlパッケージでも定義されています。

  go s 0 = foldM interpret state s

ゼロでない場合ではどうでしょうか。その場合は、単に生成規則を現在の文のそれぞれの文字に適用して、その結果を連結し、そしてこの処理を再帰します。

  go s n = go (concatMap prod s) (n - 1)

これだけです!foldMconcatMapのような高階関数を使うと、このようにアイデアを簡潔に表現することができるのです。

しかし、まだ完全に終わったわけではありません。ここで与えた型は、実際はまだ特殊化されすぎています。この定義ではキャンバスの操作が実装のどこにも使われていないことに注目してください。それに、まったくEffモナドの構造を利用していません。実際には、この関数はどんなモナドmについても動作するのです!

この章に添付されたソースコードで定義されている、lsystemのもっと一般的な型は次のようになっています。

lsystem :: forall a m s. (Monad m) =>
                         [a] ->
                         (a -> [a]) ->
                         (s -> a -> m s) ->
                         Number ->
                         s -> 
                         m s

この型が言っているのは、この翻訳関数はモナドmで追跡される任意の副作用をまったく自由に持つことができる、ということだと理解することができます。キャンバスに描画したり、またはコンソールに情報を出力するかもしれませんし、失敗や複数の戻り値に対応しているかもしれません。こういった様々な型の副作用を使ったL-Systemを記述してみることを読者にお勧めします。

この関数は実装からデータを分離することの威力を示す良い例となっています。この手法の利点は、複数の異なる方法でデータを解釈する自由が得られることです。lsystemは2つの小さな関数へと分解することができるかもしれません。ひとつめはconcatMapの適用の繰り返しを使って文を構築するもので、ふたつめはfoldMを使って文を翻訳するものです。これは読者の演習として残しておきます。

それでは翻訳関数を実装して、この章の例を完成させましょう​​。lsystemの型は型シグネチャが言っているのは、翻訳関数の型は、何らかの型as、型構築子mについて、 s -> a -> m s でなければならないということです。今回はaAlphabetsState、モナドmEff (canvas :: Canvas)というように選びたいということがわかっています。これにより次のような型になります。

interpret :: State -> Alphabet -> Eff (canvas :: Canvas) State

この関数を実装するには、Alphabet型の3つのデータ構築子それぞれについて処理する必要があります。文字L(左回転)とR(右回転)の解釈では、thetaを適切な角度へ変更するように状態を更新するだけです。

interpret state L = return $ state { theta = state.theta - Math.pi / 3 }
interpret state R = return $ state { theta = state.theta + Math.pi / 3 }

文字F(前進)を解釈するには、パスの新しい位置を計算し、線分を描画し、状態を次のように更新します。

interpret state F = do
  let x' = state.x + Math.cos state.theta * 1.5
      y' = state.y + Math.sin state.theta * 1.5
  moveTo ctx state.x state.y
  lineTo ctx x' y'
  return { x: x', y: y', theta: state.theta }

この章のソースコードでは、名前ctxを参照できるようにするために、interpret関数はmain関数内でlet束縛を使用して定義されていることに注意してください。State型がコンテキストを持つように変更することは可能でしょうが、それはこのシステムの状態の変化部分ではないので不適切でしょう。

このL-Systemsを描画するには、次のようなstrokePathアクションを使用するだけです。

strokePath ctx $ lsystem initial productions interpret 5 initialState

grunt lsystemを使ってL-Systemをコンパイルし、html/index.htmlを開いてみましょう。キャンバスにコッホ曲線が描画されるのがわかると思います。

演習

  1. (簡単) strokePathの代わりにfillPathを使用するように、上のL-Systemsの例を変更してください。ヒントclosePathの呼び出しを含め、moveTo の呼び出しをinterpret関数の外側に移動する必要があります。

  2. (簡単) 描画システムへの影響を理解するために、コード中の様々な数値の定数を変更してみてください。

  3. (やや難しい) lsystem関数を2つの小さな関数に分割してください。ひとつめはconcatMapの適用の繰り返しを使用して最終的な結果を構築するもので、ふたつめはfoldMを使用して結果を解釈するものでなくてはなりません。

  4. (やや難しい) setShadowOffsetXアクション、setShadowOffsetYアクション、setShadowBlurアクション、setShadowColorアクションを使い、塗りつぶされた図形にドロップシャドウを追加してください。ヒントpsciを使って、これらの関数の型を調べてみましょう。

  5. (やや難しい) 向きを変えるときの角度の大きさは今のところ一定(pi/3)です。その代わりに、Alphabetデータ型の中に角度の大きさを追加して、生成規則によって角度を変更できるようにしてください。

    type Angle = Number
    
    data Alphabet = L Angle | R Angle | F Angle
    生成規則でこの新しい情報を使うと、どんな面白い図形を作ることができるでしょうか。  
  6. (難しい) L(60度左回転 )、R(60度右回転)、F(前進)、M (これも前進)という4つの文字からなるアルファベットでL-Systemが与えられたとします。

    このシステムの文の初期状態は、単一の文字Mです。

    このシステムの生成規則は次のように指定されています。

    L -> L
    R -> R
    F -> FLMLFRMRFRMRFLMLF
    M -> MRFRMLFLMLFLMRFRM

    このL-Systemを描画してください。注意:最後の文のサイズは反復回数に従って指数関数的に増大するので、生成規則の繰り返しの回数を削減することが必要になります。

    ここで、生成規則における LMの間の対称性に注目してください。ふたつの「前進」命令は、次のようなアルファベット型を使用すると、 Boolean値を使って区別することができます。

    data Alphabet = L | R | F Boolean

    このアルファベットの表現を使用して、もう一度このL-Systemを実装してください。

  7. (難しい) 翻訳関数で別のモナドmを使ってみましょう。Trace作用を利用してコンソール上にL-Systemを出力したり、Random作用を利用して状態の型に無作為の突然変異を適用したりしてみてください。

9.10 まとめ

この章では、 purescript-canvasライブラリを使用することにより、PureScriptからHTML5 Canvas APIを使う方法について学びました。マップや畳み込み、レコードと行多型、副作用を扱うためのEffモナドなど、これまで学んできた手法を利用した実用的な例について多く見ました。

この章の例では、高階関数の威力を示すとともに、実装からデータを分離も実演しました。これは例えば、代数データ型を使用すると、これらの概念を次のように拡張し、描画関数からシーンの表現を完全に分離できるようになります。

data Scene = Rect Rectangle
           | Arc Arc
           | PiecewiseLinear [Point]
           | Transformed Transform Scene
           | Clipped Rectangle Scene
           | ...

この手法はpurescript-drawingパッケージでも採用されており、描画前にさまざまな方法でデータとしてシーンを操作することができるという柔軟性をもたらしています。

次の章では、PureScriptの外部関数インタフェース(foreign function interface)を使って、既存のJavaScriptの関数をラップしたpurescript-canvasのようなライブラリを実装する方法について説明します。