[Haskell] do構文をdoを使わない形に書き換える手順

  • 59
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

do は >>= の構文糖衣です。do構文の中では、<- という記号が使えるので、あたかも代入しているような形でプログラムを書いてゆくことができます。

Haskellプログラミングに少し慣れてくると、do構文を使わなくても書けるようになり、そのほうが簡潔でわかりやすいと思えることもあります(主観)。

do構文で書かれたプログラムをdoを使わない形に書き換える手順をまとめてみました。

この記事で使う、書き換えるための道具一式です。==>の左は右に置き換えられるという意味です。


-- | ----------------------------------------------------
-- | (1) do { m1 } ==> m1
-- | (2) do { m1; m2 } ==> m1 >> do { m2 }
-- | (3) do { let s1; m1 s1 } ==> let s1 in do { m1 s1 }
-- | (3’) do { let s1; m1 s1 } ==> do { m1 s1 } where s1
-- | (4) do { x <- m1; m2 x } ==> m1 >>= (\x -> do { m2 x } )
-- | (5) do { x <- m1; let s = f x; m2 s } ==> do { s <- f <$> m1; m2 s }
-- | (6) do { x <- m1; y <- m2; let s = f x y; m3 s } ==> do { s <- f <$> m1 <*> m2; m3 s }
-- | (7) let s1; let s2 ==> let (s1; s2)
-- | (8) (\x -> f x) ==> f
-- | ----------------------------------------------------

これらを組み合わせて、段階的にdoを使わない形に変形してゆきます。

do { m1 }

m1はMonadで、例えばIO ()です。これ1個がdo構文に入っている場合は、そのままdo構文からはずすことができます。

main :: IO ()  -- 以後すべてmainはIO ()です。型宣言を省略します。
main = do putStrLn "hello"

-- | (1) do { m1 } ==> m1
main = putStrLn "hello"

do { m1; m2 }

m1もm2も、IO ()です。この場合はMonadの >> を使います。以下のように(2)と(1)を使って2段階でdoをなくせます。

main = do putStr "hello"
          putStrLn " world"

-- | (2) do { m1; m2 } ==> m1 >> do { m2 }
main = putStr "hello"
       >> do putStrLn " world"

-- | (1) do { m1 } ==> m1
main :: IO ()
main = putStr "hello"
          >> putStrLn " world"
ここで使ったのは
(>>) :: Monad m => m a -> m b -> m b

do { x <-m1; m2 x }

m1の中に入っているものにxと名前をつけて、それをm2が使う場合です。

main = do x <- getLine
          putStrLn x

-- | (4) do { x <- m1; m2 x } ==> m1 >>= (\x -> do { m2 x } )
main = getLine >>= (\x -> do { putStrLn x })

-- | (1) do { m1 } ==> m1
main = getLine >>= (\x -> putStrLn x)

-- | (8) (\x -> f x) ==> f
main = getLine >>= putStrLn

ここで使ったのは
(>>=) :: Monad m => m a -> (a -> m b) -> m b

do { let s1; m1 s1 }

s1はMonadではありません。それをm1が使う場合です。let~inを使う方法と、whereを使う方法があります。

main = do let s1 = "hello"
          putStrLn s1

-- | (3) do { let s1; m1 } ==> let s1 in do { m1 s1 }
main = let s1 = "hello" in putStrLn s1

