Skip to content

Part II: 関数型スタイルのプログラミング

本章では、関数型プログラミングの核心となるテクニックを Haskell で学びます。イミュータブルなデータ操作、高階関数、そして concatMap(flatMap)による複雑なデータ変換を習得します。


第3章: イミュータブルなデータ操作

3.1 イミュータブルとは

イミュータブル(不変)とは、一度作成されたデータが変更されないことを意味します。データを「変更」する代わりに、新しいデータを「作成」します。

uml diagram

Haskell ではすべてのデータがデフォルトでイミュータブルです。

3.2 List の基本操作

ソースファイル: app/haskell/src/Ch03/ImmutableValues.hs

要素の追加

-- 要素を末尾に追加(新しいリストを返す)
appended :: [a] -> a -> [a]
appended xs x = xs ++ [x]

-- 使用例
let appleBook      = ["Apple", "Book"]
let appleBookMango = appended appleBook "Mango"

-- appleBook は ["Apple", "Book"] のまま(変わらない)
-- appleBookMango は ["Apple", "Book", "Mango"]

slice - リストの切り出し

-- リストの一部を切り出す(start <= index < end)
slice :: Int -> Int -> [a] -> [a]
slice start end xs = take (end - start) $ drop start xs

-- 最初の2要素を取得
firstTwo :: [a] -> [a]
firstTwo = take 2

-- 最後の2要素を取得
lastTwo :: [a] -> [a]
lastTwo xs = drop (length xs - 2) xs

-- 使用例
firstTwo ["a", "b", "c"]  -- ["a", "b"]
lastTwo ["a", "b", "c"]   -- ["b", "c"]
slice 1 3 ["a","b","c","d"]  -- ["b", "c"]

uml diagram

3.3 リストの変換例

-- 最初の2要素を末尾に移動
movedFirstTwoToTheEnd :: [a] -> [a]
movedFirstTwoToTheEnd xs =
    let first = take 2 xs
        rest  = drop 2 xs
    in rest ++ first

-- 使用例
movedFirstTwoToTheEnd ["a","b","c"]  -- ["c","a","b"]

-- 中央に要素を挿入
insertAtMiddle :: [a] -> a -> [a]
insertAtMiddle xs element =
    let middle = length xs `div` 2
        before = take middle xs
        after  = drop middle xs
    in before ++ [element] ++ after

-- 使用例
insertAtMiddle ["a","b","c","d"] "X"  -- ["a","b","X","c","d"]

3.4 旅程の再計画

旅行の計画変更をイミュータブルに行う例です。

-- 旅程を再計画する(指定都市の前に新都市を挿入)
replan :: [String] -> String -> String -> [String]
replan plan newCity beforeCity =
    let beforeCityIndex = findIndex beforeCity plan
        citiesBefore    = take beforeCityIndex plan
        citiesAfter     = drop beforeCityIndex plan
    in citiesBefore ++ [newCity] ++ citiesAfter

-- 使用例
let planA = ["Paris", "Berlin", "Krakow"]
let planB = replan planA "Vienna" "Krakow"

-- planA は ["Paris", "Berlin", "Krakow"] のまま!
-- planB は ["Paris", "Berlin", "Vienna", "Krakow"]

uml diagram

3.5 名前の省略

-- 名前を省略形にする
abbreviate :: String -> String
abbreviate name =
    let initial   = take 1 name
        separator = findSpaceIndex name
        lastName  = drop (separator + 1) name
    in initial ++ ". " ++ lastName

-- 使用例
abbreviate "Alonzo Church"  -- "A. Church"
abbreviate "Haskell Curry"  -- "H. Curry"

第4章: 関数を値として扱う

4.1 高階関数とは

高階関数(Higher-Order Function)とは、以下のいずれかを満たす関数です:

  1. 関数を引数として受け取る
  2. 関数を戻り値として返す

uml diagram

4.2 map - 各要素を変換

ソースファイル: app/haskell/src/Ch04/FunctionsAsValues.hs

-- map の実装
myMap :: (a -> b) -> [a] -> [b]
myMap _ []     = []
myMap f (x:xs) = f x : myMap f xs

-- 使用例
map (+1) [1, 2, 3]        -- [2, 3, 4]
map length ["scala", "rust", "ada"]  -- [5, 4, 3]
map (*2) [5, 1, 2, 4, 0]  -- [10, 2, 4, 8, 0]

