Part IV: IO と副作用の管理¶
本章では、関数型プログラミングにおける副作用の扱い方を学びます。Haskell では IO モナドを使って副作用を型システムで管理し、遅延リストで無限のデータを扱う方法を習得します。
第8章: IO モナドの導入¶
8.1 副作用の問題¶
純粋関数は副作用を持ちません。しかし、実際のプログラムには副作用が必要です:
- ファイルの読み書き
- ネットワーク通信
- データベースアクセス
- 乱数生成
- 現在時刻の取得
8.2 IO モナドとは¶
Haskell の IO モナドは「副作用を持つ計算」を表す型です。
IO aは「実行するとa型の値を返す副作用のある計算」- IO 値は
do記法で合成できる - Haskell のプログラムは
main :: IO ()から始まる
8.3 サイコロを振る例¶
ソースファイル: app/haskell/src/Ch08/IOMonad.hs
乱数生成(IO を使用)¶
import System.Random (randomRIO)
-- | サイコロを振る(副作用あり)
castTheDie :: IO Int
castTheDie = randomRIO (1, 6)
Scala との違い:
- Scala: IO.delay(random.nextInt(6) + 1) で副作用をラップ
- Haskell: randomRIO 自体が IO Int を返す
サイコロを2回振る¶
-- | サイコロを2回振って合計を返す
castTheDieTwice :: IO Int
castTheDieTwice = do
first <- castTheDie
second <- castTheDie
return (first + second)
Scala での同等のコード:
def castTheDieTwice(): IO[Int] = {
for {
first <- castTheDie()
second <- castTheDie()
} yield first + second
}
8.4 do 記法¶
Haskell の do 記法は、Scala の for 内包表記に相当します。
-- do 記法
castTheDieTwice :: IO Int
castTheDieTwice = do
first <- castTheDie
second <- castTheDie
return (first + second)
-- 脱糖後(>>=を使用)
castTheDieTwice :: IO Int
castTheDieTwice =
castTheDie >>= \first ->
castTheDie >>= \second ->
return (first + second)
| Haskell | Scala | 説明 |
|---|---|---|
do |
for |
モナド内包表記 |
<- |
<- |
値の取り出し |
return |
yield または IO.pure |
値をモナドに包む |
>>= |
flatMap |
モナドの連鎖 |
8.5 複数回のサイコロ¶
import Control.Monad (replicateM)
-- | サイコロをn回振って結果をリストで返す
castTheDieN :: Int -> IO [Int]
castTheDieN n = replicateM n castTheDie
replicateM は Scala の sequence + List.fill に相当:
// Scala
def castTheDieN(n: Int): IO[List[Int]] =
List.fill(n)(castTheDie()).sequence
8.6 ミーティングスケジューリングの例¶
データ型の定義¶
-- | ミーティング時間を表すデータ型
data MeetingTime = MeetingTime
{ mtStartHour :: Int
, mtEndHour :: Int
} deriving (Show, Eq)
重複判定(純粋関数)¶
-- | 2つのミーティングが重なっているかを判定
meetingsOverlap :: MeetingTime -> MeetingTime -> Bool
meetingsOverlap m1 m2 =
mtStartHour m1 < mtEndHour m2 && mtEndHour m1 > mtStartHour m2
空き時間の計算¶
-- | 可能なミーティング時間を計算(純粋関数)
possibleMeetings :: [MeetingTime] -> Int -> Int -> Int -> [MeetingTime]
possibleMeetings existingMeetings startHour endHour lengthHours =
let slots = [MeetingTime s (s + lengthHours) | s <- [startHour .. endHour - lengthHours]]
in filter (\slot -> all (not . meetingsOverlap slot) existingMeetings) slots
Haskell のリスト内包表記 [... | ...] は、Scala の for 内包表記に似ています:
// Scala
val slots = for {
s <- startHour to (endHour - lengthHours)
} yield MeetingTime(s, s + lengthHours)
8.7 IO の合成¶
-- | 2つの IO を合成
combineIO :: IO a -> IO b -> (a -> b -> c) -> IO c
combineIO io1 io2 f = do
a <- io1
b <- io2
return (f a b)
使用例:
ghci> result <- combineIO (return 1) (return 2) (+)
ghci> print result
3
IO のリストを順番に実行¶
-- | IO のリストを順番に実行
sequenceIO :: [IO a] -> IO [a]
sequenceIO [] = return []
sequenceIO (x:xs) = do
a <- x
as <- sequenceIO xs
return (a : as)
標準ライブラリの sequence と同じ機能です。
8.8 エラーハンドリング¶
import Control.Exception (try, SomeException)
-- | 例外をキャッチして Either に変換
catchIO :: IO a -> IO (Either String a)
catchIO action = do
result <- try action :: IO (Either SomeException a)
return $ case result of
Right a -> Right a
Left e -> Left (show e)
リトライ機能¶
-- | IO アクションをリトライ
retryIO :: Int -> IO a -> IO (Maybe a)
retryIO 0 _ = return Nothing
retryIO n action = do
result <- try action :: IO (Either SomeException a)
case result of
Right a -> return (Just a)
Left _ -> retryIO (n - 1) action
-- | リトライしてデフォルト値を返す
retryWithDefault :: Int -> a -> IO a -> IO a
retryWithDefault maxRetries defaultVal action = do
result <- retryIO maxRetries action
return $ maybe defaultVal id result
8.9 純粋関数と IO の分離¶
-- | 入力をパースして検証(純粋関数)
parseAndValidate :: String -> Either String Int
parseAndValidate input =
case reads input of
[(n, "")] | n > 0 -> Right n
| otherwise -> Left "Number must be positive"
_ -> Left "Invalid number"
-- | 入力を処理(IO と純粋関数の分離)
processInput :: IO ()
processInput = do
putStr "Enter a positive number: "
input <- getLine
case parseAndValidate input of
Right n -> putStrLn $ "Valid: " ++ show n
Left err -> putStrLn $ "Error: " ++ err
第9章: ストリーム処理¶
9.1 遅延リストとは¶
Haskell のリストは遅延評価されます。これにより、無限リストを扱うことができます。
Scala では fs2.Stream を使いますが、Haskell では標準のリストが遅延評価のため、そのまま無限ストリームとして機能します。
9.2 無限ストリームの基本¶
ソースファイル: app/haskell/src/Ch09/StreamProcessing.hs
-- | 自然数の無限ストリーム
naturals :: [Integer]
naturals = [1..]
-- | 偶数の無限ストリーム
evens :: [Integer]
evens = [2,4..]
-- | 奇数の無限ストリーム
odds :: [Integer]
odds = [1,3..]
ghci> take 10 naturals
[1,2,3,4,5,6,7,8,9,10]
ghci> take 5 evens
[2,4,6,8,10]
9.3 フィボナッチ数列¶
遅延評価を使った自己参照的な定義:
-- | フィボナッチ数列の無限ストリーム
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
ghci> take 10 fibs
[0,1,1,2,3,5,8,13,21,34]
9.4 素数のストリーム(エラトステネスのふるい)¶
-- | 素数の無限ストリーム
primes :: [Integer]
primes = sieve [2..]
where
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
sieve [] = []
ghci> take 10 primes
[2,3,5,7,11,13,17,19,23,29]
9.5 ストリーム生成関数¶
-- | 同じ値を無限に繰り返す(Scala の Stream.repeat に相当)
repeatVal :: a -> [a]
repeatVal x = x : repeatVal x
-- | リストを無限に繰り返す(Scala の Stream(...).repeat に相当)
cycleList :: [a] -> [a]
cycleList xs = xs ++ cycleList xs
-- | 関数を繰り返し適用して無限リストを生成
iterate' :: (a -> a) -> a -> [a]
iterate' f x = x : iterate' f (f x)
ghci> take 5 (repeatVal 1)
[1,1,1,1,1]
ghci> take 7 (cycleList [1,2,3])
[1,2,3,1,2,3,1]
ghci> take 5 (iterate' (*2) 1)
[1,2,4,8,16]
9.6 unfold: 汎用的なストリーム生成¶
-- | unfold: 種から無限リストを生成
unfold :: (b -> Maybe (a, b)) -> b -> [a]
unfold f seed = case f seed of
Nothing -> []
Just (a, b') -> a : unfold f b'
Scala の Stream.unfold に相当:
// Scala
Stream.unfold(0)(n => Some((n, n + 1)))
-- Haskell
ghci> take 5 (unfold (\n -> Just (n, n+1)) 0)
[0,1,2,3,4]
9.7 ストリーム操作¶
-- | 条件を満たす間、要素を取得(Scala の takeWhile)
takeWhile' :: (a -> Bool) -> [a] -> [a]
takeWhile' _ [] = []
takeWhile' p (x:xs)
| p x = x : takeWhile' p xs
| otherwise = []
-- | 条件を満たす間、要素をスキップ(Scala の dropWhile)
dropWhile' :: (a -> Bool) -> [a] -> [a]
dropWhile' _ [] = []
dropWhile' p xs@(x:xs')
| p x = dropWhile' p xs'
| otherwise = xs
ghci> takeWhile' (<5) [1,2,3,4,5,6]
[1,2,3,4]
ghci> dropWhile' (<5) [1,2,3,4,5,6]
[5,6]
9.8 スライディングウィンドウ¶
-- | スライディングウィンドウ(Scala の sliding に相当)
sliding :: Int -> [a] -> [[a]]
sliding n xs
| length window < n = []
| otherwise = window : sliding n (tail xs)
where
window = take n xs
ghci> sliding 3 [1,2,3,4,5]
[[1,2,3],[2,3,4],[3,4,5]]
9.9 チャンク分割¶
-- | 固定サイズのチャンクに分割
chunksOf :: Int -> [a] -> [[a]]
chunksOf _ [] = []
chunksOf n xs = take n xs : chunksOf n (drop n xs)
ghci> chunksOf 3 [1,2,3,4,5,6,7,8]
[[1,2,3],[4,5,6],[7,8]]
9.10 トレンド検出¶
通貨交換レートの監視に使用する関数:
-- | 上昇トレンドかどうかを判定
trending :: Ord a => [a] -> Bool
trending xs =
length xs > 1 &&
all (uncurry (<)) (zip xs (tail xs))
-- | 安定(全て同じ値)かどうかを判定
isStable :: Eq a => [a] -> Bool
isStable xs =
length xs >= 3 &&
all (== head xs) xs
ghci> trending [0.81, 0.82, 0.83]
True
ghci> trending [0.81, 0.84, 0.83]
False
ghci> isStable [5, 5, 5]
True
9.11 通貨交換レートの例¶
type ExchangeRate = Double
-- | 為替レートをシミュレート(無限ストリーム)
simulateRates :: ExchangeRate -> [ExchangeRate]
simulateRates initial = iterate' vary initial
where
vary rate = rate + sin (rate * 100) * 0.01
-- | 上昇トレンドを検出するまでレートを監視
findUptrend :: Int -> [ExchangeRate] -> Maybe [ExchangeRate]
findUptrend = findTrend
-- | ストリームから上昇トレンドを検出
findTrend :: Ord a => Int -> [a] -> Maybe [a]
findTrend windowSize xs =
case filter trending (sliding windowSize xs) of
[] -> Nothing
(w:_) -> Just w
トレンドを検出して交換¶
-- | 上昇トレンドを検出したら交換
exchangeIfTrending :: Double -> Int -> [ExchangeRate] -> Maybe Double
exchangeIfTrending amount windowSize rates =
case findUptrend windowSize rates of
Just trend -> Just $ amount * last trend
Nothing -> Nothing
ghci> exchangeIfTrending 100 3 [0.80, 0.81, 0.82, 0.83, 0.80]
Just 82.0
9.12 ストリーム合成¶
-- | インデックス付きストリーム(Scala の zipWithIndex)
zipWithIndex :: [a] -> [(Int, a)]
zipWithIndex = zip [0..]
-- | 2つのストリームを交互に合成
interleave :: [a] -> [a] -> [a]
interleave [] ys = ys
interleave xs [] = xs
interleave (x:xs) (y:ys) = x : y : interleave xs ys
-- | 2つのソート済みストリームをマージ
merge :: Ord a => [a] -> [a] -> [a]
merge [] ys = ys
merge xs [] = xs
merge (x:xs) (y:ys)
| x <= y = x : merge xs (y:ys)
| otherwise = y : merge (x:xs) ys
ghci> take 3 (zipWithIndex ['a', 'b', 'c'])
[(0,'a'),(1,'b'),(2,'c')]
ghci> take 6 (interleave [1,3,5] [2,4,6])
[1,2,3,4,5,6]
ghci> take 10 (merge [1,3,5,7,9] [2,4,6,8,10])
[1,2,3,4,5,6,7,8,9,10]
まとめ¶
Part IV で学んだこと¶
Scala と Haskell の比較¶
| 概念 | Scala (cats-effect/fs2) | Haskell |
|---|---|---|
| 副作用の型 | IO[A] |
IO a |
| 遅延実行 | IO.delay(expr) |
関数が IO を返す |
| 値のラップ | IO.pure(value) |
return value |
| モナド合成 | for 内包表記 |
do 記法 |
| flatMap | flatMap |
>>= |
| ストリーム | fs2.Stream[IO, A] |
標準の [a](遅延評価) |
| 無限繰り返し | Stream(...).repeat |
repeat / cycle |
| スライディング | stream.sliding(n) |
sliding n xs |
| リストの sequence | list.sequence |
sequence |
キーポイント¶
- IO モナド: Haskell では副作用を持つ計算は必ず
IO型を返す - do 記法: Scala の
for内包表記に相当、モナドを順序付けて合成 - 遅延評価: Haskell のリストはデフォルトで遅延評価、無限リストが自然に扱える
- 純粋関数との分離: ロジックは純粋関数で、IO は境界部分のみ
- ストリーム生成:
iterate、unfoldで無限ストリームを生成 - パターン検出:
slidingでウィンドウを作り、trendingでパターンを検出
次のステップ¶
Part V では、以下のトピックを学びます:
- 並行・並列処理
- Concurrent Haskell
- STM(Software Transactional Memory)
- async ライブラリ
演習問題¶
問題 1: IO の基本¶
以下の関数を実装してください。
-- メッセージを出力して返す
printAndReturn :: String -> IO String
printAndReturn message = ???
-- 期待される動作
-- > result <- printAndReturn "Hello"
-- Hello
-- > result
-- "Hello"
解答
printAndReturn :: String -> IO String
printAndReturn message = do
putStrLn message
return message
問題 2: IO の合成¶
以下の関数を実装してください。2つの IO を順番に実行し、結果を結合します。
combineIO :: IO a -> IO b -> (a -> b -> c) -> IO c
combineIO io1 io2 f = ???
-- 期待される動作
-- > result <- combineIO (return 1) (return 2) (+)
-- > result
-- 3
解答
combineIO :: IO a -> IO b -> (a -> b -> c) -> IO c
combineIO io1 io2 f = do
a <- io1
b <- io2
return (f a b)
問題 3: リトライ¶
以下の関数を実装してください。指定回数リトライし、全部失敗したらデフォルト値を返します。
retryWithDefault :: Int -> a -> IO a -> IO a
retryWithDefault maxRetries defaultVal action = ???
解答
import Control.Exception (try, SomeException)
retryWithDefault :: Int -> a -> IO a -> IO a
retryWithDefault 0 defaultVal _ = return defaultVal
retryWithDefault n defaultVal action = do
result <- try action :: IO (Either SomeException a)
case result of
Right a -> return a
Left _ -> retryWithDefault (n - 1) defaultVal action
問題 4: ストリーム操作¶
以下のストリームを作成してください。
-- 1. 1から10までの偶数のリスト
evens :: [Int]
evens = ???
-- 2. 無限に交互に True/False を返すリスト
alternating :: [Bool]
alternating = ???
-- 3. 最初の5つの要素の合計を計算
sumFirst5 :: [Int] -> Int
sumFirst5 xs = ???
解答
-- 1. 1から10までの偶数
evens :: [Int]
evens = [2, 4, 6, 8, 10]
-- または
evens = filter even [1..10]
-- 2. 無限に交互に True/False
alternating :: [Bool]
alternating = cycle [True, False]
-- 3. 最初の5つの要素の合計
sumFirst5 :: [Int] -> Int
sumFirst5 xs = sum (take 5 xs)
問題 5: トレンド検出¶
以下の関数を実装してください。直近の値が減少傾向かどうかを判定します。
declining :: Ord a => [a] -> Bool
declining xs = ???
-- 期待される動作
-- declining [0.83, 0.82, 0.81] -- True (減少トレンド)
-- declining [0.81, 0.82, 0.83] -- False
-- declining [0.81] -- False (1要素では判定不可)
解答
declining :: Ord a => [a] -> Bool
declining xs =
length xs > 1 &&
all (uncurry (>)) (zip xs (tail xs))