-- | (3') do { let s1; m1 } ==> let s1 in do { m1 s1 }
main = do { putStrLn s1 }
               where s1 = "hello"

-- | (1) do { m1 } ==> m1
main = putStrLn s1
          where s1 = "hello"

do { let s1; let s2; m1 s1; m2 s2 }

letが2つあって、その結果を使っているm1とm2が2つ続いている場合

main = do let s1 = "hello"
          let s2 = s1 ++ " world"
          putStrLn s1
          putStrLn s2

-- | (7) let s1; let s2 ==> let s1; s2
main = do let s1 = "hello"; s2 = s1 ++ " world"
          putStrLn s1
          putStrLn s2

-- | (3) do { let s1; m1 } ==> let s1 in do { m1 }
main = let s1 = "hello"; s2 = s1 ++ " world"
       in do putStrLn s1
             putStrLn s2

-- | (2) do { m1; m2 } ==> m1 >> do { m2 }
-- | (1) do { m1 } ==> m1
main = let s1 = "hello"; s2 = s1 ++ " world"
       in putStrLn s1
          >> putStrLn s2

-- | または
-- | (3') do { let s1; m1 } ==> do { m1 } where s1
main = do putStrLn s1
          putStrLn s2
          where s1 = "hello"; s2 = s1 ++ " world"

-- | (2) do { m1; m2 } ==> m1 >> do { m2 }
-- | (1) do { m1 } ==> m1
main = putStrLn s1 
       >> putStrLn s2
          where s1 = "hello"
                s2 = s1 ++ " world"

do { x <- m1; let s = f x; m2 s }

fは普通の関数です。普通の関数をMonadの中にある値に適用する場合はファンクタの<$>が使えます。

main = do xs <- getLine
          let n = sum . map (read :: String->Int) $ words xs
          print n

-- | (5) do { x <- m1; let s = f x; m2 s } ==> do { s <- f <$> m1; m2 s }
main = do n <- (sum . map (read :: String->Int) . words) <$> getLine
          print n

-- | (4) do { x <- m1; m2 } ==> m1 >>= (\x -> do { m2 } )
main = (sum . map (read :: String->Int) . words) <$> getLine >>= print

ここで使ったのは
(<$>) :: Functor f => (a -> b) -> f a -> f b
(>>=) :: Monad m => m a -> (a -> m b) -> m b

do { x <- m1; y <- m2; let s = f x y; m3 s }

Monadの中にある値2つを引数とする普通の関数の場合は、ファンクタの<$>とアプリカティブの<*>を使うと変換できます。

main = do s1 <- getLine
          s2 <- getLine
          let s3 = (++) s1 s2
          putStrLn s3

-- | (6) do { x <- m1; y <- m2; let s = f x y; m3 s } ==> do { s <- f <$> m1 <*> m2; m3 s }
main = do s3 <- ((++) <$> getLine) <*> getLine
          putStrLn s3

-- | (4) do { x <- m1; m2 } ==> m1 >>= (\x -> do { m2 } )
-- | (1) do { m1 }          ==> m1
-- | (6) (\x -> f x)        ==> f
main = ((++) <$> getLine) <*> getLine >>= putStrLn
ここで使ったのは
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(>>=) :: Monad m => m a -> (a -> m b) -> m b

do { x <- m1; y <- m2; z <- m3; let s = f x y z; m4 s }

Monadの中にある値3つを引数とする普通の関数の場合。さきほどの<*>をもう1回繰り返します。

main = do s1 <- getLine
          s2 <- getLine
          s3 <- getLine
          let s4 = s1 ++ s2 ++ s3
          putStrLn s4

-- | (6) do { x <- m1; y <- m2; let s = f x y; m3 s } ==> do { s <- f <$> m1 <*> m2; m3 s }
main = do s4 <- (++) <$> (((++) <$> getLine) <*> getLine) <*> getLine
          putStrLn s4

-- | (4) do { x <- m1; m2 } ==> m1 >>= (\x -> do { m2 } )
-- | (1) do { m1 }          ==> m1
-- | (6) (\x -> f x)        ==> f
main = (++) <$> (((++) <$> getLine) <*> getLine) <*> getLine >>= putStrLn
ここで使ったのは
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
(>>=) :: Monad m => m a -> (a -> m b) -> m b

次はちょっと難しい

do { x <- m1; y <- m2; let s3 = f x y; m3 s3; let s4 = g x y; m4 s4}

入力側Monadの中にある値を繰り返し出力側Monadで使う場合は、ファンクタの<$>とアプリカティブの<*>を組み合わせるパターンに持ち込めません

main = do s1 <- getLine
          s2 <- getLine
          let s3 = s1 ++ s2
          putStrLn s3
          let s4 = s1 ++ "," ++ s2
          putStrLn s4

-- | (4) do { x <- m1; m2 x } ==> m1 >>= (\x -> do { m2 x } )
main =   getLine >>= (\s1 ->
         getLine >>= (\s2 ->
           let s3 = (s1 ++ s2) in putStrLn s3
           >> let s4 = (s1 ++ "," ++ s2) in putStrLn s4
         ))

ここで使ったのは
(>>) :: Monad m => m a -> m b -> m b
(>>=) :: Monad m => m a -> (a -> m b) -> m b

入力側Monadの中にある値を繰り返し使う場合は、doで書いたほうがわかりやすいようにも思えます。やりたいことに応じて適切なほうを選んで書けばいいのだと思います。

エキスパートの方は、このような変換を頭の中でやって、短く簡潔なプログラムをどんどん書けるようになっているのだと推測しています。私はまだまだ修養が必要です。