作用モナド

この章の目標

前章では、副作用を扱うのに使う抽象化であるアプリカティブ関手を導入しました。 副作用とは省略可能な値、エラー文言、検証などです。 この章では、副作用を扱うためのより表現力の高い別の抽象化であるモナドを導入します。

この章の目的は、なぜモナドが便利な抽象化なのかということと、do記法との関係を説明することです。

プロジェクトの準備

このプロジェクトでは、以下の依存関係が追加されています。

  • effect: 章の後半の主題であるEffectモナドを定義しています。 この依存関係は全てのプロジェクトで始めから入っているものなので(これまでの全ての章でも依存関係にありました)、明示的にインストールしなければいけないことは稀です。
  • react-basic-hooks: 住所録アプリに使うwebフレームワークです。

モナドとdo記法

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

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

  • 最初の投擲で値 x選択 します。
  • 2回目の投擲で値 y選択 します。
  • もしxyの和が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でこの関数の動作を見てみましょう。

> import Test.Examples

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

> countThrows 12
[[6,6]]

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

一般に、ある型構築子mのモナドは、型m aの値を持つdo記法を使う手段を提供します。 上の配列内包表記に注意すると、何らかの型aについて全行に型Array aの計算が含まれています。 一般に、do記法ブロックの全行は、何らかの型aとモナドmについて、型m aの計算を含みます。 モナドmは全行で同じでなければなりません(つまり副作用は固定)が、型aは異なることもあります(つまり個々の計算は異なる型の結果にできる)。

以下はdo記法の別の例です。 今回は型構築子 Maybeに適用されています。 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関手について定義されているのでした。 MaybeApplicative関手のpure関数はJustとして定義されており、最後の行を Just cityへ変更しても同じように正しく動きます。

モナド型クラス

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 Bind Array where
  bind xs f = concatMap f xs

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

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

instance 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

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

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

userCity :: XML -> Maybe XML
userCity root =
  child root "profile" >>= \prof ->
    child prof "address" >>= \addr ->
      child addr "city" >>= \city ->
        pure city

do記法を使って表現されたコードは、>>=演算子を使う等価なコードより遥かに読みやすくなることがよくあることも特筆すべき点です。 しかしながら、明示的に>>=を使って束縛を書くと、ポイントフリー形式でコードが書けるようになることがよくあります。 ただし、読みやすさにはやはり注意が要ります。

モナド則

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

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

単位元律

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

do
  x <- expr
  pure x

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

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

do
  x <- pure y
  next

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

最後の規則は 結合則 (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記法をbindの呼び出しへと脱糖する定義により、 c1c2は何れも次のコードと同じです。

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

モナドで畳み込む

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

これから書いていく関数はfoldMという名前です。 以前見たfoldl関数をモナドの文脈へと一般化するものです。 型シグネチャは以下です。

foldM :: forall m a b. Monad m => (a -> b -> m a) -> a -> List b -> m a
foldl :: forall   a b.            (a -> b ->   a) -> a -> List b ->   a

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

直感的には、foldMは様々な副作用の組み合わせに対応した文脈で配列を畳み込むものと捉えられます。

例としてmとしてMaybeを選ぶとすると、各段階でNothingを返すことでこの畳み込みを失敗させられます。 各段階では省略可能な結果を返しますから、それ故畳み込みの結果も省略可能になります。

もしmとして型構築子Arrayを選ぶとすると、畳み込みの各段階で0以上の結果を返せるため、畳み込みは各結果に対して独立に次の手順を継続します。 最後に、結果の集まりは可能な経路の全ての畳み込みから構成されることになります。 これはグラフの走査と対応していますね。

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

リストが空なら、型 aの結果を生成するための選択肢は1つしかありません。第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

なお、この実装はリストに対するfoldlの実装とほとんど同じです。 ただしdo記法である点を除きます。

PSCiでこの関数を定義して試せます。 以下は一例です。 整数の「安全な除算」関数を定義するとします。 0による除算かを確認し、失敗を示すために Maybe型構築子を使うのです。

safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide a b = Just (a / b)

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

> import Test.Examples
> import Data.List (fromFoldable)

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

> foldM safeDivide 100 (fromFoldable [2, 0, 4])
Nothing

もし何れかの時点で0による除算が試みられたら、foldM safeDivide関数はNothingを返します。 そうでなければ、累算値を繰り返し除算した結果をJust構築子に包んで返します。

モナドとアプリカティブ

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

しかし、あらゆるMonadのインスタンスに「無料で」ついてくるApply型クラスの実装もあります。これは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)