uml diagram

4.3 filter - 条件に合う要素を抽出

-- filter の実装
myFilter :: (a -> Bool) -> [a] -> [a]
myFilter _ []     = []
myFilter p (x:xs)
    | p x       = x : myFilter p xs
    | otherwise = myFilter p xs

-- 使用例
filter odd [5, 1, 2, 4, 0]   -- [5, 1]
filter (> 4) [5, 1, 2, 4, 0] -- [5]
filter even [1, 2, 3, 4, 5, 6] -- [2, 4, 6]

4.4 foldl - 畳み込み

-- foldl の実装
myFoldl :: (b -> a -> b) -> b -> [a] -> b
myFoldl _ acc []     = acc
myFoldl f acc (x:xs) = myFoldl f (f acc x) xs

-- 使用例
foldl (+) 0 [5, 1, 2, 4, 100]  -- 112
foldl max minBound [5, 1, 2, 4, 15]  -- 15
foldl (*) 1 [1, 2, 3, 4, 5]  -- 120 (階乗)

uml diagram

4.5 ワードスコアリングの例

複数のスコアリングロジックを組み合わせる例です。

-- 'a' を除いた文字数をスコアとして計算
score :: String -> Int
score word = length $ filter (/= 'a') word

-- 'c' を含む場合のボーナス
bonus :: String -> Int
bonus word = if 'c' `elem` word then 5 else 0

-- 's' を含む場合のペナルティ
penalty :: String -> Int
penalty word = if 's' `elem` word then 7 else 0

-- スコア関数を使って単語をランキング
rankedWords :: (String -> Int) -> [String] -> [String]
rankedWords wordScore words =
    reverse $ sortBy (comparing wordScore) words

-- 使用例
let words = ["ada", "haskell", "scala", "java", "rust"]

-- 基本スコアでランキング
rankedWords score words
-- ["haskell", "rust", "scala", "java", "ada"]

-- ボーナス付きスコアでランキング
rankedWords (\w -> score w + bonus w) words
-- ["scala", "haskell", "rust", "java", "ada"]

-- ボーナスとペナルティ付きスコアでランキング
rankedWords (\w -> score w + bonus w - penalty w) words
-- ["java", "scala", "ada", "haskell", "rust"]

4.6 関数を返す関数

-- n より大きいかを判定する関数を返す
largerThan :: Int -> (Int -> Bool)
largerThan n = \i -> i > n

-- 使用例
filter (largerThan 4) [5, 1, 2, 4, 0]  -- [5]
filter (largerThan 1) [5, 1, 2, 4, 0]  -- [5, 2, 4]

-- n で割り切れるかを判定する関数を返す
divisibleBy :: Int -> (Int -> Bool)
divisibleBy n = \i -> i `mod` n == 0

-- 使用例
filter (divisibleBy 3) [1, 2, 3, 6, 9, 10]  -- [3, 6, 9]

uml diagram

4.7 カリー化

Haskell では、すべての関数がデフォルトでカリー化されています。

-- Haskell の関数は自動的にカリー化される
addCurried :: Int -> Int -> Int
addCurried a b = a + b

-- 部分適用
let add5 = addCurried 5
add5 3   -- 8
add5 10  -- 15

-- 3引数の関数も同様
multiply3 :: Int -> Int -> Int -> Int
multiply3 a b c = a * b * c

let double = multiply3 2 1
double 5  -- 10
double 7  -- 14

uml diagram

4.8 レコード型とパターン

-- プログラミング言語を表すデータ型
data ProgrammingLanguage = ProgrammingLanguage
    { plName :: String
    , plYear :: Int
    } deriving (Show, Eq)

let java    = ProgrammingLanguage "Java" 1995
let scala   = ProgrammingLanguage "Scala" 2004
let haskell = ProgrammingLanguage "Haskell" 1990
let langs = [java, scala, haskell]

-- フィールドにアクセス
map plName langs  -- ["Java", "Scala", "Haskell"]

-- 条件でフィルタ
filter (\lang -> plYear lang > 2000) langs
-- [ProgrammingLanguage "Scala" 2004]

-- 年でソート
sortBy (comparing plYear) langs
-- [Haskell, Java, Scala]

