Skip to content

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

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


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

3.1 イミュータブルとは

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

uml diagram

3.2 List の基本操作

ソースファイル: app/scala/src/main/scala/ch03_SlicingAndAppending.scala

appended - 要素の追加

val appleBook      = List("Apple", "Book")
val appleBookMango = appleBook.appended("Mango")

assert(appleBook.size == 2)       // 元のリストは変わらない
assert(appleBookMango.size == 3)  // 新しいリストが作成される
assert(appleBookMango == List("Apple", "Book", "Mango"))

slice - リストの切り出し

def firstTwo(list: List[String]): List[String] =
  list.slice(0, 2)

def lastTwo(list: List[String]): List[String] =
  list.slice(list.size - 2, list.size)

assert(firstTwo(List("a", "b", "c")) == List("a", "b"))
assert(lastTwo(List("a", "b", "c")) == List("b", "c"))

uml diagram

3.3 リストの変換例

ソースファイル: app/scala/src/main/scala/ch03_SlicingAndAppending.scala

// 最初の2要素を末尾に移動
def movedFirstTwoToTheEnd(list: List[String]): List[String] = {
  val firstTwo        = list.slice(0, 2)
  val withoutFirstTwo = list.slice(2, list.size)
  withoutFirstTwo.appendedAll(firstTwo)
}

assert(movedFirstTwoToTheEnd(List("a", "b", "c")) == List("c", "a", "b"))

// 最後の要素の前に挿入
def insertedBeforeLast(list: List[String], element: String): List[String] = {
  val last        = list.slice(list.size - 1, list.size)
  val withoutLast = list.slice(0, list.size - 1)
  withoutLast.appended(element).appendedAll(last)
}

assert(insertedBeforeLast(List("a", "b"), "c") == List("a", "c", "b"))

3.4 旅程の再計画

ソースファイル: app/scala/src/main/scala/ch03_ItineraryScala.scala

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

def replan(plan: List[String], newCity: String, beforeCity: String): List[String] = {
  val beforeCityIndex = plan.indexOf(beforeCity)
  val citiesBefore    = plan.slice(0, beforeCityIndex)
  val citiesAfter     = plan.slice(beforeCityIndex, plan.size)
  citiesBefore.appended(newCity).appendedAll(citiesAfter)
}

val planA = List("Paris", "Berlin", "Kraków")
val planB = replan(planA, "Vienna", "Kraków")

assert(planB == List("Paris", "Berlin", "Vienna", "Kraków"))
assert(planA == List("Paris", "Berlin", "Kraków"))  // 元の計画は変わらない!

uml diagram

3.5 String と List の類似性

ソースファイル: app/scala/src/main/scala/ch03_ListVsStringScala.scala

String と List は似た操作ができます。

操作 List String
結合 appendedAll concat
切り出し slice substring
サイズ size length
// List の操作
val abList   = List("a", "b")
val cdList   = List("c", "d")
val abcdList = abList.appendedAll(cdList)
assert(abcdList == List("a", "b", "c", "d"))

// String の操作
val abStr   = "ab"
val cdStr   = "cd"
val abcdStr = abStr.concat(cdStr)
assert(abcdStr == "abcd")

3.6 名前の省略

ソースファイル: app/scala/src/main/scala/ch03_AbbreviateNames.scala

def abbreviate(name: String): String = {
  val initial   = name.substring(0, 1)
  val separator = name.indexOf(' ')
  val lastName  = name.substring(separator + 1)
  initial + ". " + lastName
}

assert(abbreviate("Alonzo Church") == "A. Church")
assert(abbreviate("A. Church") == "A. Church")

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

4.1 高階関数とは

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

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

uml diagram

4.2 関数を引数として渡す

ソースファイル: app/scala/src/main/scala/ch04_PassingFunctions.scala

sortBy - ソート基準を関数で指定

def score(word: String): Int = word.replaceAll("a", "").length

val words = List("rust", "java")
val sortedWords = words.sortBy(score)

assert(sortedWords == List("java", "rust"))
// java: 2文字 (j, v), rust: 4文字 (r, u, s, t)

uml diagram

map - 各要素を変換

def len(s: String): Int = s.length

val lengths = List("scala", "rust", "ada").map(len)
assert(lengths == List(5, 4, 3))

def double(i: Int): Int = 2 * i
val doubles = List(5, 1, 2, 4, 0).map(double)
assert(doubles == List(10, 2, 4, 8, 0))

uml diagram

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

def odd(i: Int): Boolean = i % 2 == 1

val odds = List(5, 1, 2, 4, 0).filter(odd)
assert(odds == List(5, 1))

def largerThan4(i: Int): Boolean = i > 4
val large = List(5, 1, 2, 4, 0).filter(largerThan4)
assert(large == List(5))

foldLeft - 畳み込み

val sum = List(5, 1, 2, 4, 100).foldLeft(0)((acc, i) => acc + i)
assert(sum == 112)

val max = List(5, 1, 2, 4, 15).foldLeft(Int.MinValue)((max, i) =>
  if (i > max) i else max
)
assert(max == 15)