もしmMonad型クラスの法則の縛りがあれば、apで与えられるmについて妥当な Applyインスタンスが存在します。

興味のある読者はこれまで登場したモナドについてこのapapplyとして充足することを確かめてみてください。 モナドはArrayMaybeEither eといったものです。

もし全てのモナドがアプリカティブ関手でもあるなら、アプリカティブ関手についての直感的理解を全てのモナドについても適用できるはずです。 特に、モナドが更なる副作用の組み合わせで増強された「より大きな言語」でのプログラミングといろいろな意味で一致することを予想するのはもっともです。 mapapplyを使って、引数が任意個の関数をこの新しい言語へと持ち上げることができるはずです。

しかし、モナドはアプリカティブ関手でできること以上ができ、重要な違いはdo記法の構文で強調されています。 userCityの例についてもう一度考えてみましょう。 利用者情報をエンコードしたXML文書から利用者の市町村を検索するものでした。

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

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

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

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

演習

  1. (簡単)3つ以上の要素がある配列の3つ目の要素を返す関数thirdを書いてください。 関数は適切なMaybe型で返します。 手掛かりarraysパッケージのData.Arrayモジュールからheadtail関数の型を見つけ出してください。 これらの関数を組み合わせるにはMaybeモナドと共にdo記法を使ってください。

  2. (普通)一掴みの硬貨を使ってできる可能な全ての合計を決定する関数 possibleSumsを、 foldMを使って書いてみましょう。 入力の硬貨は、硬貨の価値の配列として与えられます。この関数は次のような結果にならなくてはいけません。

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

    手掛かりfoldMを使うと1行でこの関数を書けます。 重複を取り除いたり、結果を並び替えたりするのに、nub関数やsort関数を使うことでしょう。

  3. (普通)ap関数とapply演算子がMaybeモナドを充足することを確かめてください。 補足:この演習にはテストがありません。

  4. (普通)Maybe型についてのMonadインスタンスが、モナド則を満たしていることを検証してください。 このインスタンスはmaybeパッケージで定義されています。 補足:この演習にはテストがありません。

  5. (普通)リスト上のfilterの関数を一般化した関数filterMを書いてください。 この関数は次の型シグネチャを持ちます。

    filterM :: forall m a. Monad m => (a -> m Boolean) -> List a -> m (List a)
    
  6. (難しい)全てのモナドには次で与えられるような既定のFunctorインスタンスがあります。

    map f a = do
      x <- a
      pure (f x)
    

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

    lift2 f (pure a) (pure b) = pure (f a b)
    

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

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

    補足:この演習にはテストがありません。

ネイティブな作用

ここではPureScriptで中心的な重要性のあるモナドの1つ、Effectモナドについて見ていきます。

Effectモナドは Effectモジュールで定義されています。かつてはいわゆる ネイティブ 副作用を管理していました。Haskellに馴染みがあれば、これはIOモナドと同等のものです。

ネイティブな副作用とは何でしょうか。 この副作用はPureScript特有の式とJavaScriptの式とを2分するものです。 PureScriptの式は概して副作用とは無縁なのです。 ネイティブな作用の例を以下に示します。

  • コンソール入出力
  • 乱数生成
  • 例外
  • 変更可能な状態の読み書き

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

  • DOM操作
  • XMLHttpRequest / AJAX呼び出し
  • WebSocketによる相互作用
  • Local Storageの読み書き

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

  • Maybeデータ型で表現される省略可能な値
  • Eitherデータ型で表現されるエラー
  • 配列やリストで表現される多値関数

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

副作用と純粋性

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

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

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

Effectモナドで管理された副作用を現す手段は、型Effect aの計算をJavaScriptから実行することです。

Spagoビルドツール(や他のツール)は早道を用意しており、アプリケーションの起動時にmain計算を呼び出すための追加のJavaScriptコードを生成します。 mainEffectモナドでの計算であることが要求されます。