第5章: concatMap とネスト構造

5.1 concat と concatMap

ソースファイル: app/haskell/src/Ch05/SequentialPrograms.hs

concat - ネストしたリストを平坦化

-- concat の動作
concat [[1, 2], [3], [4, 5, 6]]  -- [1, 2, 3, 4, 5, 6]
concat [["A", "B"], ["C"]]       -- ["A", "B", "C"]

uml diagram

concatMap = map + concat(= flatMap)

-- concatMap の動作
-- Scala の flatMap に相当
concatMap (\x -> [x, x + 10]) [1, 2, 3]
-- [1, 11, 2, 12, 3, 13]

-- 本の著者をすべて取得
data Book = Book { bookTitle :: String, bookAuthors :: [String] }

let books = [ Book "FP in Scala" ["Chiusano", "Bjarnason"]
            , Book "The Hobbit" ["Tolkien"]
            ]

-- map だけだとネストする
map bookAuthors books
-- [["Chiusano", "Bjarnason"], ["Tolkien"]]

-- concatMap で平坦化
concatMap bookAuthors books
-- ["Chiusano", "Bjarnason", "Tolkien"]

5.2 concatMap によるリストサイズの変化

-- 要素数が増える
concatMap (\i -> [i, i + 10]) [1, 2, 3]
-- [1, 11, 2, 12, 3, 13] - 6要素

-- 要素数が同じ
concatMap (\i -> [i * 2]) [1, 2, 3]
-- [2, 4, 6] - 3要素

-- 要素数が減る(フィルタリング効果)
concatMap (\i -> if even i then [i] else []) [1, 2, 3]
-- [2] - 1要素

uml diagram

5.3 do 記法

ネストした concatMap は do 記法で読みやすく書けます。

-- 本の映画化推奨を生成
getRecommendations :: [Book] -> [String]
getRecommendations books = do
    book   <- books
    author <- bookAuthors book
    movie  <- bookAdaptations author
    return $ "You may like " ++ movieTitle movie ++
             ", because you liked " ++ author ++ "'s " ++ bookTitle book

-- concatMap 版(等価)
getRecommendations' :: [Book] -> [String]
getRecommendations' books =
    concatMap (\book ->
        concatMap (\author ->
            map (\movie ->
                "You may like " ++ movieTitle movie ++
                ", because you liked " ++ author ++ "'s " ++ bookTitle book
            ) (bookAdaptations author)
        ) (bookAuthors book)
    ) books

uml diagram

5.4 円内の点の判定

do 記法でフィルタリングも行う例です。

data Point = Point { pointX :: Int, pointY :: Int }

-- 点が指定半径の円内にあるか判定
isInside :: Point -> Int -> Bool
isInside point radius =
    radius * radius >= pointX point * pointX point + pointY point * pointY point

-- 全組み合わせを生成
let points   = [Point 1 1, Point 5 2]
let radiuses = [2, 1]

allCombinations :: [Point] -> [Int] -> [(String, Int, Bool)]
allCombinations points radiuses = do
    r     <- radiuses
    point <- points
    let desc = "Point " ++ show (pointX point) ++ " " ++ show (pointY point)
    return (desc, r, isInside point r)

-- 結果:
-- [("Point 1 1", 2, True), ("Point 5 2", 2, False),
--  ("Point 1 1", 1, False), ("Point 5 2", 1, False)]

ガード式によるフィルタリング

-- 円内にある点のみを返す
pointsInsideRadius :: [Point] -> Int -> [Point]
pointsInsideRadius points radius = do
    point <- points
    if isInside point radius
        then return point
        else []

-- 使用例
pointsInsideRadius [Point 1 1, Point 5 2] 2
-- [Point 1 1]

5.5 リスト内包表記

Haskell には do 記法の代わりにリスト内包表記も使えます。

-- do 記法
combinations :: [a] -> [b] -> [(a, b)]
combinations xs ys = do
    x <- xs
    y <- ys
    return (x, y)

-- リスト内包表記(等価)
combinations' :: [a] -> [b] -> [(a, b)]
combinations' xs ys = [(x, y) | x <- xs, y <- ys]

