🪄

【PureScript】do 構文を酷使しよう【Haskell】

2022/12/21に公開約4,700字

概要

みなさん、do 使ってますか?
do 構文は通常モナドの bind を書きやすくする糖衣構文ですが、使い方に依ってはモナドが全く関係ないところでも便利に使えます。
特に、PureScript は Haskell の BlockArgumentsRebindableSyntaxQualifiedDo 拡張と似た物をデフォルトで導入しているので、言語拡張等を気にしなくともすぐに使えます。
この記事では、そんな 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 さんの次の記事に詳しくまとめられています。是非参照ください。

https://mizunashi-mana.github.io/blog/posts/2020/07/no-parenthesis-with-blockarguments/

3. ST モナドを使う時に括弧を外す

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 0run (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

便利ですね!

4. 別途定義した bind を使う

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

こうすると value66 になります。
discardbind と同じような物ですが、<- の左が指定されていなかった時に使われるものです。これは次のように脱糖されます。

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 さんの次の記事で詳しく解説されていました。これも今回の内容とは離れますが、面白い話です。

https://scrapbox.io/mrsekut-p/purescript-react-basic-hooksはIxMonadでHooksの順序を規定する

さらに、qualified-do パッケージでは様々な型クラスについて、do 構文を提供しています。Semigroupoid などは結構使いやすいのではないでしょうか。

import QualifiedDo.Semigroup as Semigroup

-- hello = "Hello World!"
hello :: String
hello = Semigroup.do
  "Hello"
  " "
  "World"
  "!"

まとめ

この記事では、do 記法の様々な用途について解説しました。使い過ぎには気を付けて do 構文を酷使していきましょう。

GitHubで編集を提案

Discussion

ログインするとコメントできます