uml diagram

4.3 case class とパターン

ソースファイル: app/scala/src/main/scala/ch04_ProgrammingLanguages.scala

case class ProgrammingLanguage(name: String, year: Int)

val javalang  = ProgrammingLanguage("Java", 1995)
val scalalang = ProgrammingLanguage("Scala", 2004)

val languages = List(javalang, scalalang)

// フィールドにアクセス
val names = languages.map(lang => lang.name)
assert(names == List("Java", "Scala"))

// 省略記法(アンダースコア)
val names2 = languages.map(_.name)
assert(names2 == List("Java", "Scala"))

// 条件でフィルタ
val young = languages.filter(_.year > 2000)
assert(young == List(scalalang))

4.4 関数を返す関数

ソースファイル: app/scala/src/main/scala/ch04_ReturningFunctions.scala

def largerThan(n: Int): Int => Boolean = i => i > n

val large = List(5, 1, 2, 4, 0).filter(largerThan(4))
assert(large == List(5))

val medium = List(5, 1, 2, 4, 0).filter(largerThan(1))
assert(medium == List(5, 2, 4))

uml diagram

4.5 カリー化

カリー化(Currying)は、複数の引数を取る関数を、引数を1つずつ取る関数のチェーンに変換する技法です。

// 通常の関数
def largerThanNormal(n: Int, i: Int): Boolean = i > n

// カリー化された関数
def largerThan(n: Int)(i: Int): Boolean = i > n

// 使用例
val large = List(5, 1, 2, 4, 0).filter(largerThan(4))
assert(large == List(5))

uml diagram

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

ソースファイル: app/scala/src/main/scala/ch04_WordScoringScala.scala

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

def score(word: String): Int = word.replaceAll("a", "").length
def bonus(word: String): Int = if (word.contains("c")) 5 else 0
def penalty(word: String): Int = if (word.contains("s")) 7 else 0

val words = List("ada", "haskell", "scala", "java", "rust")

// 関数を引数として渡す
def rankedWords(wordScore: String => Int, words: List[String]): List[String] = {
  words.sortBy(wordScore).reverse
}

// 基本スコアでランキング
val ranking1 = rankedWords(score, words)
assert(ranking1 == List("haskell", "rust", "scala", "java", "ada"))

// ボーナス付きスコアでランキング
val ranking2 = rankedWords(w => score(w) + bonus(w), words)
assert(ranking2 == List("scala", "haskell", "rust", "java", "ada"))

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

第5章: flatMap とネスト構造

5.1 flatten と flatMap

ソースファイル: app/scala/src/main/scala/ch05_BookAdaptations.scala

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

case class Book(title: String, authors: List[String])

val books = List(
  Book("FP in Scala", List("Chiusano", "Bjarnason")),
  Book("The Hobbit", List("Tolkien"))
)

// map だけだとネストする
val authorLists = books.map(_.authors)
// List(List("Chiusano", "Bjarnason"), List("Tolkien"))

// flatten で平坦化
val authors = authorLists.flatten
// List("Chiusano", "Bjarnason", "Tolkien")

uml diagram

flatMap = map + flatten

// map して flatten
val authors1 = books.map(_.authors).flatten

// flatMap で同じことを1行で
val authors2 = books.flatMap(_.authors)

assert(authors1 == authors2)
// List("Chiusano", "Bjarnason", "Tolkien")

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

// 要素数が増える
List(1, 2, 3).flatMap(i => List(i, i + 10))
// List(1, 11, 2, 12, 3, 13) - 6要素

// 要素数が同じ
List(1, 2, 3).flatMap(i => List(i * 2))
// List(2, 4, 6) - 3要素

// 要素数が減る(フィルタリング効果)
List(1, 2, 3).flatMap(i => if (i % 2 == 0) List(i) else List.empty)
// List(2) - 1要素

uml diagram

5.3 ネストした flatMap

複数のリストを組み合わせる場合、flatMap をネストします。

case class Book(title: String, authors: List[String])
case class Movie(title: String)

def bookAdaptations(author: String): List[Movie] = {
  if (author == "Tolkien")
    List(Movie("An Unexpected Journey"), Movie("The Desolation of Smaug"))
  else
    List.empty
}

val books = List(
  Book("FP in Scala", List("Chiusano", "Bjarnason")),
  Book("The Hobbit", List("Tolkien"))
)

// ネストした flatMap
val recommendations = books.flatMap(book =>
  book.authors.flatMap(author =>
    bookAdaptations(author).map(movie =>
      s"You may like ${movie.title}, because you liked $author's ${book.title}"
    )
  )
)

// 結果:
// List(
//   "You may like An Unexpected Journey, because you liked Tolkien's The Hobbit",
//   "You may like The Desolation of Smaug, because you liked Tolkien's The Hobbit"
// )

uml diagram

5.4 for 内包表記

ネストした flatMap は for 内包表記で読みやすく書けます。

