Part I: 関数型プログラミングの基礎¶
本章では、関数型プログラミング(FP)の基本概念を Python で学びます。命令型プログラミングとの違いを理解し、純粋関数の利点を実感することが目標です。
第1章: 関数型プログラミング入門¶
1.1 命令型 vs 関数型¶
プログラミングには大きく分けて2つのパラダイムがあります。
命令型プログラミングは「どうやるか(HOW)」を記述します。
# 命令型でワードスコアを計算
def calculate_score_imperative(word: str) -> int:
score = 0
for _ in word:
score += 1
return score
関数型プログラミングは「何をするか(WHAT)」を記述します。
# 関数型でワードスコアを計算
def word_score(word: str) -> int:
return len(word)
1.2 Python の基本構文¶
Python での関数定義の基本形を見てみましょう。
ソースファイル: app/python/src/grokking_fp/ch01_intro.py
def increment(x: int) -> int:
"""値を1増加させる。"""
return x + 1
def get_first_character(s: str) -> str:
"""文字列の最初の文字を取得する。"""
return s[0]
def word_score(word: str) -> int:
"""単語のスコア(文字数)を返す。"""
return len(word)
1.3 関数の構造¶
def increment(x: int) -> int:
return x + 1
│ │ │ │ └── 関数本体(式)
│ │ │ └────── 戻り値の型
│ │ └────────────── パラメータと型
│ └──────────────────────── 関数名
└───────────────────────────── キーワード
Python では型ヒント(Type Hints)を使って、パラメータと戻り値の型を明示できます。
1.4 学習ポイント¶
| 概念 | 命令型 | 関数型 |
|---|---|---|
| 焦点 | 手順(How) | 結果(What) |
| 状態 | 変更する | 変換する |
| ループ | for/while | map/filter/reduce |
| データ | ミュータブル | イミュータブル |
第2章: 純粋関数とテスト¶
2.1 純粋関数とは¶
純粋関数(Pure Function)は以下の特徴を持つ関数です:
- 同じ入力には常に同じ出力を返す
- 副作用がない(外部状態を変更しない)
2.2 純粋関数の例¶
ソースファイル: app/python/src/grokking_fp/ch02_pure_functions.py
# 純粋関数の例
def add(a: int, b: int) -> int:
"""2つの数値を加算する純粋関数。"""
return a + b
def string_length(s: str) -> int:
"""文字列の長さを返す純粋関数。"""
return len(s)
def bonus_score(word: str) -> int:
"""文字 'c' を含む場合にボーナスを付与する。"""
base = len(word)
return base + 5 if "c" in word.lower() else base
純粋ではない関数の例:
import random
# 不純な関数 - random.random() は毎回異なる値を返す
def random_part(x: float) -> float:
return x * random.random()
# 不純な関数 - 外部状態を変更する
counter = 0
def increment_counter() -> int:
global counter
counter += 1
return counter
2.3 ショッピングカートの例¶
状態を持つクラスの問題点と、純粋関数による解決を見てみましょう。
問題のあるコード¶
class ShoppingCartBad:
def __init__(self):
self._items: list[str] = []
self._book_added = False
def add_item(self, item: str) -> None:
self._items.append(item)
if item == "Book":
self._book_added = True
def get_discount_percentage(self) -> int:
return 5 if self._book_added else 0
def get_items(self) -> list[str]:
return self._items # 問題: 内部状態への参照を返している
このコードの問題:
1. get_items() が内部リストへの参照を返すため、外部から変更可能
2. _book_added フラグと実際のアイテムの整合性が崩れる可能性がある
純粋関数による解決¶
def get_discount_percentage(items: list[str]) -> int:
"""アイテムリストから割引率を計算する純粋関数。"""
return 5 if "Book" in items else 0
# 使用例
items = ["Apple", "Book"]
discount = get_discount_percentage(items) # 5
この純粋関数版の利点: - 状態を持たない - 同じ入力には常に同じ出力 - テストが容易
2.4 チップ計算の例¶
def get_tip_percentage(names: list[str]) -> int:
"""グループの人数からチップ率を計算する純粋関数。"""
size = len(names)
if size > 5:
return 20
elif size > 0:
return 10
else:
return 0
この関数は: - 6人以上のグループ → 20% のチップ - 1-5人のグループ → 10% のチップ - 0人(空リスト) → 0% のチップ
2.5 純粋関数のテスト¶
純粋関数は非常にテストしやすいです。
def increment(x: int) -> int:
return x + 1
# テストケース
assert increment(6) == 7
assert increment(0) == 1
assert increment(-6) == -5
pytest を使ったテスト例:
class TestIncrement:
def test_positive_number(self) -> None:
assert increment(5) == 6
def test_zero(self) -> None:
assert increment(0) == 1
def test_negative_number(self) -> None:
assert increment(-1) == 0
2.6 文字 'a' を除外するワードスコア¶
より複雑な例を見てみましょう。
def word_score_no_a(word: str) -> int:
"""文字 'a' を除外してワードスコアを計算する。"""
return len(word.replace("a", "").replace("A", ""))
# テスト
assert word_score_no_a("Scala") == 3 # "Scl" → 3文字
assert word_score_no_a("function") == 8 # 'a' なし → 8文字
assert word_score_no_a("") == 0 # 空文字 → 0文字
2.7 参照透過性¶
純粋関数は参照透過性(Referential Transparency)を持ちます。
式をその評価結果で置き換えても、プログラムの意味が変わらないこと
# 参照透過性の例
score1 = word_score("Scala")
score2 = word_score("Scala")
# score1 と score2 は常に同じ値(5)
# 以下の2つは同等
total1 = word_score("Scala") + word_score("Java")
total2 = 5 + 4 # word_score の結果で置き換え可能
2.8 高階関数の基本¶
Python は関数を値として扱えます(第一級関数)。
from typing import Callable
def apply_twice(f: Callable[[int], int], x: int) -> int:
"""関数を2回適用する。"""
return f(f(x))
# 使用例
result = apply_twice(lambda x: x + 1, 5) # 7
def compose(
f: Callable[[int], int],
g: Callable[[int], int]
) -> Callable[[int], int]:
"""2つの関数を合成する。compose(f, g)(x) = f(g(x))"""
return lambda x: f(g(x))
# 使用例
double = lambda x: x * 2
add_one = lambda x: x + 1
double_then_add = compose(add_one, double)
print(double_then_add(5)) # 11: (5 * 2) + 1
2.9 型ヒントとイミュータビリティ¶
Python では Final を使って定数を宣言できます。
from typing import Final
MAX_SCORE: Final[int] = 100
MIN_SCORE: Final[int] = 0
def clamp(value: int, min_val: int, max_val: int) -> int:
"""値を指定された範囲内に収める。"""
return max(min_val, min(value, max_val))
def get_bounded_score(score: int) -> int:
"""スコアを有効な範囲内に収める。"""
return clamp(score, MIN_SCORE, MAX_SCORE)
まとめ¶
Part I で学んだこと¶
- 関数型プログラミングは「何をするか」を宣言的に記述する
- 純粋関数は同じ入力に対して常に同じ出力を返す
- 副作用を避けることでコードの予測可能性が向上する
- 純粋関数はテストが非常に簡単
- 参照透過性により、コードの理解と推論が容易になる
- 型ヒントを使うことで、コードの意図が明確になる
Scala との対比¶
| 概念 | Scala | Python |
|---|---|---|
| 関数定義 | def f(x: Int): Int = x + 1 |
def f(x: int) -> int: return x + 1 |
| 型宣言 | 必須(推論可) | オプション(型ヒント) |
| イミュータブル変数 | val |
Final(慣習) |
| ラムダ | x => x + 1 |
lambda x: x + 1 |
| 文字列補間 | s"Hello, $name" |
f"Hello, {name}" |
次のステップ¶
Part II では、以下のトピックを学びます:
- イミュータブルなデータ操作(tuple、frozenset)
- イテレータと高階関数(map、filter、reduce)
- ジェネレータと itertools
演習問題¶
問題 1: 純粋関数の識別¶
以下の関数のうち、純粋関数はどれですか?
# A
def double(x: int) -> int:
return x * 2
# B
counter = 0
def increment_counter() -> int:
global counter
counter += 1
return counter
# C
def greet(name: str) -> str:
return f"Hello, {name}!"
# D
import time
def current_time() -> float:
return time.time()
解答
**A と C は純粋関数**です。 - A: 同じ入力に対して常に同じ出力を返し、副作用がない - B: 外部変数 `counter` を変更する副作用がある(不純) - C: 同じ入力に対して常に同じ出力を返し、副作用がない - D: 呼び出すたびに異なる値を返す(不純)問題 2: 純粋関数への書き換え¶
以下の不純な関数を純粋関数に書き換えてください。
class Counter:
def __init__(self):
self.value = 0
def increment(self) -> int:
self.value += 1
return self.value
解答
def increment(value: int) -> int:
return value + 1
# 使用例
v1 = 0
v2 = increment(v1) # 1
v3 = increment(v2) # 2
問題 3: テストを書く¶
以下の関数に対する pytest テストケースを考えてください。
def is_even(n: int) -> bool:
return n % 2 == 0
解答
class TestIsEven:
def test_even_positive(self) -> None:
assert is_even(0) is True
assert is_even(2) is True
assert is_even(4) is True
def test_odd_positive(self) -> None:
assert is_even(1) is False
assert is_even(3) is False
def test_negative_even(self) -> None:
assert is_even(-2) is True
def test_negative_odd(self) -> None:
assert is_even(-3) is False