実例によるPureScript
ウェブのための
第8章 Effモナド
8.1 この章の目標
第7章では、オプショナルな型やエラーメッセージ、データの検証など、副作用を扱いを抽象化するApplicative関手を導入しました。この章では、より表現力の高い方法で副作用を扱うための別の抽象化、モナドを導入します。
この章の目的は、なぜモナドが便利な抽象化なのか、do記法とどう関係するのかについて説明することです。ブラウザでユーザインターフェイスを構築する副作用を扱うためのある種のモナドを使って、前の章の住所録の例を作ることにしましょう。これから扱うEffモナドは、PureScriptにおけるとても重要なモナドです。Effモナドはいわゆるネイティブな作用をカプセル化するのに使われます。
8.2 プロジェクトの準備
このプロジェクトのソースコードは前の章のソースコードの上に構築しますが、以前のプロジェクトのモジュールは、このプロジェクトの srcディレクトリに含まれています。
このプロジェクトでは、以下のBowerの依存関係が追加されています。
purescript-eff- Effモナドを提供します。purescript-react- Reactユーザインターフェイスへ接続するライブラリを提供します。
前章のモジュールに加えて、この章ではMainモジュールを使用します。このモジュールはエントリポイントであるとともに、UIの描写も行います。
このプロジェクトをコンパイルするには、まずReactをインストールするためnpm installを実行し、それからpulp browserify --to dist/Main.jsでビルドを行います。このプロジェクトを実行するには、html/index.htmlファイルをウェブブラウザで開いてください。
8.3 モナドとdo記法
do記法は配列内包表記を扱うときに最初に導入されました。配列内包表記は Data.Arrayモジュールの concatMap関数の構文糖として提供されています。
次の例を考えてみましょう。2つのサイコロを振って出た目を数え、出た目の合計が nのときそれを得点とすることを考えます。次のような非決定的なアルゴリズムを使うとこれを実現することができます。
- 最初の投擲で値
xを選択します。 - 2回目の投擲で値
yを選択します。 - もし
xとyの和がnなら組{x, y}を返し、そうでなければ失敗します。
配列内包表記を使うと、この非決定的アルゴリズムを自然に書くことができます。
import Prelude
import Control.Plus (empty)
import Data.Array ((..))
countThrows :: Int -> Array (Array Int)
countThrows n = do
x <- 1 .. 6
y <- 1 .. 6
if x + y == n
then pure [x, y]
else empty
PSCiで動作を見てみましょう。
> countThrows 10
[[4,6],[5,5],[6,4]]
> countThrows 12
[[6,6]]
前の章では、オプショナルな値に対応したより大きなプログラミング言語へとPureScriptの関数を埋め込む、 Maybe Applicative関手についての直感的理解を養いました。同様に配列モナドについても、非決定選択に対応したより大きなプログラミング言語へPureScriptの関数を埋め込む、というような直感的理解を得ることができます。
一般に、ある型構築子 mのモナドは、型 m aの値を持つdo記法を使う方法を提供します。上の配列内包表記では、すべての行に何らかの型 aについての型 Array aの計算が含まれていることに注目してください。一般に、do記法ブロックのすべての行は、何らかの型 aとモナド mについて、型 m aの計算を含んでいます。モナド mはすべての行で同じでなければなりません(つまり、副作用の種類は固定されます)が、型 aは異なることもあります(言い換えると、ここの計算は異なる型の結果を持つことができます)。
型構築子 Maybeが適用された、do記法の別の例を見てみましょう。XMLノードを表す型 XMLと演算子があるとします。
child :: XML -> String -> Maybe XML
この演算子はノードの子の要素を探し、もしそのような要素が存在しなければ Nothingを返します。
この場合、do記法を使うと深い入れ子になった要素を検索することができます。XML文書として符号化された利用者情報から、利用者の住んでいる市町村を読み取りたいとします。
userCity :: XML -> Maybe XML
userCity root = do
prof <- child root "profile"
addr <- child prof "address"
city <- child addr "city"
pure city
userCity関数は子の要素である profileを探し、 profile要素の中にある address要素、最後に address要素から city要素を探します。これらの要素のいずれかが欠落している場合は、返り値は Nothingになります。そうでなければ、返り値は cityノードから Justを使って構築されています。
最後の行にあるpure関数は、すべてのApplicative関手について定義されているのでした。MaybeのApplicative関手のpure関数はJustとして定義されており、最後の行を Just cityへ変更しても同じように正しく動きます。
8.4 モナド型クラス
Monad型クラスは次のように定義されています。
class Apply m <= Bind m where
bind :: forall a b. m a -> (a -> m b) -> m b
class (Applicative m, Bind m) <= Monad m
ここで鍵となる関数は Bind型クラスで定義されている演算子 bindで、 Functor及び Apply型クラスにある <$>や <*>などの演算子と同じ様に Preludeでは >>=として bindの別名が定義されています。
Monad型クラスは、すでに見てきた Applicative型クラスの操作で Bindを拡張します。
Bind型クラスの例をいくつか見てみるのがわかりやすいでしょう。配列についての Bindの妥当な定義は次のようになります。
instance bindArray :: Bind Array where
bind xs f = concatMap f xs
これは以前にほのめかした配列内包表記と concatMap関数の関係を説明しています。
Maybe型構築子についての Bindの実装は次のようになります。
instance bindMaybe :: Bind Maybe where
bind Nothing _ = Nothing
bind (Just a) f = f a
この定義はdo記法ブロックを通じて伝播された欠落した値についての直感的理解を補強するものです。
Bind型クラスとdo記法がどのように関係しているかを見て行きましょう。最初に何らかの計算結果から値を束縛するような、簡単などdo記法ブロックについて考えてみましょう。
do value <- someComputation
whatToDoNext
PureScriptコンパイラはこのようなパターンを見つけるたびにコードを次にように置き換えます。
bind someComputation \value -> whatToDoNext
下記のように表記することもできます。
someComputation >>= \value -> whatToDoNext
この計算 whatToDoNextは valueに依存することができます。
連続した複数の束縛がある場合でも、この規則が先頭のほうから複数回適用されます。例えば、先ほど見た userCityの例では次のように構文糖が脱糖されます。
userCity :: XML -> Maybe XML
userCity root =
child root "profile" >>= \prof ->
child prof "address" >>= \addr ->
child addr "city" >>= \city ->
pure city
do記法を使って表現されたコードは、 >>=演算子を使って書かれた同じ意味のコードよりしばしば読みやすくなることも特筆すべき点です。一方で、明示的に >>=を使って束縛が書くと、point-free形式でコードを書く機会を増やすことになります。ただし、通常は読みやすさを優先すべきでしょう。
8.5 モナド則
Monad型クラスはモナド則(monad laws)と呼ばれる3つの規則を持っています。これらは Monad型クラスの理にかなった実装から何を期待できるかを教えてくれます。
do記法を使用してこれらの規則を説明していくのが最も簡単でしょう。
Identity律
右単位元則(right-identity law)が3つの規則の中で最も簡単です。この規則はdo記法ブロックの最後の式であれば、 pureの呼び出しを排除することができると言っています。
do
x <- expr
pure x
右単位元則は、この式は単なる exprと同じだと言っています。
左単位元則(left-identity law)は、もしそれがdo記法ブロックの最初の式であれば、 pureの呼び出しを除去することができると述べています。
do
x <- pure y
next
このコードの名前 xを式 yで置き換えたものと nextは同じです。
最後の規則は結合則(associativity law)です。これは入れ子になったdo記法ブロックをどう扱うのかについて教えてくれます。
c1 = do
y <- do
x <- m1
m2
m3
上記のコード片は、次のコードと同じです。
c2 = do
x <- m1
y <- m2
m3
これら計算にはそれぞれ、3つのモナドの式 m1、 m2、 m3が含まれています。どちらの場合でも m1の結果は名前 xに束縛され、 m2の結果は名前 yに束縛されます。
c1では2つの式 m1と m2がそれぞれのdo記法ブロック内にグループ化されています。
c2では m1、 m2、 m3の3つすべての式が同じdo記法ブロックに現れています。
結合規則は 入れ子になったdo記法ブロックをこのように単純化しても安全であるということを言っています。
注意: do記法がどのように bindの呼び出しへと脱糖されるかの定義により、 c1と c2はいずれも次のコードと同じです。 `
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
-> List b
-> m a
モナド mが現れている点を除いて、 foldlの型と同じであることに注意しましょう。
foldl :: forall a b
. (a -> b -> a)
-> a
-> List b
-> a
直感的には、 foldMはさまざまな副作用の組み合わせに対応した文脈での配列の畳み込みを行うと捉えることができます。
例として mが Maybeであるとすると、この畳み込みはそれぞれの段階で Nothingを返すことで失敗することができます。それぞれの段階ではオプショナルな結果を返しますから、それゆえ畳み込みの結果もオプショナルになります。
もし mとして配列の型構築子 Arrayを選ぶとすると、畳み込みのそれぞれの段階で複数の結果を返すことができ、畳み込みは結果それぞれに対して次の手順を継続します。最後に、結果の集まりは、可能な経路すべての畳み込みから構成されることになります。これはグラフの走査と対応しています!
foldMを書くには、単に入力の配列について場合分けをするだけです。
配列が空なら、型 aの結果を生成するための選択肢はひとつしかありません。第2引数を返します。
foldM _ a Nil = pure a
aをモナド mまで持ち上げるために pureを使わなくてはいけないことも忘れないようにしてください。
配列が空でない場合はどうでしょうか?その場合、型 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 :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)
これで、 foldMで安全な除算の繰り返しを表現することができます。
> import Data.List
> foldM safeDivide 100 (fromFoldable [5, 2, 2])
(Just 5)
> foldM safeDivide 100 (fromFoldable [2, 0, 4])
Nothing
もしいずれかの時点で整数にならない除算が行われようとしたら、 foldM safeDivide関数は Nothingを返します。そうでなければ、 Just構築子に包まれた除算の繰り返した累積の結果を返します。
8.7 モナドとApplicative
クラス間に上位クラス関係があるため、 Monad型クラスのすべてのインスタンスは Applicative型クラスのインスタンスでもあります。
しかしながら、どんな Monadのインスタンスについても Applicative型クラスの実装が、それ以上の条件なしで存在し、次のような apが与えられます。
ap :: forall m a b. Monad m => m (a -> b) -> m a -> m b
ap mf ma = do
f <- mf
a <- ma
pure (f a)
もし mが Monad型クラスの規則に従っているなら、 mが apで与えられるような、妥当な Applicativeインスタンスが存在します。
興味のある読者は、これまで登場した Array、 Maybe、 Either eといったモナドについて、この apが applyと一致することを確かめてみてください。
もしすべてのモナドがApplicative関手でもあるなら、Applicative関手についての直感的理解をすべてのモナドについても適用することができるはずです。特に、更なる副作用の組み合わせで増強された「より大きな言語」でのプログラミングとモナドがいろいろな意味で一致することを当然に期待することができます。 mapと applyを使って、引数が任意個の関数をこの新しい言語へと持ち上げることができるはずです。
しかし、モナドはApplicative関手で可能な以上のことを行うことができ、重要な違いはdo記法の構文で強調されています。利用者情報を符号化したXML文書から利用者の都市を検索する、 userCityの例についてもう一度考えてみましょう。
userCity :: XML -> Maybe XML
userCity root = do
prof <- child root "profile"
addr <- child prof "address"
city <- child addr "city"
pure city
2番目の計算が最初の結果 profに依存し、3番目の計算が2番目の計算の結果 addrに依存するというようなことをdo記法は可能にします。 Applicative型クラスのインターフェイスだけを使うのでは、このような以前の値への依存は不可能です。
pureと applyだけを使って userCityを書こうとしてみれば、これが不可能であることがわかるでしょう。Applicativeは関数の互いに独立した引数を持ち上げることだけを可能にしますが、モナドはもっと興味深いデータ依存関係に関わる計算を書くことを可能にします。
前の章では Applicative型クラスは並列処理を表現できることを見ました。持ち上げられた関数の引数は互いに独立していますから、これはまさにその通りです。 Monad型クラスは計算が前の計算の結果に依存できるようにしますから、同じようにはなりません。モナドはその副作用を順番に組み合わせしなければいけません。
演習
-
(簡単)
purescript-arraysパッケージのData.Arrayモジュールからhead関数とtail関数の型を探してください。Maybeモナドとdo記法を使い、headとtailを組み合わせて、3要素以上の配列の3番目の要素を返すような関数を作ってください。その関数は適当なMaybe型を返さなければいけません。 -
(やや難しい) 与えられた幾つかの硬貨を組み合わせてできる可能性のあるすべての合計を決定する関数
sumを、foldMを使って書いてみましょう。入力の硬貨は、硬貨の価値の配列として与えられます。この関数は次のような結果にならなくてはいけません。> sums [] [0] > sums [1, 2, 10] [0,1,2,3,10,11,12,13]ヒント:
foldMを使うと1行でこの関数を書くことが可能です。重複する要素を取り除いたり、結果を昇順に並び替えたりするのに、nub関数やsort関数を使いたくなるかもしれません。 -
(やや難しい)
Maybe型構築子について、ap関数とapply演算子が一致することを確認してください。 -
(やや難しい)
purescript-maybeパッケージで定義されているMaybe型についてのMonadインスタンスが、モナド則を満たしていることを検証してください。 -
(やや難しい) 配列上の
filterの関数を一般化した関数filterMを書いてください。この関数は次の型シグネチャを持つ必要があります。filterM :: forall m a. Monad m => (a -> m Boolean) -> List a -> m (List a)PSCiでMaybeとArrayモナドを使ってその関数を試してみてください。 -
(難しい) すべてのモナドは、次で与えられるような既定の
Functorインスタンスがあります。map f a = do x <- a pure (f x)モナド則を使って、すべてのモナドが次を満たすことを証明してください。
lift2 f (pure a) (pure b) = pure (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特有の式が持つ副作用を区別するものです。ネイティブな作用には次のようなものがあります。
- コンソール入出力
- 乱数生成
- 例外
- 変更可能な状態の読み書き
また、ブラウザでは次のようなものがあります。
- DOM操作
- XMLHttpRequest / AJAX呼び出し
- WebSocketによる相互作用
- Local Storageの読み書き
すでに「ネイティブでない」副作用の例については数多く見てきています。
Maybeデータ型で表現される省略可能な値Eitherデータ型で表現されるエラー- 配列やリストで表現される多価関数
これらの区別はわかりにくいので注意してください。エラーメッセージは例外の形でJavaScriptの式の副作用となることがあります。その意味では例外はネイティブな副作用を表していて、 Effを使用して表現することができます。しかし、 Eitherを使用して実装されたエラーメッセージはJavaScriptランタイムの副作用ではなく、 Effを使うスタイルでエラーメッセージを実装するのは適切ではありません。そのため、ネイティブなのは作用自体というより、実行時にどのように実装されているかです。
8.9 副作用と純粋性
PureScriptのような言語が純粋であるとすると、疑問が浮かんできます。副作用がないなら、どうやって役に立つ実際のコードを書くことができるというのでしょうか。
その答えはPureScriptの目的は副作用を排除することではないということです。これは、純粋な計算と副作用のある計算とを型システムにおいて区別することができるような方法で、副作用を表現することを目的としているのです。この意味で、言語はあくまで純粋だということです。
副作用のある値は、純粋な値とは異なる型を持っています。このように、例えば副作用のある引数を関数に渡すことはできず、予期せず副作用持つようなことが起こらなくなります。
Effモナドで管理された副作用を実行する唯一の方法は、型 Eff eff aの計算をJavaScriptから実行することです。
ビルドツールPulp(や他のツール)は、オプションを与えることで、アプリケーションの起動時に main計算を呼び出すためのJavaScriptコードを簡単に追加で生成できるようにしています。 mainは Effモナドでの計算であることが要求されます。
このように、 mainによって使われる副作用が期待されることを、開発者は正確に知ることができます。加えて、 mainがどのような種類の副作用を持つかを制限するのに Effモナドを使うことができるので、例えば、アプリケーションはコンソールと相互作用するが、それ以外は何もしない、ということを確実に言うことができます。
8.10 Effモナド
Effモナドの目的は、副作用のある計算に型付けされたAPIを提供すると同時に、効率的なJavaScriptを生成することにあります。これは拡張可能作用(extensible effects)のモナドとも呼ばれており、これについては後述します。
例を示しましょう。次のコードでは乱数を生成するための関数が定義されている purescript-randomモジュールを使用しています。
module Main where
import Prelude
import Control.Monad.Eff.Random (random)
import Control.Monad.Eff.Console (logShow)
main = do
n <- random
logShow n
このファイルが Main.pursという名前で保存されているなら、次のコマンドでコンパイルすることができます。
$ pulp run
コンパイルされたJavaScriptを実行すると、コンソールに出力 0と 1の間で無作為に選ばれた数が表示されるでしょう。
このプログラムは、乱数生成とコンソール入出力というJavaScriptランタイムが提供する2種類のネイティブな作用を、do記法で組み合わせて使っています。
8.11 拡張可能作用
PSCiでモジュールを読み込み、 mainの型を調べてみましょう。
> import Main
> :type main
forall eff. Eff (console :: CONSOLE, random :: RANDOM | eff) Unit
この型はかなり複雑そうに見えますが、PureScriptのレコードの比喩で簡単に説明することができます。
レコード型を使った簡単な関数を考えてみましょう。
fullName person = person.firstName <> " " <> person.lastName
この関数は firstNameと lastNameというプロパティを含むレコードから完全な名前の文字列を作成します。もし PSCiでこの関数の型を同様に調べたとすると、次のように表示されるでしょう。
forall r. { firstName :: String, lastName :: String | r } -> String
この型は「少なくともfullNameは firstNameと lastNameという2つのフィールドを持つようなレコードをとり、 Stringを返す。」というように読みます。
渡したレコードが firstNameと lastNameいうプロパティさえ持っていれば、その他に余計なフィールドを持っていたとしても 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に (console :: CONSOLE | eff)を選べば、これらの2つの作用の集合は同じになります。
同様に logShowは mainの作用に合わせて特殊化できる型を持っています。
forall eff2. Show a => a -> Eff (console :: CONSOLE | eff2) Unit
この場合は、 eff2に (random :: Random | eff)を選ばなくてはなりません。
それが含む副作用を示す randomと logShowの型がポイントで、より大きな副作用の集まりを持ったより大きな計算を構築するために、他の副作用を混ぜ合わせることができるのです。
mainの型注釈を与えなくてもよいことに注意してください。コンパイラは randomと logShowの多相的な型が与えられた mainについて、最も一般的な型を見つけることができます。
8.13 Effの種
mainの型は今まで見てきた他の型とは異なります。それを説明するためには、まず Effの種について考える必要があります。値がその型によって分類されるように、型がその種によって分類されることを思い出してください。これまでは Type(型の種)と ->(型構築子のための種を構築する)だけから構築された種のみを見てきました。
Effの種を見るには、 PSCiで :kindコマンドを使います。
> import Control.Monad.Eff
> :kind Eff
# Control.Monad.Eff.Effect -> Type -> Type
今まで見たことのない記号が2つあります。
Control.Monad.Eff.Effectは副作用の型についての型レベルのラベルを表す作用の種です。これを理解するためには、上の mainで見た2つのラベルがいずれも種 Control.Monad.Eff.Effectを持っていることに注目してください。
> import Control.Monad.Eff.Console
> import Control.Monad.Eff.Random
> :kind CONSOLE
Control.Monad.Eff.Effect
> :kind RANDOM
Control.Monad.Eff.Effect
#種構築子は行の種を構築するのに使われます。行とは順序なしラベル付きの集合のことです。
そして、 Effは作用の行と作用の返り値の型という2つの引数を持っています。つまり、 Effの最初の引数は、作用の型の順序なしラベル付きの集合であり、2つめの引数は返り値の型だということです。
これで、先ほどの mainの型を読むことができるようになりました。
forall eff. Eff (console :: CONSOLE, random :: RANDOM | eff) Unit
Effの最初の引数は (console :: CONSOLE, random :: RANDOM | eff)です。これは CONSOLE作用と 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
種 Typeの型だけが値を持つので、関数の矢印の左辺にある型の種は Typeでなければなりません。
中括弧は実際には構文糖であり、PureScriptコンパイラによって理解されている完全な型は次のようなものです。
fullName :: forall r. Record (firstName :: String, lastName :: String | r) -> String
中括弧がなくなっており、 Record構築子が追加されていることに注意してください。 Recordは Primモジュールで定義されている組み込みの型構築子です。 Recordの種を調べてみると、次のようになっています。
> :kind Record
# Type -> Type
つまり、 Recordは型の行をとり型を構築する型構築子なのです。これがレコードについての行多相関数を書くことを可能にしているのです。
この型システムでは、拡張可能作用を扱うのに、行多相レコード(拡張可能レコード)を使うときと同じ機構が使われています。唯一の違いは、ラベルに現れる型の種です。レコードは型の行によってパラメータ化され、 Effは作用の行によってパラメータ化されるのです。
これと同じ型システムの機能は、型構築子の行や、行の行でパラメータ化される型を構築するのにさえ使われることがあります!
8.15 きめ細かな作用
作用の行は推論されるので、大抵の場合は Effを使うときに型注釈は必須ではありませんが、計算でどの作用が期待されるのかをコンパイラに示すために型注釈が使われることがあります。
先ほどの例を、作用の閉じた行で注釈すると次のようになります。
main :: Eff (console :: CONSOLE, random :: RANDOM) Unit
main = do
n <- random
logShow n
行変数 effがないことに注意してください。こうすると、異なった作用の型を使う計算を誤って含めることはできません。このように、コードが持つことを許される副作用を制御することができるのです。
8.16 ハンドラとアクション
logShowや randomのような関数はアクションと呼ばれます。アクションはそれらの関数の右辺に Eff型を持っており、その目的は新たな効果を導入することにあります。
これは Eff型が関数の引数の型として現れるハンドラとは対照的です。アクションが集合へ必要な作用を追加するのに対し、ハンドラは集合から作用を除去します。
例として、 purescript-exceptionsパッケージを考えてみます。このパッケージでは throwExceptionと catchExceptionという二つの関数が定義されています。
throwException :: forall a eff
. Error
-> Eff (exception :: EXCEPTION | eff) a
catchException :: forall a eff
. (Error -> Eff eff a)
-> Eff (exception :: EXCEPTION | eff) a
-> Eff eff a
throwExceptionはアクションです。 Effは右辺に現れていて、新しく Exception作用を導入します。
catchExceptionはハンドラです。 Effは関数の第2引数の型として出現しており、作用全体としては Exception作用を除去します。
特定の作用を必要とするコードの部分を限定するために型システムを使うことができるので、これは便利です。作用のあるコードをハンドラで包むことにより、その作用を許さないコードブロックの中に埋め込むことができます。
例えば、 Exception作用を使って例外を投げるコード片を書き、それからそのコードを catchExceptionで包むことによって、例外を許さないコード片の中にその計算を埋め込むことができるのです。
JSONドキュメントからアプリケーションの設定を読みたいとしましょう。文書を構文解析する過程で例外を投げることがあります。設定を読み構文解析するこの処理は、次のような型シグネチャを持つ関数として書くことができます。
readConfig :: forall eff. Eff (exception :: EXCEPTION | eff) Config
それから、 main関数で catchExceptionを使用して Exception作用を処理することができます。
main = do
config <- catchException printException readConfig
runApplication config
where
printException e = do
log (message e)
pure defaultConfig
purescript-effパッケージでも、副作用なしの計算を取り、それを純粋な値として安全に評価する runPureハンドラが定義されています。
type Pure a = Eff () 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 aは readSTRefアクションを使って状態を読み取ったり、 writeSTRefアクションや modifySTRefアクションで状態を変更するのに使われます。型 aは領域に格納された値の型で、型 hは型システムのメモリ領域を表しています。
例を示します。小さな時間刻みで簡単な更新関数の実行を何度も繰り返すことによって、重力に従って落下する粒子の落下の動きをシミュレートしたいとしましょう。
粒子の位置と速度を保持する変更可能な参照領域を作成し、領域に格納された値を更新するのにforループ(Control.Monad.Effの forEアクション)を使うことでこれを実現することができます。
import Prelude
import Control.Monad.Eff (Eff, forE)
import Control.Monad.ST (ST, newSTRef, readSTRef, modifySTRef)
simulate :: forall eff h. Number -> Number -> Int -> Eff (st :: ST h | eff) Number
simulate x0 v0 time = do
ref <- newSTRef { x: x0, v: v0 }
forE 0 (time * 1000) \_ -> do
modifySTRef ref \o ->
{ v: o.v - 9.81 * 0.001
, x: o.x + o.v * 0.001
}
pure unit
final <- readSTRef ref
pure 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はこの例の唯一の作用なので、 runPureと runSTを併用すると simulateを純粋な関数に変えることができます、
simulate' :: Number -> Number -> Number -> Number
simulate' x0 v0 time = runPure (runST (simulate x0 v0 time))
PSCiでこの関数を実行してみてください。
> import Main
> simulate' 100.0 0.0 0.0
100.00
> simulate' 100.0 0.0 1.0
95.10
> simulate' 100.0 0.0 2.0
80.39
> simulate' 100.0 0.0 3.0
55.87
> simulate' 100.0 0.0 4.0
21.54
もし simulateの定義を runSTの呼び出しのところへ埋め込むとすると、次のようになります。
simulate :: Number -> Number -> Int -> Number
simulate x0 v0 time = runPure $ runST do
ref <- newSTRef { x: x0, v: v0 }
forE 0 (time * 1000) \_ -> do
modifySTRef ref \o ->
{ v: o.v - 9.81 * 0.001
, x: o.x + o.v * 0.001
}
pure unit
final <- readSTRef ref
pure final.x
参照区画はそのスコープから逃れることができないことがコンパイラにわかりますし、安全に varに変換することができます。 runSTの呼び出しの本体に対して生成されたJavaScriptは次のようになります。
var ref = { x: x0, v: v0 };
Control_Monad_Eff.forE(0)(time * 1000 | 0)(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モナドで効率のよいループを生成する forE、 foreachE、 whileE、 untilEのようなアクションを一緒に使うときには、 ST作用は短いJavaScriptを生成できる良い方法となります。
演習
-
(やや難しい) もし分母で分子を割り切れないなら
throwExceptionを使って例外を投げるようにsafeDivide関数を書き直してください。 -
(難しい) PIを推定するには次のような簡単な方法があります。単位正方形内にある多数の
N個の点を無作為に選び、内接する円に含まれるものの個数nを数えます。このとき4n/Nが円周率piの概算となります。forE関数、Random作用、ST作用を使って、この方法で円周率piを推定する関数を書いてください。
8.18 DOM作用
この章の最後の節では、 Effモナドでの作用についてこれまで学んだことを、実際のDOM操作の問題に応用します。
DOMを直接扱ったり、オープンソースのDOMライブラリを扱う、自由に利用可能なPureScriptパッケージが幾つかあります。
purescript-dom- 低レベルなJavaScript DOM APIのバインディングpurescript-jquery- jQueryライブラリのバインディング
上記のライブラリを抽象化するPureScript向けのライブラリもあります。
purescript-thermite-purescript-react上で構築されるライブラリpurescript-halogen- 仮想DOMを抽象的する型安全なライブラリ
この章では purescript-reactを使用し、住所簿にインターフェイスを追加しますが、興味のあるユーザは異なるアプローチで進めることをおすすめします。
8.19 住所録のユーザーインタフェース
purescript-reactを使用するためにReactコンポーネントと同じ様にアプリケーションを定義します。Reactコンポーネントは、コード内のHTML要素を純粋なデータ構造体として記述し、効率的にDOMにレンダリングします。さらに、コンポーネントはボタンのクリックなどのイベントに応答できます。 purescript-reactライブラリは Effモナドを使ってこれらのイベントをどのように扱うかを記述します。
Reactライブラリの完全なチュートリアルはこの章の範囲をはるかに超えていますが、読者は必要に応じてマニュアルを参照することをお勧めします。目的に応じて、Reactは Effモナドの実用的な例を提供してくれます。
まずは利用者が住所録に新しい項目を追加できるフォームを構築することにしましょう。フォームには、さまざまなフィールド(姓、名前、都市、州など)を入力するテキストボックス、および検証エラーが表示される領域が含まれます。テキストボックスに利用者がテキストを入力すると、検証エラーが更新されます。
シンプルさを保つために、フォームは固定の形状とします。電話番号は種類(自宅、携帯電話、仕事、その他)ごとに別々のテキストボックスへ分けることにします。
次の行を除いて、HTMLファイルは基本的に空です。
<script type="text/javascript" src="../dist/Main.js"></script>
この行には、Pulpによって生成されたJavaScriptコードが含まれています。これをファイルの最後に配置して、コードからアクセスしようとする関連要素が上にあることを確認します。 Main.jsファイルを再構築するには、 pulp browserifyコマンドを使うことができます。最初に distディレクトリが存在し、ReactをNPM依存関係としてインストールしたことを確認してください。
$ npm install # Install React
$ mkdir dist/
$ pulp browserify --to dist/Main.js
Mainモジュールは住所録コンポーネントを作成して画面に表示する main関数を定義しています。 main関数は CONSOLE作用と DOM作用のみを使用しており、型シグニチャは次のことを示します。
main :: Eff (console :: CONSOLE, dom :: DOM) Unit
まず、 mainはコンソールにステータスメッセージを記録します。
main = void do
log "Rendering address book component"
その後、 mainはDOM APIを使用してドキュメント本体への参照( doc)を取得します。
doc <- window >>= document
これは混在した作用の一例になっていることに注目してください。 log関数は CONSOLE作用を使い、 windowと document関数は両方とも DOM作用を使います。 mainの型は両方の作用を利用することを示します。
mainは windowアクションを使ってウィンドウオブジェクトへの参照を取得し、その結果を document関数に >>=を使って渡します。 documentはウィンドウオブジェクトをとり、そのドキュメントへの参照を返します。
do記法の定義により、これを次のようにも書けることに注意してください。
w <- window
doc <- document w
どちらが読みやすいかどうかは個人の好みの問題です。前者は名前が付けられた関数の引数がなく、point-free形式の一例となっています。その一方で、後者ではウィンドウオブジェクトの名前として wが使われています。
Mainモジュールは addressBookと呼ばれる住所録コンポーネントを定義します。その定義を理解するために、まずいくつかの概念を理解する必要があります。
Reactコンポーネントを作成するには、最初にコンポーネントのテンプレートのように動作するReactクラスを作成する必要があります。 purescript-reactでは、 createClass関数を使ってクラスを作成することができます。 createClassはクラスの仕様を必要とします。この本質は、コンポーネントのライフサイクルについて処理するために使われる Effアクションの集合です。開発者が注目するべきなのは Renderアクションです。
Reactライブラリが提供するいくつかの関連する関数の型は次のとおりです。
createClass
:: forall props state eff
. ReactSpec props state eff
-> ReactClass props
type Render props state eff
= ReactThis props state
-> Eff ( props :: ReactProps
, refs :: ReactRefs Disallowed
, state :: ReactState ReadOnly
| eff
) ReactElement
spec
:: forall props state eff
. state
-> Render props state eff
-> ReactSpec props state eff
Render型同義語は、いくつかの型シグネチャを単純化するために提供され、コンポーネントのレンダリング機能を表します。Renderアクションは(ReactThis型の)コンポーネントへの参照をとり、EffモナドにReactElementを返します。ReactElementはレンダリング後の意図したDOMの状態を記述するデータ構造体です。- すべてのReactコンポーネントは、ある型の状態を定義します。ボタンのクリックなどのイベントに応じて状態を変更することができます。
purescript-reactでは、初期状態値がspec関数で提供されます。 Render型の作用の行は、いくつかの面白い作用を使用して、特定の関数からReactコンポーネントの状態へのアクセスを制限します。たとえば、レンダリングのあいだ、「refs」オブジェクトへのアクセスはDisallowedであり、コンポーネント状態へのアクセスはReadOnlyです。
Mainモジュールは、住所録コンポーネントの状態の型と初期状態を定義します。
newtype AppState = AppState
{ person :: Person
, errors :: Errors
}
initialState :: AppState
initialState = AppState
{ person: examplePerson
, errors: []
}
状態には、(フォームコンポーネントを使用して編集可能にする) Personレコードと、既存の検証コードを使用して入力されるエラーの配列が含まれています。
次に、コンポーネントの定義を見てみましょう。
addressBook :: forall props. ReactClass props
すでに述べたように、addressBookはcreateClassとspecを使用してReactクラスを作成します。ここから初期状態の値とRenderアクションを得ることができます。取得したRenderアクションでいったい何ができるのでしょうか? 例えば、purescript-reactは以下のような単純なアクションを提供しています。
readState
:: forall props state access eff
. ReactThis props state
-> Eff ( state :: ReactState ( read :: Read
| access
)
| eff
) state
writeState
:: forall props state access eff
. ReactThis props state
-> state
-> Eff ( state :: ReactState ( write :: Write
| access
)
| eff
) state
readStateと writeStateアクションは拡張可能作用を伴って、ReactState作用を使ってReactの状態にアクセスできるようにしますが、他の行の ReactState作用をパラメータ化することで、読み書き権限がさらに分離されることに注意してください!
これは、PureScriptの行ベースの作用に関する興味深い点を示しています。行内に現れる作用は単純な1要素である必要はなく、様々な構造を持つことができ、この柔軟性によってコンパイル時にいくつかの有用な制限が可能になります。 purescript-reactライブラリがこの制限をしなかった場合、 Renderアクションで状態を書き込もうとすると、実行時に例外を受け取ることになります。適切な制限を行うことで、このような間違いがコンパイル時に捕捉されるようになりました。
これで addressBookコンポーネントの定義を読むことができるようになりました。まずは現在のコンポーネントの状態を読むことから始めましょう。
addressBook = createClass $ spec initialState \ctx -> do
AppState { person: Person person@{ homeAddress: Address address }
, errors
} <- readState ctx
次の点に注意してください。
- 名前
ctxはReactThisを参照しており、必要に応じて状態を読み書きするために使用することができます。 - AppState内のレコードは、レコードパターンを使用して照合しています。これにはerrorsフィールドのレコード同名利用も含まれます。便利なように、状態の構造のそれぞれの部分に明示的な名前をつけています。
RenderはDOMの次の状態を表すReactElement構造体を返さなければならないのでした。Renderアクションはいくつかの補助関数から定義されています。その補助関数の1つは renderValidationErrorsです。これはErrors構造体をReactElementの配列に変換します。
renderValidationError :: String -> ReactElement
renderValidationError err = D.li' [ D.text err ]
renderValidationErrors :: Errors -> Array ReactElement
renderValidationErrors [] = []
renderValidationErrors xs =
[ D.div [ P.className "alert alert-danger" ]
[ D.ul' (map renderValidationError xs) ]
]
purescript-reactでは、 ReactElementは通常、単一のHTML要素を divのような関数を適用することで作成します。これらの関数は通常、属性の配列と子要素の配列を引数として取ります。しかし、ここでは ul'のようにプライム記号(', prime character)で終わる名前は属性配列を省略し、代わりにデフォルトの属性を使用します。
ここでは通常のデータ構造体を単純に操作しているので、 mapのような関数を使って様々な要素を構築することができます。
2番目の補助関数は formFieldです。これは、単一フォームフィールドのテキスト入力を含む ReactElementを作成します。
formField
:: String
-> String
-> String
-> (String -> Person)
-> ReactElement
formField name hint value update =
D.div [ P.className "form-group" ]
[ D.label [ P.className "col-sm-2 control-label" ]
[ D.text name ]
, D.div [ P.className "col-sm-3" ]
[ D.input [ P._type "text"
, P.className "form-control"
, P.placeholder hint
, P.value value
, P.onChange (updateAppState ctx update)
] []
]
]
繰り返しますが、単純な要素から様々な要素を構成し、それぞれの要素に属性を適用しています。ここで注目すべき属性の1つは、 input要素に適用される onChange属性です。これはイベントハンドラで、ユーザーがテキストボックス内のテキストを編集するときにコンポーネントの状態を更新するために使用されます。イベントハンドラは、3番目の補助関数 updateAppStateを使用して定義されています。
updateAppState
:: forall props eff
. ReactThis props AppState
-> (String -> Person)
-> Event
-> Eff ( console :: CONSOLE
, state :: ReactState ReadWrite
| eff
) Unit
updateAppStateは、 ReactThis値の形式でコンポーネントへの参照、 Personレコードを更新する関数、そして Eventレコードを取ります。まず、(valueOf補助関数を使用して) changeイベントからテキストボックスの新しい値を抽出し、それを使って新しい Person状態を作成します。
for_ (valueOf e) \s -> do
let newPerson = update s
次に、検証関数を実行し、それに応じて(writeStateを使用して)コンポーネントの状態を更新します。
log "Running validators"
case validatePerson' newPerson of
Left errors ->
writeState ctx (AppState { person: newPerson
, errors: errors
})
Right _ ->
writeState ctx (AppState { person: newPerson
, errors: []
})
これは、コンポーネント実装の基本をカバーしています。しかし、コンポーネントの仕組みを完全に理解するためには、この章に付随する情報をお読みください。
pulp browserify --to dist/Main.jsを実行して、それからWebブラウザで html/index.htmlを開き、ユーザインターフェイスを試してみてください。フォームフィールドにいろいろな値を入力すると、ページ上に出力された検証エラーを見ることができるでしょう。
このユーザインタフェースには明らかに改善すべき点がたくさんあります。演習ではアプリケーションがより使いやすくなるような方法を追究していきます。
演習
-
(簡単) このアプリケーションを変更し、職場の電話番号を入力できるテキストボックスを追加してください。
-
(やや難しい) 検証エラーを
ul要素を使ってリストで表示するかわりに、それぞれのエラーについてひとつづつalertスタイルでdivを作成するように、コードを変更してください。 -
(難しい、拡張) このユーザーインターフェイスの問題のひとつは、検証エラーがその発生源であるフォームフィールドの隣に表示されていないことです。コードを変更してこの問題を解決してください。
ヒント:検証器によって返されるエラーの型は、エラーの原因となっているフィールドを示すために拡張する必要があります。次のようなエラー型を使用したくなるかもしれません。
data Field = FirstNameField | LastNameField | StreetField | CityField | StateField | PhoneField PhoneType data ValidationError = ValidationError String Field type Errors = Array ValidationError適切なフォーム要素を選択するように、
FieldをquerySelectorアクションの呼び出しに変更する関数を書く必要があるでしょう。
まとめ
この章ではPureScriptでの副作用の扱いについての多くの考え方を導入しました。
Monad型クラスと、それに関連するdo記法の導入をしました。- モナド則を導入し、do記法使って書かれたコードを変換する方法を説明しました。
- 異なる副作用で動作するコードを書くために、モナドを抽象的に扱う方法を説明しました。
- モナドがApplicative関手の一例であること、両者がどのように副作用のある計算を可能にするのか、2つの手法の違いを説明しました。
- ネイティブな作用の概念を定義し、ネイティブな副作用を処理するために使用する
Effモナドを導入しました。 - どのように
Effモナドが拡張可能作用を提供するか、複数の種類のネイティブな作用を同じ計算に混在させる方法を説明しました。 - 作用やレコードが種システムでどのように扱われるか、拡張可能レコードと拡張可能作用の関連を見ました。
- 乱数生成、例外、コンソール入出力、変更可能な状態、およびDOM操作といった、さまざまな作用を扱うために
Effモナドを使いました。
Effモナドは現実のPureScriptコードにおける基本的なツールです。本書ではこのあとも、様々な場面で副作用を処理するために Effモナドを使っていきます。