Skip to content

Part I: 関数型プログラミングの基礎

本章では、関数型プログラミング(FP)の基本概念を TypeScript と fp-ts を使って学びます。命令型プログラミングとの違いを理解し、純粋関数の利点を実感することが目標です。


第1章: 関数型プログラミング入門

1.1 命令型 vs 関数型

プログラミングには大きく分けて2つのパラダイムがあります。

uml diagram

命令型プログラミングは「どうやるか」を記述します。

// TypeScript: 命令型でワードスコアを計算
const calculateScoreImperative = (word: string): number => {
  let score = 0
  for (const _ of word) {
    score += 1
  }
  return score
}

関数型プログラミングは「何をするか」を記述します。

// TypeScript: 関数型でワードスコアを計算
const wordScore = (word: string): number => word.length

1.2 TypeScript の基本構文

TypeScript での関数定義の基本形を見てみましょう。

ソースファイル: app/typescript/src/ch01_intro.ts

// アロー関数(推奨)
const increment = (x: number): number => x + 1

const getFirstCharacter = (s: string): string => s.charAt(0)

const wordScore = (word: string): number => word.length

1.3 関数の構造

uml diagram

1.4 fp-ts の pipe を使った関数合成

TypeScript では fp-ts の pipe を使って関数を合成できます。

import { pipe } from 'fp-ts/function'

// 文字列を大文字にして反転する
const transformString = (s: string): string =>
  pipe(
    s,
    toUppercase,
    reverseString
  )

// 値を2倍にして1増加させる
const transformNumber = (x: number): number =>
  pipe(
    x,
    double,
    increment
  )

pipe は左から右へデータが流れるように読めるため、可読性が向上します。

1.5 学習ポイント

概念 命令型 関数型
焦点 手順(How) 結果(What)
状態 変更する 変換する
ループ for/while map/filter/reduce
データ ミュータブル イミュータブル
合成 手続き呼び出し pipe/flow

第2章: 純粋関数とテスト

2.1 純粋関数とは

純粋関数(Pure Function)は以下の特徴を持つ関数です:

  1. 同じ入力には常に同じ出力を返す
  2. 副作用がない(外部状態を変更しない)

uml diagram

2.2 純粋関数の例

ソースファイル: app/typescript/src/ch02_pure_functions.ts

// 純粋関数の例
const increment = (x: number): number => x + 1

const add = (a: number, b: number): number => a + b

const getFirstCharacter = (s: string): string => s.charAt(0)

純粋ではない関数の例:

// 不純な関数 - Math.random() は毎回異なる値を返す
const randomPart = (x: number): number => x * Math.random()

// 不純な関数 - 外部状態を変更する
let counter = 0
const incrementCounter = (): number => {
  counter += 1
  return counter
}

2.3 ショッピングカートの例

状態を持つクラスの問題点を見てみましょう。

問題のあるコード

class ShoppingCartBad {
  private items: string[] = []
  private bookAdded = false

  addItem(item: string): void {
    this.items.push(item)
    if (item === 'Book') {
      this.bookAdded = true
    }
  }

  getDiscountPercentage(): number {
    return this.bookAdded ? 5 : 0
  }

  getItems(): string[] {
    return this.items  // 問題: 内部状態への参照を返している
  }
}

純粋関数による解決

// 純粋関数版
const getDiscountPercentage = (items: readonly string[]): number =>
  items.includes('Book') ? 5 : 0

const calculateFinalPrice = (price: number, items: readonly string[]): number => {
  const discountPercent = getDiscountPercentage(items)
  const discount = (price * discountPercent) / 100
  return price - discount
}

uml diagram

2.4 チップ計算の例

ソースファイル: app/typescript/src/ch02_pure_functions.ts

const getTipPercentage = (names: readonly string[]): number => {
  const size = names.length
  if (size > 5) return 20
  if (size > 0) return 10
  return 0
}

この関数は: - 6人以上のグループ → 20% のチップ - 1-5人のグループ → 10% のチップ - 0人(空リスト) → 0% のチップ

2.5 fp-ts を使ったリスト操作

fp-ts の ReadonlyArray モジュールを使って、型安全なリスト操作ができます。

import { pipe } from 'fp-ts/function'
import * as RA from 'fp-ts/ReadonlyArray'

// 全ての要素を2倍にする
const doubleAll = (numbers: readonly number[]): readonly number[] =>
  pipe(
    numbers,
    RA.map((n) => n * 2)
  )

// 正の数のみをフィルタリング
const filterPositive = (numbers: readonly number[]): readonly number[] =>
  pipe(
    numbers,
    RA.filter((n) => n > 0)
  )

// リストの合計
const sumList = (numbers: readonly number[]): number =>
  pipe(
    numbers,
    RA.reduce(0, (acc, n) => acc + n)
  )

2.6 Option を使った安全な平均計算

空リストの場合に null/undefined を返す代わりに、Option 型を使います。

import * as O from 'fp-ts/Option'