作用モナド

Effectは副作用のある計算を充分に型付けするAPIを提供すると同時に、効率的なJavaScriptを生成します。

馴染みのあるlog関数から返る型を見てみましょう。 Effectはこの関数がネイティブな作用を生み出すことを示しており、この場合はコンソールIOです。

Unitはいかなる意味のあるデータも返らないことを示しています。 UnitはC、Javaなど他の言語でのvoidキーワードと似たものとして考えられます。

log :: String -> Effect Unit

余談 :より一般的な(そしてより込み入った型を持つ)Effect.Class.Consolelog関数をIDEから提案されるかもしれません。 これは基本的なEffectモナドを扱う際はEffect.Consoleからの関数と交換可能です。 より一般的なバージョンがあることの理由は「モナドな冒険」章の「モナド変換子」について読んだあとにより明らかになっていることでしょう。 好奇心のある(そしてせっかちな)読者のために言うと、これはEffectMonadEffectインスタンスがあるから機能するのです。

log :: forall m. MonadEffect m => String -> m Unit

それでは意味のあるデータを返すEffectを考えましょう。 Effect.Randomrandom関数は乱択されたNumberを生み出します。

random :: Effect Number

以下は完全なプログラムの例です(この章の演習フォルダのtest/Random.pursにあります)。

module Test.Random where

import Prelude
import Effect (Effect)
import Effect.Random (random)
import Effect.Console (logShow)

main :: Effect Unit
main = do
  n <- random
  logShow n

Effectはモナドなので、do記法を使って含まれるデータを開封し、それからこのデータを作用のあるlogShow関数に渡します。 気分転換に、以下はbind演算子を使って書かれた同等なコードです。

main :: Effect Unit
main = random >>= logShow

これを手元で走らせてみてください。

spago run --main Test.Random

コンソールに出力 0.01.0の間で無作為に選ばれた数が表示されるでしょう。

余談:spago runは既定でmain関数をMainモジュールの中から探索します。 --mainフラグで代替のモジュールを入口として指定することも可能で、上の例ではそうしています。 この代替のモジュールにもmain関数が含まれているようにはしてください。

なお、不浄な作用付きのコードに訴えることなく、「乱択された」(技術的には疑似乱択された)データも生成できます。 この技法は「テストを生成する」章で押さえます。

以前言及したようにEffectモナドはPureScriptで核心的な重要さがあります。 なぜ核心かというと、それはPureScriptの外部関数インターフェースとやり取りする上での常套手段だからです。 外部関数インターフェースはプログラムを実行したり副作用を発生させたりする仕組みを提供します。 外部関数インターフェースを使うことは避けるのが望ましいのですが、どのように動作しどう使うのか理解することもまた極めて大事なことですので、実際にPureScriptで何か動かす前にその章を読まれることをお勧めします。 要はEffectモナドは結構単純なのです。 幾つかの補助関数がありますが、副作用を内包すること以外には大したことはしません。

例外

2つのネイティブな副作用が絡むnode-fsパッケージの関数を調べましょう。 ここでの副作用は可変状態の読み取りと例外です。

readTextFile :: Encoding -> String -> Effect String

もし存在しないファイルを読もうとすると……

import Node.Encoding (Encoding(..))
import Node.FS.Sync (readTextFile)

main :: Effect Unit
main = do
  lines <- readTextFile UTF8 "iDoNotExist.md"
  log lines

以下の例外に遭遇します。

    throw err;
    ^
Error: ENOENT: no such file or directory, open 'iDoNotExist.md'
...
  errno: -2,
  syscall: 'open',
  code: 'ENOENT',
  path: 'iDoNotExist.md'

この例外をうまく管理するには、潜在的に問題があるコードをtryに包めばどのような出力でも制御できます。

main :: Effect Unit
main = do
  result <- try $ readTextFile UTF8 "iDoNotExist.md"
  case result of
    Right lines -> log $ "Contents: \n" <> lines
    Left  error -> log $ "Couldn't open file. Error was: " <> message error

tryEffectを走らせて起こりうる例外をLeft値として返します。 もし計算が成功すれば結果はRightに包まれます。

