Haskellのdoブロックの中はbindで結ばれています。アクションとラムダを組み合わせることで仕組みを探ります。アクションをもっと便利に使うことを目的としているため、モナドや圏論には言及しません。
シリーズの記事です。
- Haskell 超入門
- Haskell 代数的データ型 超入門
- Haskell アクション 超入門
- Haskell ラムダ 超入門
- Haskell アクションとラムダ 超入門 ← この記事
- Haskell IOモナド 超入門
- Haskell リストモナド 超入門
- Haskell Maybeモナド 超入門
- Haskell 状態系モナド 超入門
- Haskell モナド変換子 超入門
- Haskell 例外処理 超入門
- Haskell 構文解析 超入門
- 【予定】Haskell 継続モナド 超入門
- 【予定】Haskell 型クラス 超入門
- 【予定】Haskell モナドとゆかいな仲間たち
- 【予定】Haskell Freeモナド 超入門
- 【予定】Haskell Operationalモナド 超入門
- 【予定】Haskell Effモナド 超入門
- 【予定】Haskell アロー 超入門
練習の解答例は別記事に掲載します。
bind
doブロックの各行はbindで結ばれています。
簡単な例で確認します。まずdoを使った例です、
main = do
print "hello"
print "world"
"hello"
"world"
doを外して明示的に>>=を記述します。
main =
print "hello" >>= \_ ->
print "world"
"hello"
"world"
同じように動くことが確認できました。
行を増やす
行を増やして確認します。
main = do
print "abc"
print "def"
print "ghi"
print "jkl"
print "mno"
main =
print "abc" >>= \_ ->
print "def" >>= \_ ->
print "ghi" >>= \_ ->
print "jkl" >>= \_ ->
print "mno"
"abc"
"def"
"ghi"
"jkl"
"mno"
doによって自動的に>>= \_ ->の部分が補われているため、複数行が記述できているわけです。
インデント
ラムダ式のインデントがどんどん深くなって読みにくいため、便宜上フラットに記述します。
main =
print "abc" >>= \_ ->
print "def" >>= \_ ->
print "ghi" >>= \_ ->
print "jkl" >>= \_ ->
print "mno"
暗黙の取り出し
ラムダ式の引数_は、doで行われる暗黙の取り出しを表しています。
main = do
print "abc"
print "def"
print "ghi"
main = do
_ <- print "abc"
_ <- print "def"
print "ghi"
main =
print "abc" >>= \_ ->
print "def" >>= \_ ->
print "ghi"
ラムダ式の引数は、アクションから取り出された値を束縛する変数に相当します。
<-
変数に束縛されるケースで確認します。
main = do
a <- return "hello"
print a
main =
return "hello" >>= \a ->
print a
"hello"
ポイントフリースタイル
printにそのまま渡している引数は、ポイントフリースタイルの要領で省略できます。
main =
return "hello" >>=
print
"hello"
練習
【問1】次のコードからdoを取り除いて、>>=でつないでください。
import System.Random
shuffle [] = return []
shuffle xs = do
n <- getStdRandom $ randomR (0, length xs - 1) :: IO Int
xs' <- shuffle $ take n xs ++ drop (n + 1) xs
return $ (xs !! n) : xs'
main = do
xs <- shuffle [1..9]
print xs
⇒ 解答例
ここまでのまとめ
-
doブロックの実体:各行がラムダ式化されて>>=で連結
doを使わなくても同じ処理を書くことができます。しかし表記が煩雑になるため、以後は特別な事情がない限りdoを付けたコードを掲載します。重要なのは仕組みを認識していることです。
※ その先には>>=の仕組みも待ち構えています。詳細は続編のHaskell IOモナド 超入門で説明します。
IORef
アクション超入門で紹介したIORefですが、ラムダ超入門の知識があれば便利になります。
まず比較として、今までの知識でIORefの値に1を足してみます。
import System.Random
import Data.IORef
main = do
a <- newIORef =<< (getStdRandom $ randomR (0, 9) :: IO Int)
a' <- readIORef a
writeIORef a (a' + 1)
print =<< readIORef a
5
modifyIORef
IORefには値の更新を行う関数modifyIORefが用意されています。
main = do
a <- newIORef =<< (getStdRandom $ randomR (0, 9) :: IO Int)
modifyIORef a (+ 1)
print =<< readIORef a
アクション超入門でmodifyIORefを紹介しなかったのは、ラムダ式やセクションの知識がないと有効活用できないためです。
練習
【問2】配列にはmodifyIORefに相当する関数がありません。modifyArrayを実装してください。
具体的には以下のコードが動くようにしてください。
main = do
a <- newArray (0, 2) 0 :: IO (IOUArray Int Int)
print =<< getElems a
modifyArray a 1 (+ 5)
print =<< getElems a
[0,0,0]
[0,5,0]
⇒ 解答例
色々な関数や演算子
アクションはモナドと呼ばれるものの一種です。ここではモナドが何かということは説明しませんが、とりあえずアクションのこととして先に進みます。
Control.Monadモジュールにはアクションと組み合わせて使える関数が用意されています。その中のいくつかを紹介します。
replicateM
アクションに対して使うreplicateです。指定された回数だけアクションから値を取り出してリスト化します。
※ 関数の接尾辞Mはモナド用であることを意味しています。アクションはモナドの一種のため、ここではアクション用と読み替えます。
乱数で試すとその都度値が取り出されていることが確認できます。
import Control.Monad
import System.Random
dice :: IO Int
dice = getStdRandom $ randomR (1, 6)
main = do
print $ replicate 5 1
print =<< replicateM 5 (return 1)
print =<< replicateM 5 dice
[1,1,1,1,1]
[1,1,1,1,1]
[4,6,2,1,2]
replicateM_
アクションから取り出した値を捨てるタイプのreplicateMです。戻り値は()が入ったアクションです。副作用を繰り返すのに使えます。
※ 関数の接尾辞_は値を捨てることを意味しています。引数などを無視するための変数名_と関係があります。
import Control.Monad
import System.Random
dice :: IO Int
dice = getStdRandom $ randomR (1, 6)
main = do
replicateM_ 3 $ do
print =<< dice
6
1
5
doブロックに対する指定回数ループだと捉えれば良いでしょう。
forM
C#などにあるforeachに似た処理を行う関数です。リストを渡すと値を1つずつ取り出して、指定した関数に引数として渡します。引数はラムダ式で受け取るのが手軽です。戻り値としてリストを返します。
import Control.Monad
main = do
a <- forM [1..3] $ \i -> do
print i
return i
print a
1
2
3
[1,2,3]
forM_
forMの戻り値を捨てる版です。単なるループとして使うにはこちらが手軽です。
import Control.Monad
main = do
forM_ [1..3] $ \i -> do
print i
1
2
3
when と unless
ifではelseを省略できませんが、それに相当する関数がwhenです。
when 条件 アクション
サイコロで3が出るまで繰り返す例です。
import Control.Monad
import System.Random
dice :: IO Int
dice = getStdRandom $ randomR (1, 6)
main = do
r <- dice
print r
when (r /= 3) main
1
5
3
elseだけに相当するのがunlessです。
main = do
r <- dice
print r
unless (r == 3) main
>>
アクションから値を取り出して捨て、次のアクションに移るための演算子です。
暗黙で取り出して捨てる動作を一行で書くようなイメージです。
test1 = do
print 1
print 2
test2 = print 1 >> print 2
main = test1 >> test2
1
2
1
2
>=> と <=<
アクションを返す関数を合成します。奇妙な見た目で、初めて見るとびっくりします。
同じ処理を別の記法で記述して比較します。
import Control.Monad
f1 x = putStr x >>= print
f2 = \x -> putStr x >>= print
f3 = putStr >=> print
f4 = print <=< putStr
f5 = (>>= print) . putStr
main = f1 "1" >> f2 "2" >> f3 "3" >> f4 "4" >> f5 "5"
1()
2()
3()
4()
5()
-
f1: 普通の関数です。 -
f2:f1をラムダ式化したものです。モナド則に出て来る書き方ですが、今回の範囲を超えるため説明を省略します。 -
f3:>=>で関数を合成しました。 -
f4: 逆向きの演算子<=<で合成しました。 -
f5:.で関数とセクションを合成しました。パズルみたいな書き方です。
f5の書き換えについては、興味があれば次の記事を参照してください。
- @7shi: Haskell - 関数合成を機械的に扱う試み - Qiita 2015.2.9
>=>については次の記事が参考になります。
- @CyLomw: Haskell - (>>=) を (>=>) に書き換える - Qiita 2014.12.30
練習
【問3】replicateM, replicateM_, forM, forM_, when, unlessを再実装してください。関数名には'を付けてください。
具体的には以下のコードが動くようにしてください。
main = do
let dice = getStdRandom $ randomR (1, 6) :: IO Int
print =<< replicateM' 5 dice
putStrLn "---"
replicateM_' 3 $ do
print =<< dice
putStrLn "---"
a <- forM' [1..3] $ \i -> do
print i
return i
print a
putStrLn "---"
forM_' [1..3] $ \i -> do
print i
putStrLn "---"
let y f = f (y f)
y $ \f -> do
r <- dice
print r
when' (r /= 1) f
putStrLn "---"
y $ \f -> do
r <- dice
print r
unless' (r == 6) f
⇒ 解答例
【問4】0.0~1.0までのDouble型の乱数を12個足したものを四捨五入して6を引いた整数値について、100回の分布を求めてください。四捨五入にはroundではなく、切り捨て関数truncateを工夫して使用してください。
具体的には以下のように出力してください。
-3: *
-2: ******
-1: ****************************
0: **********************************
1: ***********************
2: *******
3: *
⇒ 解答例