実例によるPureScript

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

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

8 Effモナド

8.1 この章の目標

第7章では、オプショナルな型やエラーメッセージ、データの検証など、副作用を扱いを抽象化するApplicative関手を導入しました。この章では、より表現力の高い方法で副作用を扱うための別の抽象化、モナドを導入します。

この章の目的は、なぜモナドが便利な抽象化なのか、do記法とどう関係するのかについて説明することです。ブラウザでユーザインターフェイスを構築する副作用を扱うためのある種のモナドを使って、前の章の住所録の例を作ることにしましょう。これから扱うEffモナドは、PureScriptにおけるとても重要なモナドです。Effモナドはいわゆるネイティブな作用をカプセル化するのに使われます。

8.2 プロジェクトの準備

このプロジェクトのソースコードは前の章のソースコードの上に構築しますが、そのソースファイルを含めるようにGruntビルドスクリプトを使用しています。

コー​​ドは3つのモジュールに分かれています。

このプロジェクトを実行するには、Gruntでビルドし、html/index.htmlファイルをウェブブラウザで開いてください。

8.3 モナドとdo記法

do記法は配列内包表記を扱うときに最初に導入されました。配列内包表記は Data.ArrayモジュールのconcatMap関数の構文糖として提供されています。

次の例を考えてみましょう。2つのサイコロを振って出た目を数え、出た目の合計がnのときそれを得点とすることを考えます。次のような非決定的なアルゴリズムを使うとこれを実現することができます。

配列内包表記を使うと、この非決定的アルゴリズムを自然に書くことができます。

countThrows :: Number -> [[Number]]
countThrows n = do
  x <- range 1 6
  y <- range 1 6
  if x + y == n then return [x, y] else empty

psciで動作を見てみましょう。

> countThrows 10
[[4,6],[5,5],[6,4]]

> countThrows 12  
[[6,6]]

前の章では、オプショナルな値に対応したより大きなプログラミング言語へとPureScriptの関数を埋め込む、MaybeApplicative関手についての直感的理解を養いました。同様に配列モナドについても、非決定選択に対応したより大きなプログラミング言語へPureScriptの関数を埋め込む、というような直感的理解を得ることができます。

一般に、ある型構築子mのモナドは、型m aの値を持つdo記法を使う方法を提供します。上の配列内包表記では、すべての行に何らかの型aについての型[a]の計算が含まれていることに注目してください。一般に、do記法ブロックのすべての行は、何らかの型aとモナドmについて、型m aの計算を含んでいます。モナドmはすべての行で同じでなければなりません(つまり、副作用の種類は固定されます)が、型aは異なることもあります(言い換えると、ここの計算は異なる型の結果を持つことができます)。

型構築子Maybeが適用された、do記法の別の例を見てみましょう。XMLノードを表す型XMLと演算子があるとします。

(</>) :: XML -> String -> Maybe XML

この演算子はノードの子の要素を探し、もしそのような要素が存在しなければNothingを返します。

この場合、do記法を使うと深い入れ子になった要素を検索することができます。XML文書として符号化された利用者情報から、利用者の住んでいる市町村を読み取りたいとします。

userCity :: XML -> Maybe XML
userCity root = do
  prof <- root </> "profile"
  addr <- prof </> "address"
  city <- addr </> "city"
  return city

userCity関数は子の要素であるprofileを探し、profile要素の中にあるaddress要素、最後にaddress要素からcity要素を探します。これらの要素のいずれかが欠落している場合は、返り値はNothingになります。そうでなければ、返り値は cityノードからJustを使って構築されています。

最後の行の return関数は予約語ではないことを思い出してください。returnは実際にすべての Applicative関手について定義されているpure関数の別名です。JavaScriptのreturn文を連想するかもしれませんが、関数の途中での復帰とはまったく関係がありません。最後の行をJust cityへ変更しても同じように正しく動きます。

8.4 モナド型クラス

Monad型クラスは次のように定義されています。

class (Apply m) <= Bind m where
  (>>=) :: forall a b. m a -> (a -> m b) -> m b

class (Applicative m, Bind m) <= Monad m

ここで鍵となる関数はBind型クラスで定義されている演算子=>>で、これは「束縛」(bind)と呼ばれています。Monad型クラスは、すでに見てきたApplicative型クラスの操作でBindを拡張します。

Bind型クラスの例をいくつか見てみるのがわかりやすいでしょう。配列についてのBindの妥当な定義は次のようになります。

instance bindArray :: Bind [] where
  (>>=) xs f = f `concatMap` xs

これは以前にほのめかした配列内包表記とconcatMap関数の関係を説明しています。

Maybe型構築子についてのBindの実装は次のようになります。

instance bindMaybe :: Bind Maybe where
  (>>=) Nothing  _ = Nothing
  (>>=) (Just a) f = f a

この定義はdo記法ブロックを通じて伝播された欠落した値についての直感的理解を補強するものです。

Bind型クラスとdo記法がどのように関係しているかを見て行きましょう。最初に何らかの計算結果から値を束縛するような、簡単などdo記法ブロックについて考えてみましょう。

do value <- someComputation
   whatToDoNext

PureScriptコンパイラはこのようなパターンを見つけるたびにコードを次にように置き換えます。

someComputation >>= \value -> whatToDoNext

この計算whatToDoNextvalueに依存することができます。

