概要
Pythonで関数型プログラミングを実現するためのライブラリがいくつか存在するので、実際に使ってみた際のメモ
ライブラリ一覧
ライブラリ | バージョン | 最終更新日 |
---|---|---|
cytoolz | 1.0.1 | 2024/12/13 |
PyMonad | 2.4.0 | 2021/5/15 |
returns | 0.25.0 | 2025/5/22 |
cytoolz
関数型プログラミングを行うためのAPIを提供するライブラリ。
cytoolz
をインストールすると以下の2つのライブラリがインストールされる。
- toolz
- cytoolz
両者の違いは、toolz
がPython実装で、cytoolz
がCython実装という点だけで、インターフェースについては同一。
※ CythonはPythonコードをC/C++にコンパイルして、C/C++として動作させる技術。基本的にはCythonの方が速いので、cytoolz
を利用したほうが速くなるハズ。
cytoolz
専用のドキュメントは存在しないので、以下のtoolz
のドキュメントを参照する。
以下のような機能が提供されている
- カリー化
- ストリーム処理(ストリームによる関数チェーン)
カリー化
あまりおもしろくない例であるが、add(x, y)
のカリー化の例。
カリー化については他の解説記事を参照してほしいが、簡単にいうと関数の部分適用を可能にするというもの。
以下のケースだとadd(2)
という呼び出しが可能になり、「2を足す関数」として再定義される感じである。
from cytoolz import curry
@curry
def add(x, y):
return x + y
print(add(2)(2)) # 4
ストリーム処理
pipe
を使って、ストリーム的に関数チェーンを実現できる。
以下のように、一々中間変数に値を格納することなく、平均値を求めることができる。
from cytoolz import pipe, groupby, valmap
# 学生データの処理
students = [
{'name': 'Alice', 'grade': 'A', 'score': 95, 'subject': 'Math'},
{'name': 'Bob', 'grade': 'B', 'score': 87, 'subject': 'Math'},
{'name': 'Charlie', 'grade': 'A', 'score': 92, 'subject': 'Science'},
{'name': 'Diana', 'grade': 'C', 'score': 78, 'subject': 'Math'},
{'name': 'Eve', 'grade': 'A', 'score': 96, 'subject': 'Science'},
]
subject_averages = pipe(
# 元データ
students,
# subject でグルーピング
lambda z: groupby(lambda x: x['subject'], z),
# グルーピングしたリストの集計(平均値の算出)
lambda z: valmap(lambda group: sum(
s['score'] for s in group) / len(group), z)
)
print(subject_averages) # {'Math': 86.67, 'Science': 94.0}
注意点として、上記のようにfrom cytoolz
からgroupby
などをインポートすると、カリー化されていないので、pipe
の引数に指定する際には、ラムダ式化が必須となる。
一々ラムダ式化するのは面倒なので、以下のように from cytools.curried
からインポートすると、カリー化されているので見た目もスッキリする。
from cytoolz.curried import pipe, groupby, valmap
# 学生データの処理
students = [
{'name': 'Alice', 'grade': 'A', 'score': 95, 'subject': 'Math'},
{'name': 'Bob', 'grade': 'B', 'score': 87, 'subject': 'Math'},
{'name': 'Charlie', 'grade': 'A', 'score': 92, 'subject': 'Science'},
{'name': 'Diana', 'grade': 'C', 'score': 78, 'subject': 'Math'},
{'name': 'Eve', 'grade': 'A', 'score': 96, 'subject': 'Science'},
]
subject_averages = pipe(
students,
# カリー化されているので、ラムダ式化する必要なし
groupby(lambda x: x['subject']),
valmap(lambda group: sum(
s['score'] for s in group) / len(group)),
)
print(subject_averages) # {'Math': 86.67, 'Science': 94.0}
その他、色々なAPIが提供されているが詳細は以下のリンクを参照して下さい。
PyMonad
Haskellライクにモナドが利用できるライブラリ。
※ メンテナンスが4年程されていない状況なので、新規で利用する場合は、後述のreturns
を利用すると良い。
以下の機能などを提供している
- カリー化
- モナド
- メソッドチェーン(モナド)
カリー化
先ほどと同様の例であるが、PyMonad
の場合はcurry
に引数の数を渡す必要がある点には注意。
from pymonad.tools import curry
@curry(2)
def add(x: int, y: int) -> int:
return x + y
print(add(2)(2)) # 4
モナド
Maybe
モナドやEther
モナドなどが利用可能である。
モナド自体の説明は省くが、簡単に言うと例外や未定義値などをうまく扱うための技術である。
以下はMaybe
モナドを使った安全な割り算の例である。
分母が0の場合に例外を発生させずにNothing
を返すようにしている。
from pymonad.maybe import Just, Nothing, Maybe
def safe_divide_maybe(x: float, y: float) -> Maybe[float]:
if y == 0:
return Nothing
return Just(x / y)
print(safe_divide_maybe(10, 2)) # Just(5.0)
print(safe_divide_maybe(10, 0)) # Nothing
例外メッセージを呼び出し元に伝播させたい場合は、Either
モナドを利用して以下のようにできる。
例外やエラーはLeft
、処理に成功した場合はRight
として返す。
from pymonad.either import Left, Right, Either
def safe_divide_either(x: float, y: float) -> Either[str, float]:
if y == 0:
return Left("Division by zero")
return Right(x / y)
print(safe_divide_either(10, 2)) # Right(5.0)
print(safe_divide_either(10, 0)) # Left(Division by zero)
# 呼び出し元で判定するために`is_left`, `is_right`が用意されている。
value = safe_divide_either(10, 2)
if value.is_left():
print(f"Error: {value.value}")
elif value.is_right():
print(f"Result: {value.value}")
メソッドチェーン(モナド)
cytoolz
でいうところのpipe
を実現するためには、モナドによるメソッドチェーンを利用する
from pymonad.maybe import Just
print(Just(10).map(lambda x: x + 2)) # Just(12)
returns
こちらもPyMonadと同様にモナドなどの機能をサポートしているライブラリである。
以下のような機能を提供している。
- カリー化
- モナド
- メソッドチェーン(モナド)
カリー化
こちらはcytoolz
と同様に単に@curry
アノテーションを付与すればOKである。
from returns.curry import curry
@curry
def add(x: int, y: int) -> int:
return x + y
print(add(2)(2)) # 4
モナド
PyMoadと同様にこちらもモナドを提供しているが、より実践的な定義になっているので、微妙に違う。
- Maybeモナド
- Maybe, Nothing, Some
- ※ Justは存在しないので、Someを利用する
- Resultモナド (PyMonadのEitherモナドに相当)
- Result, Success, Failure
Maybeモナドの利用例
def maybe_age(age) -> Maybe[int]:
# シンプルな値はSome
# 問題のある値はNoneではなくNothingを利用する
return Nothing if age < 0 else Some(age)
print(maybe_age(10)) # Some(10)
print(maybe_age(-1)) # Nothing
print(maybe_age(-1).value_or('Invalid age')) # Invalid age
Resultモナドの利用例
結果が成功したかどうかを判定する際には、以下のようにmatch
を利用するか
単純にinstanceof
で型を判定する。
from returns.result import Result, Success, Failure
def divide(x: float, y: float) -> Result[str, float]:
if y == 0:
return Failure("Division by zero")
return Success(x / y)
match divide(10, 2):
case Success(value):
print(f'Result is {value}')
case Failure(error):
print(f'Error: {error}')
メソッドチェーン(モナド)
cytoolz
でいうところのpipe
を実現するために、以下の2つの方法がある。
モナドによるメソッドチェーン
from pymonad.maybe import Just
print(Just(10).map(lambda x: x + 2)) # Just(12)
flow
, pipe
による関数チェーン
from returns.pipeline import flow, pipe
# flow : 1番目の引数に対して順次適用する
print(flow(100, lambda x: x + 1, lambda x: x * 2)) # 202
# pipe : 関数合成
print(pipe(lambda x: x + 1, lambda x: x * 2)(100)) # 202
その他の機能については、以下のドキュメントを参照
まとめ
モナドまで利用する必要が無い場合は cytoolz
を利用するのが一番良い。
モナドを利用したい場合は PyMonad
かreturns
を利用することになるが、
PyMonad
は最終リリースが4年前なので、returns
を利用する方が良さそうである。
returns
に関しては調査していない範囲でも色々と機能がありそうなので、時間がアレば深堀りしたい。