-- ガード付きリスト内包表記
evenPairs :: [Int] -> [Int] -> [(Int, Int)]
evenPairs xs ys = [(x, y) | x <- xs, y <- ys, even (x + y)]

5.6 デカルト積

-- 3つのリストのデカルト積
cartesianProduct :: [Int] -> [Int] -> [Int] -> [Int]
cartesianProduct xs ys zs = do
    x <- xs
    y <- ys
    z <- zs
    return (x + y + z)

-- 使用例
cartesianProduct [1, 2] [10, 20] [100, 200]
-- [111, 211, 121, 221, 112, 212, 122, 222]

-- リスト内包表記版
cartesianProduct' xs ys zs = [x + y + z | x <- xs, y <- ys, z <- zs]

まとめ

Part II で学んだこと

uml diagram

キーポイント

主要概念 キー操作
第3章 イミュータブル take, drop, (++), slice
第4章 高階関数 map, filter, foldl, sortBy
第5章 平坦化 concat, concatMap, do 記法

Scala との対応

Scala Haskell 説明
list.appended(x) list ++ [x] 要素追加
list.slice(s, e) take (e-s) $ drop s list スライス
list.map(f) map f list 変換
list.filter(p) filter p list フィルタ
list.foldLeft(z)(f) foldl f z list 畳み込み
list.flatten concat list 平坦化
list.flatMap(f) concatMap f list 変換+平坦化
for { ... } yield do { ... } または [ | ] 内包表記

重要な法則

  1. イミュータブルデータ: Haskell ではすべてデフォルトでイミュータブル
  2. 関数は値: 関数を引数として渡したり、戻り値として返したりできる
  3. 自動カリー化: Haskell のすべての関数は自動的にカリー化される
  4. concatMap パターン: ネストした構造を平坦化しながら変換する
  5. do 記法: concatMap/map の糖衣構文として使える

次のステップ

Part III では、以下のトピックを学びます:

  • Maybe 型による安全なエラーハンドリング
  • Either 型と複合的なエラー処理

演習問題

問題 1: イミュータブルな操作

以下の関数を実装してください。リストの中央に要素を挿入する関数です。

insertAtMiddle :: [a] -> a -> [a]
insertAtMiddle = ???

-- 期待される動作
insertAtMiddle ["a","b","c","d"] "X" == ["a","b","X","c","d"]
insertAtMiddle ["a","b"] "X" == ["a","X","b"]
解答
insertAtMiddle :: [a] -> a -> [a]
insertAtMiddle xs element =
    let middle = length xs `div` 2
        before = take middle xs
        after  = drop middle xs
    in before ++ [element] ++ after

問題 2: 高階関数

以下の関数を実装してください。条件を満たす要素の数をカウントする関数です。

countWhere :: (a -> Bool) -> [a] -> Int
countWhere = ???

-- 期待される動作
countWhere (> 3) [1, 2, 3, 4, 5] == 2
countWhere (\s -> length s > 1) ["a", "bb", "ccc"] == 2
解答
countWhere :: (a -> Bool) -> [a] -> Int
countWhere p xs = length $ filter p xs

-- または foldl を使って
countWhere' :: (a -> Bool) -> [a] -> Int
countWhere' p = foldl (\count elem -> if p elem then count + 1 else count) 0

問題 3: do 記法

以下のネストした concatMap を do 記法で書き換えてください。

result = concatMap (\x ->
    concatMap (\y ->
        map (\z ->
            x + y + z
        ) [100, 200]
    ) [10, 20]
) [1, 2]
解答
result = do
    x <- [1, 2]
    y <- [10, 20]
    z <- [100, 200]
    return (x + y + z)

-- またはリスト内包表記
result' = [x + y + z | x <- [1, 2], y <- [10, 20], z <- [100, 200]]

-- 結果: [111, 211, 121, 221, 112, 212, 122, 222]

問題 4: concatMap によるフィルタリング

do 記法の if を使わずに、concatMap だけで偶数のみを抽出するコードを書いてください。

numbers = [1, 2, 3, 4, 5, 6]
-- 偶数のみを抽出: [2, 4, 6]
解答
evenNumbers = concatMap (\n -> if even n then [n] else []) numbers

-- または関数に分離
evenFilter :: Int -> [Int]
evenFilter n = if even n then [n] else []

evenNumbers' = concatMap evenFilter numbers