連続した複数の束縛がある場合でも、この規則が先頭のほうから複数回適用されます。例えば、先ほど見たuserCityの例では次のように構文糖が脱糖されます。

userCity :: XML -> Maybe XML
userCity root = 
  root </> "profile" >>= \prof ->
    prof </> "address" >>= \addr ->
      addr </> "city" >>= \city ->
        return city

do記法を使って表現されたコードは、>>=演算子を使って書かれた同じ意味のコードよりしばしば読みやすくなることも特筆すべき点です。一方で、明示的に>>=を使って束縛が書くと、point-free形式でコードを書く機会を増やすことになります。ただし、通常は読みやすさを優先すべきでしょう。

8.5 モナド則

Monad型クラスはモナド則(monad laws)と呼ばれる3つの規則を持っています。これらはMonad型クラスの理にかなった実装から何を期待できるかを教えてくれます。

do記法を使用してこれらの規則を説明していくのが最も簡単でしょう。

8.5.1 Identity律

右単位元則(right-identity law)が3つの規則の中で最も簡単です。この規則はdo記法ブロックの最後の式であれば、returnの呼び出しを排除することが できると言っています。

do 
  x <- expr
  return x

右単位元則は、この式は単なるexprと同じだと言っています。

左単位元則(left-identity law)は、もしそれがdo記法ブロックの最初の式であれば、returnの呼び出しを除去することができると述べています。

do 
  x <- return y
  next

このコードの名前xを式yで置き換えたものとnextは同じです。

最後の規則は結合則(associativity law)です。これは入れ子になったdo記法ブロックをどう扱うのかについて教えてくれます。

c1 = do 
  y <- do 
    x <- m1
    m2
  m3

上記のコード片は、次のコードと同じです。

c2 = do 
  x <- m1
  y <- m2
  m3

これら計算にはそれぞれ、3つのモナドの式 m1m2m3が含まれています。どちらの場合でもm1 の結果は名前 xに束縛され、m2の結果は名前 yに束縛されます。

c1では2つの式m1m2がそれぞれのdo記法ブロック内にグループ化されています。

c2ではm1m2m3の3つすべての式が同じdo記法ブロックに現れています。

結合規則は 入れ子になったdo記法ブロックをこのように単純化しても安全であるということを言っています。