try :: forall a. Effect a -> Effect (Either Error a)

独自の例外も生成できます。 以下はData.List.headの代替実装で、Maybeの値のNothingを返す代わりにリストが空のとき例外を投げます。

exceptionHead :: List Int -> Effect Int
exceptionHead l = case l of
  x : _ -> pure x
  Nil -> throwException $ error "empty list"

ただしexceptionHead関数はどこかしら非実用的な例です。 というのも、PureScriptのコードで例外を生成するのは避け、代わりにEitherMaybeのようなネイティブでない作用でエラーや欠けた値を使うのが一番だからです。

可変状態

中核ライブラリには ST作用という、これまた別の作用も定義されています。

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

ST作用は Control.Monad.STモジュールで定義されています。 この挙動を確認するには、その動作の型を見る必要があります。

new :: forall a r. a -> ST r (STRef r a)

read :: forall a r. STRef r a -> ST r a

write :: forall a r. a -> STRef r a -> ST r a

modify :: forall r a. (a -> a) -> STRef r a -> ST r a

newは型STRef r aの可変参照領域を新規作成するのに使われます。 この領域はread動作を使って読み取ったり、write動作やmodify動作で状態を変更するのに使えます。 型aは領域に格納された値の型を、型rメモリ領域(またはヒープ)を、それぞれ型システムで表しています。

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

粒子の位置と速度を保持する変更可能な参照領域を作成し、領域に格納された値を更新するのにforループを使うことでこれを実現できます。

import Prelude

import Control.Monad.ST.Ref (modify, new, read)
import Control.Monad.ST (ST, for, run)

simulate :: forall r. Number -> Number -> Int -> ST r Number
simulate x0 v0 time = do
  ref <- new { x: x0, v: v0 }
  for 0 (time * 1000) \_ ->
    modify
      ( \o ->
          { v: o.v - 9.81 * 0.001
          , x: o.x + o.v * 0.001
          }
      )
      ref
  final <- read ref
  pure final.x

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

なお、この関数が変更可能な状態を使っていても、その参照領域refがプログラムの他の部分での使用が許されない限り、これは純粋な関数のままです。 このことが正にST作用が禁止するものであることを見ていきます。

ST作用付きで計算するには、run関数を使用する必要があります。

run :: forall a. (forall r. ST r a) -> a

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

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

simulate' :: Number -> Number -> Int -> Number
simulate' x0 v0 time = run (simulate x0 v0 time)

PSCiでもこの関数を実行してみることができます。

> import Main

> simulate' 100.0 0.0 0
100.00

> simulate' 100.0 0.0 1
95.10

> simulate' 100.0 0.0 2
80.39

> simulate' 100.0 0.0 3
55.87

> simulate' 100.0 0.0 4
21.54

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

simulate :: Number -> Number -> Int -> Number
simulate x0 v0 time =
  run do
    ref <- new { x: x0, v: v0 }
    for 0 (time * 1000) \_ ->
      modify
        ( \o ->
            { v: o.v - 9.81 * 0.001
            , x: o.x + o.v * 0.001
            }
        )
        ref
    final <- read ref
    pure final.x

そうして、参照領域はそのスコープから逃れられないことと、安全にrefvarに変換できることにコンパイラが気付きます。 runが埋め込まれたsimulateに対して生成されたJavaScriptは次のようになります。

var simulate = function (x0) {
  return function (v0) {
    return function (time) {
      return (function __do() {

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

        Control_Monad_ST_Internal["for"](0)(time * 1000 | 0)(function (v) {
          return Control_Monad_ST_Internal.modify(function (o) {
            return {
              v: o.v - 9.81 * 1.0e-3,
              x: o.x + o.v * 1.0e-3
            };
          })(ref);
        })();

        return ref.value.x;

      })();
    };
  };
};

なお、この結果として得られたJavaScriptは最適化の余地があります。 詳細はこちらの課題を参照してください。 上記の抜粋はそちらの課題が解決されたら更新されるでしょう。

比較としてこちらが埋め込まれていない形式で生成されたJavaScriptです。