// flatMap/map 版
val c1 = books.flatMap(book =>
  book.authors.flatMap(author =>
    bookAdaptations(author).map(movie =>
      s"You may like ${movie.title}, because you liked $author's ${book.title}"
    )
  )
)

// for 内包表記版(等価)
val c2 = for {
  book   <- books
  author <- book.authors
  movie  <- bookAdaptations(author)
} yield s"You may like ${movie.title}, because you liked $author's ${book.title}"

assert(c1 == c2)

uml diagram

5.5 円内の点の判定

ソースファイル: app/scala/src/main/scala/ch05_PointsInsideCircles.scala

for 内包表記でフィルタリングも行う例です。

case class Point(x: Int, y: Int)

val points   = List(Point(5, 2), Point(1, 1))
val radiuses = List(2, 1)

def isInside(point: Point, radius: Int): Boolean = {
  radius * radius >= point.x * point.x + point.y * point.y
}

// 全組み合わせを生成
val allCombinations = for {
  r     <- radiuses
  point <- points
} yield s"$point is within a radius of $r: ${isInside(point, r)}"

// 結果:
// List(
//   "Point(5,2) is within a radius of 2: false",
//   "Point(1,1) is within a radius of 2: true",
//   "Point(5,2) is within a radius of 1: false",
//   "Point(1,1) is within a radius of 1: false"
// )

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

// if を使ったフィルタリング(ガード式)
val insidePoints = for {
  r     <- radiuses
  point <- points
  if isInside(point, r)  // ガード式
} yield s"$point is within a radius of $r"

// 結果: List("Point(1,1) is within a radius of 2")

uml diagram

5.6 for 内包表記と型

ソースファイル: app/scala/src/main/scala/ch05_RandomForComprehensions.scala

for 内包表記の戻り値の型は、最初のジェネレータの型に依存します。

// List から始まる → List を返す
val listResult = for {
  a <- List(1, 2)
  b <- Set(2, 1)
} yield a * b
// List(2, 1, 4, 2) - 重複あり

// Set から始まる → Set を返す
val setResult = for {
  a <- Set(1, 2)
  b <- List(2, 1)
} yield a * b
// Set(2, 1, 4) - 重複なし

uml diagram


まとめ

Part II で学んだこと

uml diagram

キーポイント

主要概念 キー操作
第3章 イミュータブル slice, appended, appendedAll
第4章 高階関数 map, filter, foldLeft, sortBy
第5章 平坦化 flatten, flatMap, for 内包表記

重要な法則

  1. イミュータブルデータ: 元のデータは変更せず、新しいデータを作成する
  2. 関数は値: 関数を引数として渡したり、戻り値として返したりできる
  3. flatMap パターン: ネストした構造を平坦化しながら変換する
  4. for 内包表記: flatMap/map の糖衣構文として使える

次のステップ

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

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

演習問題

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

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

def insertAtMiddle(list: List[String], element: String): List[String] = ???

// 期待される動作
assert(insertAtMiddle(List("a", "b", "c", "d"), "X") == List("a", "b", "X", "c", "d"))
assert(insertAtMiddle(List("a", "b"), "X") == List("a", "X", "b"))
解答
def insertAtMiddle(list: List[String], element: String): List[String] = {
  val middle = list.size / 2
  val before = list.slice(0, middle)
  val after  = list.slice(middle, list.size)
  before.appended(element).appendedAll(after)
}

問題 2: 高階関数

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

def countWhere[A](list: List[A], predicate: A => Boolean): Int = ???

// 期待される動作
assert(countWhere(List(1, 2, 3, 4, 5), (i: Int) => i > 3) == 2)
assert(countWhere(List("a", "bb", "ccc"), (s: String) => s.length > 1) == 2)
解答
def countWhere[A](list: List[A], predicate: A => Boolean): Int = {
  list.filter(predicate).size
}

// または foldLeft を使って
def countWhere2[A](list: List[A], predicate: A => Boolean): Int = {
  list.foldLeft(0)((count, elem) => if (predicate(elem)) count + 1 else count)
}

問題 3: for 内包表記

以下のネストした flatMap を for 内包表記で書き換えてください。

val result = List(1, 2).flatMap(x =>
  List(10, 20).flatMap(y =>
    List(100, 200).map(z =>
      x + y + z
    )
  )
)
解答
val result = for {
  x <- List(1, 2)
  y <- List(10, 20)
  z <- List(100, 200)
} yield x + y + z

// 結果: List(111, 211, 121, 221, 112, 212, 122, 222)

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

for 内包表記の if を使わずに、flatMap だけで偶数のみを抽出するコードを書いてください。

val numbers = List(1, 2, 3, 4, 5, 6)
// 偶数のみを抽出: List(2, 4, 6)
解答
val evenNumbers = numbers.flatMap(n =>
  if (n % 2 == 0) List(n) else List.empty
)

// または関数に分離
def evenFilter(n: Int): List[Int] =
  if (n % 2 == 0) List(n) else List.empty

val evenNumbers2 = numbers.flatMap(evenFilter)