注意: do記法がどのように>>=の呼び出しへと脱糖されるかの定義により、c1c2はいずれも次のコードと同じです。`

c3 = do 
  x <- m1
  do
    y <- m2
    m3

8.6 モナドと畳み込み

抽象的にモナドを扱う例として、この節ではMonad型クラスの何らかの型構築子と一緒に機能するある関数を示していきます。これはモナドによるコードが副作用を伴う「より大きな言語」でのプログラミングと対応しているという直感的理解を補強しますし、モナドによるプログラミングがもたらす一般性も示しています。

これからfoldMと呼ばれる関数を書いてみます。これは以前扱ったfoldl関数をモナドの文脈へと一般化します。型シグネチャは次のようになっています。

foldM :: forall m a b. (Monad m) => (a -> b -> m a) -> a -> [b] -> m a 

モナドmが現れている点を除いて、foldlの型と同じであることに注意しましょう。

foldl :: forall a b. (a -> b -> a) -> a -> [b] -> a

直感的には、foldMはさまざまな副作用の組み合わせに対応した文脈での配列の畳み込みを行うと捉えることができます。

例としてmMaybeであるとすると、この畳み込みはそれぞれの段階でNothingを返すことで失敗することができます。それぞれの段階ではオプショナルな結果を返しますから、それゆえ畳み込みの結果もオプショナルになります。

もしmとして配列の型構築子[]を選ぶとすると、畳み込みのそれぞれの段階で複数の結果を返すことができ、畳み込みは結果それぞれに対して次の手順を継続します。最後に、結果の集まりは、可能な経路すべての畳み込みから構成されることになります。これはグラフの走査と対応しています!

foldMを書くには、単に入力の配列について場合分けをするだけです。

配列が空なら、型aの結果を生成するための選択肢はひとつしかありません。第2引数を返します。

foldM _ a [] = return a

aをモナドmまで持ち上げるためにreturnを使わなくてはいけないことも忘れないようにしてください。

配列が空でない場合はどうでしょうか?その場合、型aの値、型bの値、型a -> b -> m aの関数があります。もしこの関数を適用すると、型m aのモナドの結果を手に入れることになります。この計算の結果を逆向きの矢印<-で束縛することができます。

あとは配列の残りに対して再帰するだけです。実装は簡単です。

foldM f a (b : bs) = do
  a' <- f a b
  foldM f a' bs

do記法を除けば、この実装は配列に対するfoldlの実装とほとんど同じであることにも注意してください。

psciでこれを定義し、試してみましょう。除算可能かどうかを調べて、失敗を示すためにMaybe型構築子を使う、整数の「安全な除算」関数を定義するとしましょう。

safeDivide :: Number -> Number -> Maybe Number
safeDivide a b | a % b == 0 = Just (a / b)
safeDivide _ _ = Nothing

これで、foldMで安全な除算の繰り返しを表現することができます。

> foldM safeDivide 100 [5, 2, 2]
Just (5)

> foldM safeDivide 100 [2, 3, 4]
Nothing

もしいずれかの時点で整数にならない除算が行われようとしたら、foldM safeDivide関数はNothingを返します。そうでなければ、Just構築子に包まれた除算の繰り返した累積の結果を返します。

8.7 モナドとApplicative

クラス間に上位クラス関係があるため、Monad型クラスのすべてのインスタンスはApplicative型クラスのインスタンスでもあります。

しかしながら、どんなMonadのインスタンスについてもApplicative型クラスの実装が、それ以上の条件なしで存在し、次のようなapが与えられます。

ap :: forall m. (Monad m) => m (a -> b) -> m a -> m b
ap mf ma = do
  f <- mf
  a <- ma
  return (f a)

もしmMonad型クラスの規則に従っているなら、purereturnで与えられ、<*>apで与えられるような、妥当なApplicativeインスタンスが存在します。    

興味のある読者は、これまで登場した[]MaybeEither eV eといったモナドについて、このap<*>と一致することを確かめてみてください。

もしすべてのモナドがApplicative関手でもあるなら、Applicative関手についての直感的理解をすべてのモナドについても適用することができるはずです。特に、更なる副作用の組み合わせで増強された「より大きな言語」でのプログラミングとモナドがいろいろな意味で一致することを当然に期待することができます。<$><*>を使って、引数が任意個の関数をこの新しい言語へと持ち上げることができるはずです。

しかし、モナドはApplicative関手で可能な以上のことを行うことができ、重要な違いはdo記法の構文で強調されています。利用者情報を符号化したXML文書から利用者の都市を検索する、userCityの例についてもう一度考えてみましょう。

userCity :: XML -> Maybe XML
userCity root = do
  prof <- root </> "profile"
  addr <- prof </> "address"
  city <- addr </> "city"
  return city

2番目の計算が最初の結果profに依存し、3番目の計算が2番目の計算の結果addrに依存するというようなことをdo記法は可能にします。Applicative型クラスのインターフェイスだけを使うのでは、このような以前の値への依存は不可能です。

pure<*>だけを使ってuserCityを書こうとしてみれば、これが不可能であることがわかるでしょう。Applicativeは関数の互いに独立した引数を持ち上げることだけを可能にしますが、モナドはもっと興味深いデータ依存関係に関わる計算を書くことを可能にします。

前の章ではApplicative型クラスは並列処理を表現できることを見ました。持ち上げられた関数の引数は互いに独立していますから、これはまさにその通りです。Monad型クラスは計算が前の計算の結果に依存できるようにしますから、同じようにはなりません。モナドはその副作用を順番に組み合わせしなければいけません。

演習

  1. (簡単) purescript-arraysパッケージのData.Arrayモジュールからhead関数とtail関数の型を探してください。Maybeモナドとdo記法を使い、headtailを組み合わせて、3要素以上の配列の3番目の要素を返すような関数を作ってください。その関数は適当なMaybe型を返さなければいけません。
  2. (やや難しい) 与えられた幾つかの硬貨を組み合わせてできる可能性のあるすべての合計を決定する関数sumを、foldMを使って書いてみましょう。入力の硬貨は、硬貨の価値の配列として与えられます。この関数は次のような結果にならなくてはいけません。

    > sums []
    [0]
    
    > sums [1, 2, 10]
    [0,1,2,3,10,11,12,13]

    ヒントfoldMを使うと1行でこの関数を書くことが可能です。重複する要素を取り除いたり、結果を昇順に並び替えたりするのに、nub関数やsort関数を使いたくなるかもしれません。

  3. (やや難しい) Maybe型構築子について、ap関数と<*>演算子が一致することを確認してください。

  4. (やや難しい) purescript-maybeパッケージで定義されているMaybe型についてのMonadインスタンスが、モナド則を満たしていることを検証してください。

  5. (やや難しい) 配列上のfilterの関数を一般化した関数filterMを書いてください。この関数は次の型シグネチャを持つ必要があります。

    filterM :: forall m a. (Monad m) => (a -> m Boolean) -> [a] -> m [a]

    psciMaybe[]モナドを使ってその関数を試してみてください。

  6. (難しい) すべてのモナドは、次で与えられるような既定のFunctorインスタンスがあります。

    (<$>) f a = do
      x <- a
      return (f a)

    モナド則を使って、すべてのモナドが次を満たすことを証明してください。

    lift2 f (return a) (return b) = return (f a b)

    ここで、 Applicativeインスタンスは上で定義されたap関数を使用しています。lift2が次のように定義されていたことを思い出してください。

    lift2 :: forall f a b c. (Applicative f). (a -> b -> c) -> f a -> f b -> f c
    lift2 f a b = f <$> a <*> b

8.8 ネイティブな作用

ここではPureScriptの中核となる重要なモナド、Effモナドについて見ていきます。

EffモナドはControl.Monad.Effモジュール、およびPreludeで定義されています。これはいわゆるネイティブな作用を扱うために使います。

ネイティブな副作用とは何でしょうか。ネイティブな副作用とは、従来のJavaScriptの式が持つ副作用と、PureScript特有の式が持つ副作用を区別するものです。ネイティブな作用には次のようなものがあります。

また、ブラウザでは次のようなものがあります。

すでに「ネイティブでない」副作用の例については数多く見てきています。

これらの区別はわかりにくいので注意してください。エラーメッセージは例外の形でJavaScriptの式の副作用となることがあります。その意味では例外はネイティブな副作用を表していて、Effを使用して表現することができます。しかし、Eitherを使用して実装されたエラーメッセージはJavaScriptランタイムの副作用ではなく、Effを使うスタイルでエラーメッセージを実装するのは適切ではありません。そのため、ネイティブなのは作用自体というより、実行時にどのように実装されているかです。

8.9 副作用と純粋性

PureScriptのような言語が純粋であるとすると、疑問が浮かんできます。副作用がないなら、どうやって役に立つ実際のコードを書くことができるというのでしょうか。

その答えはPureScriptの目的は副作用を排除することではないということです。これは、純粋な計算と副作用のある計算とを型システムにおいて区別することができるような方法で、副作用を表現することを目的としているのです。この意味で、言語はあくまで純粋だということです。

副作用のある値は、純粋な値とは異なる型を持っています。このように、例えば副作用のある引数を関数に渡すことはできず、予期せず副作用持つようなことが起こらなくなります。

Effモナドで管理された副作用を実行する唯一の方法は、型Eff eff aの計算をJavaScriptから実行することです。

PureScriptコンパイラは、--mainコンパイラオプションを与えることで、アプリケーションの起動時に main計算を呼び出すためのJavaScriptコードを簡単に追加で生成できるようにしています。mainEffモナドでの計算であることが要求されます。

このように、mainによって使われる副作用が期待されることを、開発者は正確に知ることができます。加えて、mainがどのような種類の副作用を持つかを制限するのにEffモナドを使うことができるので、例えば、アプリケーションはコンソールと相互作用するが、それ以外は何もしない、ということを確実に言うことができます。

8.10 Effモナド

Effモナドの目的は、副作用のある計算に型付けされたAPIを提供すると同時に、効率的なJavascriptを生成することにあります。これは拡張可能作用(extensible effects)のモナドとも呼ばれており、これについては後述します。

例を示しましょう。次のコードでは乱数を生成するための関数が定義されているpurescript-randomモジュールを使用しています。

module Main where

import Control.Monad.Eff
import Control.Monad.Eff.Random

import Debug.Trace

main = do
  n <- random
  print n

このファイルがMain.purs という名前で保存されているなら、次のコマンドでコンパイルすることができます。

psc --main Main Main.purs

コンパイルされたJavaScriptを実行すると、コンソールに出力01の間で無作為に選ばれた数が表示されるでしょう。

このプログラムは、乱数生成とコンソール入出力というJavaScriptランタイムが提供する2種類のネイティブな作用を、do記法で組み合わせて使っています。

8.11 拡張可能作用

psciでモジュールを読み込み、mainの型を調べてみましょう。

> :t Main.main

forall eff. Eff (trace :: Trace, random :: Random | eff) Unit

この型はかなり複雑そうに見えますが、PureScriptのレコードの比喩で簡単に説明することができます。

レコード型を使った簡単な関数を考えてみましょう。

fullName person = person.firstName ++ " " ++ person.lastName

この関数は firstNamelastNameというプロパティを含むレコードから完全な名前の文字列を作成します。もしpsciでこの関数の型を同様に調べたとすると、次のように表示されるでしょう。

forall r. { firstName :: String, lastName :: String | r } -> String

この型は「少なくともfullNamefirstNamelastNameという2つのフィールドを持つようなレコードをとり、Stringを返す.」というように読みます。

渡したレコードが firstNamelastNameいうプロパティさえ持っていれば、その他に余計なフィールドを持っていたとしてもfullNameは気にしません。

> firstName { firstName: "Phil", lastName: "Freeman", location: "Los Angeles" }

Phil Freeman

同様に、上のmainの型は「main副作用のある計算で、乱数生成とコンソール入出力、およびそれ以外の任意の種類の副作用を備えた任意の環境で実行することができ、型Unitの値を返す」というように解釈できます。

これは 「拡張可能作用」という名前の由来になっています。必要な副作用さえ備えていれば、その副作用の集まりをいつでも拡張できるということです。

8.12 作用の混在

拡張可能作用はEffモナドで異なる型の副作用を混在(interleave)させることを可能にします。

先ほど使ったrandom関数は次のような型を持っています。

forall eff1. Eff (random :: Random | eff1) Number

この作用の集まり(random :: Random | eff1)mainで見たものと同じではありません

しかし、作用が一致するようにrandomの型を特殊化できます。eff1(trace :: Trace | eff)を選べば、これらの2つの作用の集合は同じになります。

同様に tracemainの作用に合わせて特殊化できる型を持っています。

forall eff2. String -> Eff (trace :: Trace | eff2) Unit

この場合は、eff2(random :: Random | eff)を選ばなくてはなりません。

それが含む副作用を示すrandomprintの型がポイントで、より大きな副作用の集まりを持ったより大きな計算を構築するために、他の副作用を混ぜ合わせることができるのです。

mainの型注釈を与えなくてもよいことに注意してください。pscrandomtraceの多相的な型が与えられたmainの最も一般的な型を見つけることができます。

8.13 Effの種

mainの型は今まで見てきた他の型とは異なります。それを説明するためには、まずEffについて考える必要があります。値がその型によって分類されるように、型がその種によって分類されることを思い出してください。これまでは*(型の種)と->(型構築子のための種を構築する)だけから構築された種のみを見てきました。

Effの種を見るには、psci:k コマンドを使います。

> :k Control.Monad.Eff.Eff

 # ! -> * -> *

今まで見たことのない記号が2つあります。

!は副作用の型についての型レベルのラベルを表す作用の種です。これを理解するためには、上のmainで見た2つのラベルがいずれも種!を持っていることに注目してください。

> :k Debug.Trace.Trace

  !

> :k Control.Monad.Eff.Random.Random

  !

#種構築子はの種を構築するのに使われます。行とは順序なしラベル付きの集合のことです。

そして、Effは作用の行と作用の返り値の型という2つの引数を持っています。つまり、 Effの最初の引数は、作用の型の順序なしラベル付きの集合であり、2つめの引数は返り値の型だということです。

これで、先ほどのmainの型を読むことができるようになりました。

forall eff. Eff (trace :: Trace, random :: Random | eff) Unit

Effの最初の引数は(trace :: Trace, random :: Random | eff)です。これはTrace作用とRandom作用を含む行です。パイプ記号|は、ラベルが付けられた作用と、それに混ぜあわせたいそれ以外の任意の作用を表す行変数(row variable)effを区切っています。

Effの2番目の引数は、計算の戻り値の型Unitです。

8.14 オブジェクトと行

拡張可能作用とレコードに深いつながりをもたらしているEffの種を考えてみましょう。

上で定義した関数fullNameを考えます。

fullName :: forall r. { firstName :: String, lastName :: String | r } -> String
fullName person = person.firstName ++ " " ++ person.lastName

*の型だけが値を持つので、関数の矢印の左辺にある型の種は*でなければなりません。

中括弧は実際には構文糖であり、PureScriptコンパイラによって理解されている完全な型は次のようなものです。

fullName :: forall r. Object (firstName :: String, lastName :: String | r) -> String

中括弧がなくなっており、Object構築子が追加されていることに注意してください。ObjectPrimモジュールで定義されている組み込みの型構築子です。Objectの種を調べてみると、次のようになっています。

> :k Object

  # * -> *

つまり、Object型の行をとり型を構築する型構築子なのです。これがレコードについての行多相関数を書くことを可能にしているのです。

この型システムでは、拡張可能作用を扱うのに、行多相レコード(拡張可能レコード)を使うときと同じ機構が使われています。唯一の違いは、ラベルに現れる型のです。レコードは型の行によってパラメータ化され、Effは作用の行によってパラメータ化されるのです。

これと同じ型システムの機能は、型構築子の行や、行の行でパラメータ化される型を構築するのにさえ使われることがあります!

8.15 きめ細かな作用

作用の行は推論されるので、大抵の場合はEffを使うときに型注釈は必須ではありませんが、計算でどの作用が期待されるのかをコンパイラに示すために型注釈が使われることがあります。

先ほどの例を、作用の閉じた行で注釈すると次のようになります。

main :: Eff (trace :: Trace, random :: Random) Unit
main = do
  n <- random
  print n

行変数effがないことに注意してください。こうすると、異なった作用の型を使う計算を誤って含めることはできません。このように、コードが持つことを許される副作用を制御することができるのです。

8.16 ハンドラとアクション

tracerandomのような関数はアクションと呼ばれます。アクションはそれらの関数の右辺にEff型を持っており、その目的は新たな効果を導入することにあります。

これはEff型が関数の引数の型として現れるハンドラとは対照的です。アクションが集合へ必要な作用を追加するのに対し、ハンドラは集合から作用を除去します。

例として、purescript-exceptionsパッケージを考えてみます。このパッケージではthrowExceptioncatchExceptionという二つの関数が定義されています。

throwException :: forall a eff. Error -> Eff (err :: Exception | eff) a

catchException :: forall a eff. (Error -> Eff eff a) -> 
                                Eff (err :: Exception | eff) a -> 
                                Eff eff a

throwExceptionはアクションです。 Effは右辺に現れていて、新しくException作用を導入します。

catchExceptionはハンドラです。 Effは関数の第2引数の型として出現しており、作用全体としてはException作用を除去します。

特定の作用を必要とするコードの部分を限定するために型システムを使うことができるので、これは便利です。作用のあるコードをハンドラで包むことにより、その作用を許さないコードブロックの中に埋め込むことができます。

例えば、Exception作用を使って例外を投げるコード片を書き、それからそのコードをcatchExceptionで包むことによって、例外を許さないコード片の中にその計算を埋め込むことができるのです。

JSONドキュメントからアプリケーションの設定を読みたいとしましょう。文書を構文解析する過程で例外を投げることがあります。設定を読み構文解析するこの処理は、次のような型シグネチャを持つ関数として書くことができます。

readConfig :: forall eff. Eff (err :: Exception | eff) Config

それから、main関数でcatchExceptionを使用してException作用を処理することができます。

main = catchException printException $ do
  config <- readConfig
  runApplication config
  
  where
  printException e = trace (stackTrace e)

Preludeでも、副作用なしの計算を取り、それを純粋な値として安全に評価するrunPureハンドラが定義されています。

type Pure a = forall e. Eff e a

runPure :: forall a. Pure a -> a

8.17 可変状態

PreludeにはST作用というまた別の作用も定義されています。

ST作用は変更可能な状態を操作するために使われます。純粋関数プログラミングを知っているなら、共有される変更可能な状態は問題を引き起こしやすいということも知っているでしょう。しかしながら、ST作用は型システムを使って安全で局所的な状態変化を可能にし、状態の共有を制限するのです。

ST作用はControl.Monad.STモジュールで定義されています。これがどのように動作するかを確認するには、そのアクションの型を見る必要があります。

newSTRef :: forall a h eff. a -> Eff (st :: ST h | eff) (STRef h a)

readSTRef :: forall a h eff. STRef h a -> Eff (st :: ST h | eff) a

writeSTRef :: forall a h eff. STRef h a -> a -> Eff (st :: ST h | eff) a

modifySTRef :: forall a h eff. STRef h a -> (a -> a) -> Eff (st :: ST h | eff) a

newSTRefは型STRef h aの変更可能な参照領域を新しく作るのに使われます。STRef h areadSTRefアクションを使って状態を読み取ったり、writeSTRefアクションやmodifySTRefアクションで状態を変更するのに使われます。型aは領域に格納された値の型で、型hは型システムのメモリ領域を表しています。

例を示します。小さな時間刻みで簡単な更新関数の実行を何度も繰り返すことによって、重力に従って落下する粒子の落下の動きをシミュレートしたいとしましょう。

粒子の位置と速度を保持する変更可能な参照領域を作成し、領域に格納された値を更新するのにforループ(Control.Monad.EffforEアクション)を使うことでこれを実現することができます。

import Control.Monad.Eff
import Control.Monad.ST

simulate :: forall eff h. Number -> Number -> Number -> Eff (st :: ST h | eff) Number
simulate x0 v0 time = do
  ref <- newSTRef { x: x0, v: v0 }
  forE 0 (time * 1000) $ \i -> do
    modifySTRef ref (\o ->
      { v: o.v - 9.81 * 0.001
      , x: o.x + o.v * 0.001
      })
    return unit
  final <- readSTRef ref
  return final.x

計算の最後では、参照領域の最終的な値を読み取り、粒子の位置を返しています。

この関数が変更可能な状態を使っていても、その参照区画refがプログラムの他の部分で使われるのが許されない限り、これは純粋な関数のままであることに注意してください。ST作用が禁止するものが正確には何であるのかについては後ほど見ます。

ST作用で計算を実行するには、runST関数を使用する必要があります。

runST :: forall a eff. (forall h. Eff (st :: ST h | eff) a) -> Eff eff a

ここで注目して欲しいのは、領域型hが関数矢印の左辺にある括弧の内側で量化されているということです。runSTに渡したどんなアクションでも、 任意の領域hがなんであれ動作するということを意味しています。

しかしながら、ひとたび参照領域がnewSTRefによって作成されると、その領域の型はすでに固定されており、runSTによって限定されたコードの外側で参照領域を使おうとしても型エラーになるでしょう。runSTが安全にST作用を除去できるのはこれが理由なのです!

実際に、STはこの例の唯一の作用なので、runPurerunSTを併用するとsimulateを純粋な関数に変えることができます、

simulate' :: Number -> Number -> Number -> Number
simulate' x0 v0 time = runPure (runST (simulate x0 v0 time))

psci でこの関数を実行してみてください。

> Main.simulate' 100 0 0.0
100.00

> Main.simulate' 100 0 1.0
95.10

> Main.simulate' 100 0 2.0
80.39

> Main.simulate' 100 0 3.0
55.87

> Main.simulate' 100 0 4.0
21.54

もしsimulateの定義をrunSTの呼び出しのところへ埋め込むとすると、次のようになります。

simulate :: Number -> Number -> Number -> Number
simulate x0 v0 time = runPure (runST (do
  ref <- newSTRef { x: x0, v: v0 }
  forE 0 (time * 1000) $ \i -> do
    modifySTRef ref (\o ->  
      { v: o.v - 9.81 * 0.001
      , x: o.x + o.v * 0.001  
      })
    return unit  
  final <- readSTRef ref
  return final.x))

参照区画はそのスコープから逃れることができないことがpscコンパイラにわかりますし、安全にvarに変換することができます。runSTの呼び出しの本体に対して生成されたJavaScriptは次のようになります。

var ref = { x: x0, v: v0 };

Control_Monad_Eff.forE(0)(time * 1000)(function (i) {
  return function __do() {
    ref = (function (o) {
      return {
        v: o.v - 9.81 * 1.0e-3, 
        x: o.x + o.v * 1.0e-3
      };
    })(ref);
    return Prelude.unit;
  };
})();

return ref.x;

局所的な変更可能状態を扱うとき、特にEffモナドで効率のよいループを生成するforEforeachEwhileEuntilEのようなアクションを一緒に使うときには、ST作用は短いJavaScriptを生成できる良い方法となります。

演習

  1. (やや難しい) もし分母で分子を割り切れないならthrowExceptionを使って例外を投げるようにsafeDivide関数を書き直してください。

  2. (難しい) PIを推定するには次のような簡単な方法があります。単位正方形内にある多数のN個の点を無作為に選び、内接する円に含まれるものの個数nを数えます。このとき4n/Nが円周率piの概算となります。forE関数、Random作用、ST作用を使って、この方法で円周率piを推定する関数を書いてください。

8.18 DOM作用

この章の最後の節では、Effモナドでの作用についてこれまで学んだことを、実際のDOM操作の問題に応用します。

DOMを直接扱ったり、オープンソースのDOMライブラリを扱う、自由に利用可能なPureScriptパッケージが幾つかあります。

しかしながら、これらのライブラリのほとんどはまだ非常に新しくAPIが安定なため、この章の内容を安定させられるように、この章のソースコードのControl.Monad.DOMモジュールにはDOM要素を操作するための最小限の関数群が含まれています。

DOM要素を作成や操作をするための次のようなアクションが含まれています。

body :: forall eff. Eff (dom :: DOM | eff) Node
createElement :: forall eff. String -> Eff (dom :: DOM | eff) Node
querySelector :: forall eff. String -> Eff (dom :: DOM | eff) (Maybe Node)

既存の要素の内容やスタイルを変更するためのアクションも用意されています。

setText :: forall eff. String -> Node -> Eff (dom :: DOM | eff) Node
setInnerHTML :: forall eff. String -> Node -> Eff (dom :: DOM | eff) Node
appendChild :: forall eff. Node -> Node -> Eff (dom :: DOM | eff) Node
addClass :: forall eff. String -> Node -> Eff (dom :: DOM | eff) Node

そして、DOMイベントを処理するためのアクションがあります。

addEventListener :: forall eff. String -> 
                                Eff (dom :: DOM | eff) Unit -> 
                                Node -> 
                                Eff (dom :: DOM | eff) Node

これらが住所録アプリケーションのユーザインターフェイスを作るのに必要なアクションです。

8.19 住所録のユーザーインタフェース

これから構築しようとしているユーザ·インタフェースは、HTMLとPureScriptファイルに分かれています。HTMLはページ上の要素の配置を定義し、PureScriptのコードはフォームの動的な振る舞いを制御する方法を定義します。

まずは利用者が住所録に新しい項目を追加できるフォームを構築することにしましょう。フォームには、さまざまなフィールド(姓、名前、都市、州など)を入力するテキストボックス、および検証エラーが表示される領域が含まれます。テキストボックスに利用者がテキストを入力すると、検証エラーが更新されます。

シンプルさを保つために、フォームは固定の形状とします。電話番号は種類(自宅、携帯電話、仕事、その他)ごとに別々のテキストボックスへ分けることにします。

head要素内の次のようなコードを除いて、HTMLファイルは完全に静的です。

<script type="text/javascript" src="../dist/Main.js"></script>
<script type="text/javascript">
  onload = PS.Main.main;
</script>  

最初の行ではpscによって生成されるJavaScriptコードを読み込み、2行目ではページがロードされたときにPS.Main.main関数が確実に実行されるようにしています。

Mainモジュールはとても単純です。Data.AddressBook.UIモジュールにはsetupEventHandlers関数に処理をそのまま移譲するmain関数だけが定義されています。

main :: forall eff. Eff (trace :: Trace, dom :: DOM | eff) Unit
main = do
  trace "Attaching event handlers"
  setupEventHandlers 

これは混在した作用の一例になっていることに注目してください。下で見るように、trace関数はTrace作用を使い、setupEventHandlers関数はTrace作用とDOM作用の両方を使っています(DOM作用はControl.Monad.Eff.DOMで定義されています)。

setupEventHandlers関数もとても簡単です(単一の目的を持った小​​さな関数それぞれに分割することによって、コードについて理解するのが簡単になっていることに注目してください)​​。

setupEventHandlers :: forall eff. Eff (trace :: Trace, dom :: DOM | eff) Unit
setupEventHandlers = do
  -- Listen for changes on form fields
  body >>= addEventListener "change" validateAndUpdateUI 

setupEventHandlersはまず文書のbodyへの参照を取得するためにbodyアクションを使い、>>=を使ってその結果をaddEventListenerアクションに渡しています。addEventListenerchangeイベントを監視して、イベントが発生するとその度にvalidateAndUpdateUIアクションを呼び出します。

do記法の定義により、これを次のようにも書けることに注意してください。

setupEventHandlers = do
  -- Listen for changes on form fields
  b <- body
  addEventListener "change" validateAndUpdateUI b

どちらが読みやすいかどうかは個人の好みの問題です。前者は名前が付けられた関数の引数がなく、point-free形式の一例となっています。その一方で、後者では文書のbodyの名前としてbが使われています。

validateAndUpdateUIアクションの役目は、フォーム検証器を実行し、必要に応じて利用者にエラーのリストを表示することです。この場合も、他の関数へ処理を委譲することによってこれを行います。最初に、querySelectorアクションを使用してページのvalidationErrors要素を選択しています。それから、その要素の内容を消去するためにsetInnerHTMLアクションを使用しています。

validateAndUpdateUI :: forall eff. Eff (trace :: Trace, dom :: DOM | eff) Unit
validateAndUpdateUI = do
  Just validationErrors <- querySelector "#validationErrors"        
  setInnerHTML "" validationErrors 

次にvalidateAndUpdateUIvalidateControlsアクションを呼び出し、フォームの検証を実行しています。

  errorsOrResult <- validateControls

後ほどすぐに見るように、errorsOrResultはエラーのリストかPersonレコードのどちらかを表す型Either [String] Personを持っています。

最後に、もし入​​力の検証に失敗すると、validateAndUpdateUIはページ上のエラーを表示するために displayValidationErrorsアクションに処理を委譲します。

  case errorsOrResult of
    Left errs -> displayValidationErrors errs
    Right result -> print result

  return unit

検証が成功した場合、コードは単にコンソールに検証結果を出力します。当然のことながら、実際のアプリケーションでは、次の手順でデータベースまたは同様のものにデータを保存することになるでしょう。

validateControls関数はより興味深いものです。validateControlsの役割は、フォームの検証を実行し、成功または失敗のいずれかを示す結果を返すことであることを思い出してください。最初に行うことは、コンソールにデバッグメッセージを出力することです。

validateControls :: forall eff. Eff (trace :: Trace, dom :: DOM | eff) 
                                    (Either [String] Person)  
validateControls = do
  trace "Running validators"

Data.AddressBook.UIモジュールでは、フォームフィールドから値を読み込む関数 valueOfが定義されています。ここでは型シグネチャだけを示し、実装については議論しません。

valueOf :: forall eff. String -> Eff (dom :: DOM | eff) String

valueOfはフォーム要素のIDをとり、利用者がそのテキストボックスに入力した値を返します。

次に、validateControlsはページ上のフォームフィールドからいろいろな文字列を読み取ってData.AddressBook.Personデータ構造体を構築します。

  p <- person 
    <$> valueOf "#inputFirstName"
    <*> valueOf "#inputLastName"
    <*> (address <$> valueOf "#inputStreet"
                 <*> valueOf "#inputCity"
                 <*> valueOf "#inputState")
    <*> sequence [ phoneNumber HomePhone <$> valueOf "#inputHomePhone"
                 , phoneNumber CellPhone <$> valueOf "#inputCellPhone"
                 ]

この計算では personaddressphoneNumber関数を持ち上げるために、EffをApplicative関手として使用していることに注意してください。また、Personデータ構造体の電話番号配列をまとめるために必要なEffの配列を連鎖させるために、Data.Traversablesequense関数を使っています。

最後に、validateControlsは前の章で書いた検証関数を実行し、その結果を返します。

  return $ validatePerson' p

残りのコー​​ドはdisplayValidationErrors関数です。displayValidationErrorsはエラーの配列をとり、ページ上にそれらの文字列を出力します。

この関数が最初に行うことは、エラーを表示するための新しいdiv要素を作成することです。フォームのレイアウトを制御するためにBootstrap libraryを使っているので、addClassアクションを使って新しい要素に適切なCSSクラスを設定しています。

displayValidationErrors :: forall eff. [String] -> Eff (dom :: DOM | eff) Unit
displayValidationErrors errs = do
  alert <- createElement "div"
    >>= addClass "alert" 
    >>= addClass "alert-danger"

このコードがpoint-free形式であることに改めて注意してください。興味のある読者は、これを>>=を使わないように書き換えてみることをおすすめします。

次のコードはul要素を作成し先ほどのdivに追加します。

  ul <- createElement "ul"
  ul `appendChild` alert

配列内の各エラーそれぞれについてli要素を作成して、リストに追加します。setTextアクションは、エラーメッセージをli要素のテキストコンテンツを設定するために使用されています。

  foreachE errs $ \err -> do
    li <- createElement "li" >>= setText err
    li `appendChild` ul
    return unit

配列の要素について繰り返しを行うために、このコードではforeachEアクションを使っています。これは以前に見たtraverse関数に似ていますが、Effモナドだけで使うように特殊化されています。

最後に、querySelectorアクションを使ってvalidationErrors要素を検索し、それに先ほどのdivを追加します。

  Just validationErrors <- querySelector "#validationErrors"
  alert `appendChild` validationErrors
  
  return unit

以上です!gruntを実行して、それからWebブラウザで html/index.htmlを開き、ユーザインターフェイスを試してみてください。

フォームフィールドにいろいろな値を入力すると、ページ上に出力された検証エラーを見ることができるでしょう。検証エラーをすべて修正すると、ブラウザのコンソール上に検証の結果が表示されるはずです。

このユーザインタフェースには明らかに改善すべき点がたくさんあります。演習ではアプリケーションがより使いやすくなるような方法を追究していきます。

演習

  1. (簡単) このアプリケーションを変更し、職場の電話番号のテキストボックスを追加してください。

  2. (やや難しい) 検証エラーをul要素を使ってリストで表示するかわりに、それぞれのエラーについてひとつづつalertスタイルでdivを作成するように、コードを変更してください。

  3. (やや難しい) >>=の明示的な呼び出しを使わないように、Data.AddressBook.UIモジュールのコードを書き直してください。
  4. (難しい、拡張) このユーザーインターフェイスの問題のひとつは、検証エラーがその発生源であるフォームフィールドの隣に表示されていないことです。コードを変更してこの問題を解決してください。

    ヒント:検証器によって返されるエラーの型は、エラーの原因となっているフィールドを示すために拡張する必要があります。次のようなエラー型を使用したくなるかもしれません。

    data ValidationError = ValidationError String Field
    
    data Field = FirstNameField
               | LastNameField 
               | StreetField
               | CityField
               | StateField
               | PhoneField PhoneType

    適切なフォーム要素を選択するように、FieldquerySelectorアクションの呼び出しに変更する関数を書くする必要があるでしょう。

8.20 まとめ

この章ではPureScriptでの副作用の扱いについての多くの考え方を導入しました。

Effモナドは現実のPureScriptコードにおける基本的なツールです。本書ではこのあとも、様々な場面で副作用を処理するためにEffモナドを使っていきます。