var simulate = function (x0) {
  return function (v0) {
    return function (time) {
      return function __do() {

        var ref = Control_Monad_ST_Internal["new"]({ x: x0, v: v0 })();

        Control_Monad_ST_Internal["for"](0)(time * 1000 | 0)(function (v) {
          return Control_Monad_ST_Internal.modify(function (o) {
            return {
              v: o.v - 9.81 * 1.0e-3,
              x: o.x + o.v * 1.0e-3
            };
          })(ref);
        })();

        var $$final = Control_Monad_ST_Internal.read(ref)();
        return $$final.x;
      };
    };
  };
};

局所的な変更可能状態を扱うとき、ST作用は短いJavaScriptを生成する良い方法となります。 作用を持つ繰り返しを生成するforforeachwhileのような動作を一緒に使うときは特にそうです。

演習

  1. (普通)safeDivide関数を書き直し、もし分母がゼロならthrowExceptionを使って文言"div zero"の例外を投げるようにしたものをexceptionDivideとしてください。
  2. (普通)関数estimatePi :: Int -> Numberを書いてください。 この関数はnGregory Seriesを使ってpiの近似を計算するものです。 手掛かり:解答は上記のsimulateの定義に倣うことができます。 またData.InttoNumber :: Int -> Numberを使って、IntNumberに変換する必要があるかもしれません。
  3. (普通)n番目のフィボナッチ数を計算する関数fibonacci :: Int -> Intを書いてください。 STを使って前2つのフィボナッチ数の値を把握します。 PSCiを使い、STに基づく新しい実装の実行速度を第5章の再帰による実装と比較してください。

DOM作用

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

DOMを直接扱ったり、オープンソースのDOMライブラリを扱ったりするPureScriptパッケージが沢山あります。 例えば以下です。

  • web-domはW3CのDOM規格に向けた型定義と低水準インターフェース実装を提供します。
  • web-htmlはW3CのHTML5規格に向けた型定義と低水準インターフェース実装を提供します。
  • jqueryjQueryライブラリのバインディングの集まりです。

上記のライブラリを土台に抽象化を進めたPureScriptライブラリもあります。 以下のようなものです。

  • thermitereactを土台に構築されています。
  • react-basic-hooksreact-basicを土台に構築されています。
  • halogenは独自の仮想DOMライブラリを土台とする型安全な一揃いの抽象化を提供します。

この章では react-basic-hooksライブラリを使用し、住所簿アプリケーションにユーザーインターフェイスを追加しますが、興味のあるユーザは異なるアプローチで進めることをお勧めします。

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

react-basic-hooksライブラリを使い、アプリケーションをReactコンポーネントとして定義していきます。ReactコンポーネントはHTML要素を純粋なデータ構造としてコードで記述します。それからこのデータ構造は効率的にDOMへ描画されます。加えてコンポーネントはボタンクリックのようなイベントに応答できます。react-basic-hooksライブラリはEffectモナドを使ってこれらのイベントの制御方法を記述します。

Reactライブラリの完全な入門はこの章の範囲をはるかに超えていますが、読者は必要に応じて説明書を参照することをお勧めします。 目的に応じて、Reactは Effectモナドの実用的な例を提供してくれます。

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

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

exercises/chapter8ディレクトリから以下のコマンドでwebアプリを立ち上げることができます。

$ npm install
$ npx spago build
$ npx parcel src/index.html --open

もしspagoparcelのような開発ツールが大域的にインストールされていれば、npxの前置は省けるでしょう。 恐らく既にspagonpm i -g spagoで大域的にインストールしていますし、parcelについても同じことができるでしょう。

parcelは「住所録」アプリのブラウザ窓を立ち上げます。 parcelの端末を開いたままにし、他の端末でspagoで再構築すると、最新の編集を含むページが自動的に再読み込みされるでしょう。 また、purs ideに対応していたりpscidを走らせていたりするエディタを使っていれば、ファイルを保存したときに自動的にページが再構築される(そして自動的にページが再読み込みされる)ように設定できます。

この住所録アプリでフォームフィールドにいろいろな値を入力すると、ページ上で出力された検証エラーが見られます。

動作の仕組みを散策しましょう。

src/index.htmlファイルは最小限です。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Address Book</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous">
  </head>
  <body>
    <div id="container"></div>
    <script type="module" src="./index.js"></script>
  </body>
</html>