const average = (numbers: readonly number[]): O.Option<number> =>
  pipe(
    numbers,
    O.fromPredicate((arr) => arr.length > 0),
    O.map((arr) => sumList(arr) / arr.length)
  )

// 使用例
const result1 = average([1, 2, 3, 4, 5])  // some(3)
const result2 = average([])               // none

2.7 参照透過性

純粋関数は参照透過性(Referential Transparency)を持ちます。

式をその評価結果で置き換えても、プログラムの意味が変わらないこと

// 参照透過性の例
const score1 = wordScore('Scala')
const score2 = wordScore('Scala')
// score1 と score2 は常に同じ値(5)

// 以下の2つは同等
const total1 = wordScore('Scala') + wordScore('Java')
const total2 = 5 + 4  // wordScore の結果で置き換え可能

uml diagram

2.8 高階関数の基本

関数を引数として受け取ったり、関数を返す関数を高階関数といいます。

// 関数を2回適用する
const applyTwice = (f: (x: number) => number, x: number): number => f(f(x))

// 使用例
applyTwice((x) => x + 1, 5)  // => 7
applyTwice((x) => x * 2, 3)  // => 12

// 関数合成
const compose = <A, B, C>(f: (b: B) => C, g: (a: A) => B): ((a: A) => C) =>
  (a) => f(g(a))

// fp-ts の flow を使った関数合成
import { flow } from 'fp-ts/function'

const double = (x: number) => x * 2
const addOne = (x: number) => x + 1
const doubleThenAdd = flow(double, addOne)

doubleThenAdd(5)  // => 11

まとめ

Part I で学んだこと

uml diagram

キーポイント

  1. 関数型プログラミングは「何をするか」を宣言的に記述する
  2. 純粋関数は同じ入力に対して常に同じ出力を返す
  3. 副作用を避けることでコードの予測可能性が向上する
  4. pipeを使って関数を読みやすく合成できる
  5. readonlyを使ってイミュータブルなデータを表現する
  6. Optionを使って null/undefined を型安全に扱う

Scala vs TypeScript fp-ts 比較

Scala TypeScript (fp-ts) 説明
def f(x: Int): Int const f = (x: number): number => 関数定義
List(1, 2, 3) [1, 2, 3] as const イミュータブルリスト
list.map(f) pipe(list, RA.map(f)) map 操作
list.filter(p) pipe(list, RA.filter(p)) filter 操作
list.foldLeft(z)(f) pipe(list, RA.reduce(z, f)) fold/reduce
f andThen g flow(f, g) 関数合成(左から右)
f compose g compose(f, g) 関数合成(右から左)

次のステップ

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

  • イミュータブルなデータ操作(ReadonlyArray)
  • 高階関数(関数を値として扱う)
  • chain(flatMap)とネスト構造の平坦化
  • sequenceT による複数の効果の合成

演習問題

問題 1: 純粋関数の識別

以下の関数のうち、純粋関数はどれですか?

// A
const double = (x: number): number => x * 2

// B
let counter = 0
const incrementCounter = (): number => {
  counter += 1
  return counter
}

// C
const greet = (name: string): string => `Hello, ${name}!`

// D
const currentTime = (): number => Date.now()
解答 **A と C は純粋関数**です。 - A: 同じ入力に対して常に同じ出力を返し、副作用がない - B: 外部変数 `counter` を変更する副作用がある(不純) - C: 同じ入力に対して常に同じ出力を返し、副作用がない - D: 呼び出すたびに異なる値を返す(不純)

問題 2: pipe を使った関数合成

以下の関数を pipe を使って書き換えてください。

const processString = (s: string): number => {
  const upper = s.toUpperCase()
  const reversed = upper.split('').reverse().join('')
  return reversed.length
}
解答
import { pipe } from 'fp-ts/function'

const toUpper = (s: string): string => s.toUpperCase()
const reverse = (s: string): string => s.split('').reverse().join('')
const length = (s: string): number => s.length

const processString = (s: string): number =>
  pipe(
    s,
    toUpper,
    reverse,
    length
  )

問題 3: fp-ts を使ったリスト操作

以下の関数を fp-ts の ReadonlyArray モジュールを使って実装してください。

  1. リストの全ての要素を3倍にする関数
  2. 10より大きい数のみをフィルタリングする関数
  3. リストの積(全要素の掛け算)を計算する関数
解答
import { pipe } from 'fp-ts/function'
import * as RA from 'fp-ts/ReadonlyArray'

// 1. 全ての要素を3倍にする
const tripleAll = (numbers: readonly number[]): readonly number[] =>
  pipe(
    numbers,
    RA.map((n) => n * 3)
  )

// 2. 10より大きい数のみをフィルタリング
const filterGreaterThanTen = (numbers: readonly number[]): readonly number[] =>
  pipe(
    numbers,
    RA.filter((n) => n > 10)
  )

// 3. リストの積を計算
const product = (numbers: readonly number[]): number =>
  pipe(
    numbers,
    RA.reduce(1, (acc, n) => acc * n)
  )