0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Pythonで関数型プログラミングを実現するために利用できるライブラリについて

Posted at

概要

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を利用するのが一番良い。
モナドを利用したい場合は PyMonadreturns を利用することになるが、
PyMonadは最終リリースが4年前なので、returns を利用する方が良さそうである。

returnsに関しては調査していない範囲でも色々と機能がありそうなので、時間がアレば深堀りしたい。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?