<scriptの行にJavaScriptの入口が含まれており、index.jsにはこの実質1行だけが含まれています。

import { main } from "../output/Main/index.js";

main();

module Main (src/main.purs) のmain関数と等価な、生成したJavaScriptを呼び出しています。 spago buildは生成された全てのJavaScriptをoutputディレクトリに置くことを思い出してください。

main関数はDOMとHTML APIを使い、index.htmlに定義したcontainer要素の中に住所録コンポーネントを描画します。

main :: Effect Unit
main = do
  log "Rendering address book component"
  -- Get window object
  w <- window
  -- Get window's HTML document
  doc <- document w
  -- Get "container" element in HTML
  ctr <- getElementById "container" $ toNonElementParentNode doc
  case ctr of
    Nothing -> throw "Container element not found."
    Just c -> do
      -- Create AddressBook react component
      addressBookApp <- mkAddressBookApp
      let
        -- Create JSX node from react component. Pass-in empty props
        app = element addressBookApp {}
      -- Render AddressBook JSX node in DOM "container" element
      D.render app c

これら3行に注目してください。

w <- window
doc <- document w
ctr <- getElementById "container" $ toNonElementParentNode doc

これは次のように統合できます。

doc <- document =<< window
ctr <- getElementById "container" $ toNonElementParentNode doc

あるいは更なる統合さえできます。

ctr <- getElementById "container" <<< toNonElementParentNode =<< document =<< window
-- or, equivalently:
ctr <- window >>= document >>= toNonElementParentNode >>> getElementById "container"

途中のwdoc変数が読みやすさの助けになるかは主観的な好みの問題です。

AddressBookのreactComponentを深堀りしましょう。 単純化されたコンポーネントから始め、それからMain.pursで実際のコードに構築していきます。

以下の最小限のコンポーネントをご覧ください。 遠慮なく全体のコンポーネントをこれに置き換えて実行の様子を見てみましょう。

mkAddressBookApp :: Effect (ReactComponent {})
mkAddressBookApp =
  reactComponent
    "AddressBookApp"
    (\props -> pure $ D.text "Hi! I'm an address book")

reactComponentにはこのような威圧的なシグネチャがあります。

reactComponent ::
  forall hooks props.
  Lacks "children" props =>
  Lacks "key" props =>
  Lacks "ref" props =>
  String ->
  ({ | props } -> Render Unit hooks JSX) ->
  Effect (ReactComponent { | props })

重要な注意点は全ての型クラス制約の後の引数にあります。 String(任意のコンポーネント名)、propsを描画されたJSXに変換する方法を記述する関数を取り、そしてEffectに包まれたReactComponentを返します。

propsからJSXへの関数は単にこうです。

\props -> pure $ D.text "Hi! I'm an address book"

propsは無視されており、D.textJSXを返し、そしてpureは描画されたJSXに持ち上げます。 これでcomponentにはReactComponentを生成するのに必要な全てがあります。

次に、完全な住所録コンポーネントにある幾つかの複雑な事柄を調べていきます。

これらは完全なコンポーネントの最初の数行です。

mkAddressBookApp :: Effect (ReactComponent {})
mkAddressBookApp = do
  reactComponent "AddressBookApp" \props -> R.do
    Tuple person setPerson <- useState examplePerson

personuseStateフックの状態の一部として追跡します。

Tuple person setPerson <- useState examplePerson

なお、複数回useStateを呼び出すことで、コンポーネントの状態を複数の状態の部品に分解することが自在にできます。 例えばPersonの各レコードフィールドについて分離した状態の部品を使って、このアプリを書き直すことができるでしょう。 しかしこの場合にそうすると僅かに利便性を損なうアーキテクチャになってしまいます。

他の例ではTuple用の/\中置演算子に出喰わすかもしれません。 これは先の行と等しいものです。

firstName /\ setFirstName <- useState p.firstName

useStateは、既定の初期値を取って現在の値と値を更新する方法を取ります。 useStateの型を確認すれば型personsetPersonについてより深い洞察が得られます。

useState ::
  forall state.
  state ->
  Hook (UseState state) (Tuple state ((state -> state) -> Effect Unit))

