【PureScript】do 構文を酷使しよう【Haskell】
概要
みなさん、do 使ってますか?
do 構文は通常モナドの bind
を書きやすくする糖衣構文ですが、使い方に依ってはモナドが全く関係ないところでも便利に使えます。
特に、PureScript は Haskell の BlockArguments
、RebindableSyntax
、QualifiedDo
拡張と似た物をデフォルトで導入しているので、言語拡張等を気にしなくともすぐに使えます。
この記事では、そんな do 構文の使い方をいくつかに分けて紹介します。
1. モナド
これは通常の do 構文の使い方です。bind
(>>=
) でつながれた一連の処理を縦に並べて書き、処理の流れを分かりやすくします。
例えば、二つのランダムな 0 ~ 1 までの Number
を生成して足し算し、その結果をログに出力する処理は以下のように書けます。
main = do
x <- random
y <- random
log $ "x + y: " <> show (x + y)
これは以下のように脱糖されます。
main = random >>= (\x ->
random >>= (\y ->
log $ "x + y: " <> show (x + y)
)
)
>>=
の右辺のラムダ式の引数部分を <-
の左に持っていくというトリッキーな書き方が do 構文の特徴です。これによって、圧倒的に処理の流れが分かりやすくなります。
2. 一行で構成される do
do 構文の中身が 1 行だけだと、その式がそのまま展開されます。
例えば次のように書くと
test = show do 1 + 2
次のように展開されます。
test = show (1 + 2)
展開した後、>>=
が含まれていないので、モナドに依存しない処理にも使えます。展開する前のコードに括弧が含まれていないことに注意してください。この程度の処理なら括弧があっても問題ないですが、括弧の中身が多かったり、引数が多くなると面倒です。こういう時に do
構文を使うとかなり便利です。
例えば、1 から 10 までの数を足し合わせながら、その経過をログに残すという処理を書いてみます。
main = foldM
do
\acc x -> do
log $ show x
pure $ acc + x
do 0
do 1 .. 10
これは以下のように展開されます。
main = foldM
(\acc x -> do
log $ show x
pure $ acc + x
)
0
(1 .. 10)
括弧が消えていますね。特に第一引数はかなり読みやすくなっています。
この使い方の詳細は Mizunashi Mana さんの次の記事に詳しくまとめられています。是非参照ください。
ST
モナドを使う時に括弧を外す
3. 2 に関連しています。今、ST
モナドの値を生成する関数 mkST :: forall r. Int -> ST r Int
を持っているとします。
mkST :: forall r. Int -> ST r Int
mkST x = pure x
これに 0 を突っ込んで run :: forall a. (forall r. ST r a) -> a
で実行するコードは次のように書けます。
testST :: Int
testST = run (mkST 0)
では、次のように書けるでしょうか。
testST :: Int
testST = run $ mkST 0
実はこう書くことは できません。 run $ mkST 0
の部分で次のような型エラーが起きてしまいます。
The type variable r has escaped its scope, appearing in the type
ST r10 Int -> Int
in the expression apply run
in value declaration testST
これは、run $ mkST 0
が run (mkST 0)
と同じではなく、$
演算子のもとになっている関数 apply
を使った apply run (mkST 0)
に展開されることに起因しています。
apply
は次のような型を持っています。
apply :: forall a b. (a -> b) -> a -> b
この第一引数 (a -> b)
に run :: forall a. (forall r. ST r a) -> a
を適用しようとすると、その推論の過程で r
を決定しようとし、型がスコープの外を出てしまうようです。(いまいち何故出るのか分かっていないですが、分かるためには推論の方法を正確に知らないといけなさそうです。)
この場合、do
はただの糖衣構文なので、次のように書けば大丈夫です。
testST :: Int
testST = run do mkST 0
便利ですね!
bind
を使う
4. 別途定義した Haskell では RebindableSyntax 等を導入して do
に使う bind
等を変更できます。PureScript でも同様の事ができます。
例えば、Int の足し算を do
で書けるようにしてみましょう。
value6 :: Int
value6 = do
1
2
3
where
discard :: Int -> (Unit -> Int) -> Int
discard a b = a + b unit
こうすると value6
は 6
になります。
discard
は bind
と同じような物ですが、<-
の左が指定されていなかった時に使われるものです。これは次のように脱糖されます。
value6 = discard 1 (\_ -> discard 2 (\_ -> discard 3 (\_ -> 3)))
discard a b = a + b unit
を代入すれば
value6 = 1 + (\_ -> 2 + (\_ -> 3 + (\_ -> 3) unit) unit) unit
= 1 + 2 + 3
= 6
となります。
さらに、PureScript では Haskell の QualifiedDo と同様の機能が利用できるので、次のようなモジュールを作って
module IntPlus where
discard :: Int -> (Unit -> Int) -> Int
discard a b = a + b unit
名前付きで読み込む事で、まるで do
が最初からそうだったかのように使えます。
import IntPlus as IntPlus
value6 = IntPlus.do
1
2
3
非常に面白いです!
この例は簡単のため余り意味のないような do
を構成しましたが、例えば Indexed Monad 用の do
構文が Control.Monad.Indexed.Qualified にて用意されています。
この Indexed Monad はなんと React Hooks Like な UI の記述方法と関係があり、react-basic-hooks や、halogen-hooks で使われています。
簡単にいうと、PureScript 型レベルの情報を使い、毎回同じ順番で Hooks が実行される事を保証できます。こちらについては mrsekut さんの次の記事で詳しく解説されていました。これも今回の内容とは離れますが、面白い話です。
さらに、qualified-do パッケージでは様々な型クラスについて、do
構文を提供しています。Semigroupoid
などは結構使いやすいのではないでしょうか。
import QualifiedDo.Semigroup as Semigroup
-- hello = "Hello World!"
hello :: String
hello = Semigroup.do
"Hello"
" "
"World"
"!"
まとめ
この記事では、do 記法の様々な用途について解説しました。使い過ぎには気を付けて do 構文を酷使していきましょう。
Discussion