Haskell

Haskell アクションとラムダ 超入門

More than 3 years have passed since last update.

Haskellのdoブロックの中はbindで結ばれています。アクションとラムダを組み合わせることで仕組みを探ります。アクションをもっと便利に使うことを目的としているため、モナドや圏論には言及しません。

シリーズの記事です。


  1. Haskell 超入門

  2. Haskell 代数的データ型 超入門

  3. Haskell アクション 超入門

  4. Haskell ラムダ 超入門

  5. Haskell アクションとラムダ 超入門 ← この記事

  6. Haskell IOモナド 超入門

  7. Haskell リストモナド 超入門

  8. Haskell Maybeモナド 超入門

  9. Haskell 状態系モナド 超入門

  10. Haskell モナド変換子 超入門

  11. Haskell 例外処理 超入門

  12. Haskell 構文解析 超入門

  13. 【予定】Haskell 継続モナド 超入門

  14. 【予定】Haskell 型クラス 超入門

  15. 【予定】Haskell モナドとゆかいな仲間たち

  16. 【予定】Haskell Freeモナド 超入門

  17. 【予定】Haskell Operationalモナド 超入門

  18. 【予定】Haskell Effモナド 超入門

  19. 【予定】Haskell アロー 超入門

練習の解答例は別記事に掲載します。


bind

doブロックの各行はbindで結ばれています。

簡単な例で確認します。まずdoを使った例です、

main = do

print "hello"
print "world"


実行結果

"hello"

"world"

doを外して明示的に>>=を記述します。

main =

print "hello" >>= \_ ->
print "world"


実行結果

"hello"

"world"

同じように動くことが確認できました。


行を増やす

行を増やして確認します。


doあり

main = do

print "abc"
print "def"
print "ghi"
print "jkl"
print "mno"


doなし

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"


doなし

main =

print "abc" >>= \_ ->
print "def" >>= \_ ->
print "ghi"

ラムダ式の引数は、アクションから取り出された値を束縛する変数に相当します。


<-

変数に束縛されるケースで確認します。


doあり

main = do

a <- return "hello"
print a


doなし

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の書き換えについては、興味があれば次の記事を参照してください。

>=>については次の記事が参考になります。


練習

【問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: *

解答例