結果の値の梱包Hook (UseState state)は取り去ることができますが、それはuseStateR.doブロックの中で呼ばれているからです。 R.doは後で詳述します。

さてこれで以下のシグネチャを観察できます。

person :: state
setPerson :: (state -> state) -> Effect Unit

stateの限定された型は初期の既定値によって決定されます。 これはexamplePersonの型なのでこの場合はPerson Recordです。

personは各再描画の時点で現在の状態にアクセスする方法です。

setPersonは状態を更新する方法です。 単に現在の状態を新しい状態に変形する方法を記述する関数を提供します。 stateの型が偶然Recordのときは、レコード更新構文がこれにぴったり合います。 例えば以下。

setPerson (\currentPerson -> currentPerson {firstName = "NewName"})

あるいは短かく以下です。

setPerson _ {firstName = "NewName"}

Recordでない状態もまた、この更新パターンに従います。 ベストプラクティスについて、より詳しいことはこの手引きを参照してください。

useStateR.doブロックの中で使われていることを思い出しましょう。 R.dodoの特別なreactフックの派生です。 R.の前置はこれがReact.Basic.Hooksから来たものとして「限定する」もので、R.doブロックの中でフック互換版のbindを使うことを意味しています。 これは「限定されたdo」として知られています。 Hook (UseState state)の梱包を無視し、内部の値のTupleと変数に束縛してくれます。

他の状態管理戦略として挙げられるのはuseReducerですが、それはこの章の範疇外です。

以下ではJSXの描画が行われています。

pure
  $ D.div
      { className: "container"
      , children:
          renderValidationErrors errors
            <> [ D.div
                  { className: "row"
                  , children:
                      [ D.form_
                          $ [ D.h3_ [ D.text "Basic Information" ]
                            , formField "First Name" "First Name" person.firstName \s ->
                                setPerson _ { firstName = s }
                            , formField "Last Name" "Last Name" person.lastName \s ->
                                setPerson _ { lastName = s }
                            , D.h3_ [ D.text "Address" ]
                            , formField "Street" "Street" person.homeAddress.street \s ->
                                setPerson _ { homeAddress { street = s } }
                            , formField "City" "City" person.homeAddress.city \s ->
                                setPerson _ { homeAddress { city = s } }
                            , formField "State" "State" person.homeAddress.state \s ->
                                setPerson _ { homeAddress { state = s } }
                            , D.h3_ [ D.text "Contact Information" ]
                            ]
                          <> renderPhoneNumbers
                      ]
                  }
              ]
      }

ここでDOMの意図した状態を表現するJSXを生成しています。 このJSXは単一のHTML要素を作るHTMLタグ(例:divformh3liullabelinput)に対応する関数を適用することで作られるのが普通です。 これらのHTML要素はそれ自体がReactコンポーネントであり、JSXに変換されます。 通常これらの関数にはそれぞれ3つの種類があります。

  • div_: 子要素の配列を受け付けます。 既定の属性を使います。
  • div: 属性のRecordを受け付けます。 子要素の配列をこのレコードのchildrenフィールドに渡すことができます。
  • div': divと同じですが、JSXに変換する前にReactComponentを返します。

検証エラーをフォームの一番上に(もしあれば)表示するため、Errors構造体をJSXの配列に変えるrenderValidationErrors補助関数を作ります。この配列はフォームの残り部分の手前に付けます。

renderValidationErrors :: Errors -> Array R.JSX
renderValidationErrors [] = []
renderValidationErrors xs =
  let
    renderError :: String -> R.JSX
    renderError err = D.li_ [ D.text err ]
  in
    [ D.div
        { className: "alert alert-danger row"
        , children: [ D.ul_ (map renderError xs) ]
        }
    ]

なお、ここでは単に通常のデータ構造体を操作しているので、mapのような関数を使ってもっと面白い要素を構築できます。

children: [ D.ul_ (map renderError xs)]

classNameプロパティを使ってCSSスタイルのクラスを定義します。 このプロジェクトではBootstrapstylesheetを使っており、これはindex.htmlでインポートされています。 例えばフォーム中のアイテムはrowとして配置されてほしいですし、検証エラーはalert-dangerの装飾で強調されていてほしいです。

className: "alert alert-danger row"

2番目の補助関数は formFieldです。 これは、単一フォームフィールドのテキスト入力を作ります。

formField :: String -> String -> String -> (String -> Effect Unit) -> R.JSX
formField name placeholder value setValue =
  D.div
    { className: "form-group row"
    , children:
        [ D.label
            { className: "col-sm col-form-label"
            , htmlFor: name
            , children: [ D.text name ]
            }
        , D.div
            { className: "col-sm"
            , children:
                [ D.input
                    { className: "form-control"
                    , id: name
                    , placeholder
                    , value
                    , onChange:
                        let
                          handleValue :: Maybe String -> Effect Unit
                          handleValue (Just v) = setValue v
                          handleValue Nothing  = pure unit
                        in
                          handler targetValue handleValue
                    }
                ]
            }
        ]
    }

inputを置いてlabelの中にtextを表示すると、スクリーンリーダーのアクセシビリティの助けになります。

onChange属性があれば利用者の入力に応答する方法を記述できます。handler関数を使いますが、これは以下の型を持ちます。

handler :: forall a. EventFn SyntheticEvent a -> (a -> Effect Unit) -> EventHandler

handlerへの最初の引数にはtargetValueを使いますが、これはHTMLのinput要素中のテキストの値を提供します。 この場合は型変数aMaybe Stringで、handlerが期待するシグネチャに合致しています。

targetValue :: EventFn SyntheticEvent (Maybe String)

JavaScriptではinput要素のonChangeイベントにはString値が伴います。 しかし、JavaScriptの文字列はnullになり得るので、安全のためにMaybeが使われています。

したがって(a -> Effect Unit)handlerへの2つ目の引数は、このシグネチャを持ちます。

Maybe String -> Effect Unit

この関数はMaybe String値を求める作用に変換する方法を記述します。 この目的のために以下のように独自のhandleValue関数を定義してhandlerを渡します。

onChange:
  let
    handleValue :: Maybe String -> Effect Unit
    handleValue (Just v) = setValue v
    handleValue Nothing  = pure unit
  in
    handler targetValue handleValue

setValueformFieldの各呼び出しに与えた関数で、文字列を取りsetPersonフックに適切なレコード更新呼び出しを実施します。

なお、handleValueは以下のようにも置き換えられます。

onChange: handler targetValue $ traverse_ setValue

traverse_の定義を調査して、両方の形式が確かに等価であることをご確認ください。

これでコンポーネント実装の基本を押さえました。 しかし、コンポーネントの仕組みを完全に理解するためには、この章に付随するソースをお読みください。

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

演習

以下の演習ではsrc/Main.pursを変更してください。 これらの演習には単体試験はありません。

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

  2. (普通)現時点でアプリケーションは検証エラーを単一の「pink-alert」背景に集めて表示させています。 空行で分離することにより、各検証エラーにpink-alert背景を持たせるように変更してください。

    手掛かり:リスト中の検証エラーを表示するのにul要素を使う代わりに、コードを変更し、各エラーにalertalert-danger装飾を持つdivを作ってください。

  3. (難しい、発展)このユーザーインターフェイスの問題の1つは、検証エラーがその発生源であるフォームフィールドの隣に表示されていないことです。 コードを変更してこの問題を解決してください。

    手掛かり:検証器によって返されるエラーの型を、エラーの原因となっているフィールドを示すために拡張するべきです。 以下の変更されたエラー型を使うと良いでしょう。

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

    Error構造体から特定のFieldのための検証エラーを取り出す関数を書く必要があるでしょう。

まとめ

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

  • Monad型クラスとdo記法との関係性を見ました。
  • モナド則を導入し、do記法を使って書かれたコードを変換する方法を見ました。
  • 異なる副作用を扱うコードを書く上で、モナドを抽象的に使う方法を見ました。
  • モナドがアプリカティブ関手の一例であること、両者がどのように副作用のある計算を可能にするのかということ、そして2つの手法の違いを説明しました。
  • ネイティブな作用の概念を定義し、Effectモナドを見ました。 これはネイティブな副作用を扱うものでした。
  • 乱数生成、例外、コンソール入出力、変更可能な状態、及びReactを使ったDOM操作といった、様々な作用を扱うために Effectモナドを使いました。

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