Python

Pythonを用いた関数型プログラミング

Pythonを用いた関数型プログラミング

Pythonはマルチパラダイム言語のため、関数型スタイルのプログラミングにも対応しています。関数型プログラミング概要を紹介するとともにPythonで関数型スタイルのプログラミングをする方法について解説します。

関数型プログラミングとは

関数型プログラミングとは、数学的な関数の概念に基づき、副作用を避け、関数の合成や高階関数を中心としてプログラミングをするスタイルのことです。

プログラミング言語には、手続き型言語、オブジェクト指向言語、関数型言語といったものがあります。関数型プログラミング言語は、関数型プログラミングに特化した言語で、代表的な言語にHaskell、Scala、F#、Lispなどがあります。

Pythonはマルチパラダイム言語のため、手続き型やオブジェクト指向のプログラミングだけでなく、関数型スタイルのプログラミングにも対応しています。

本記事では、まず関数型プログラミングや関数型プログラミング言語全般の特徴を紹介し、Pythonにおける関数型プログラミングの方法について紹介しようと思います。Pythonでの関数型プログラミングの方法にのみ興味がある方は、関数型プログラミングの説明部分は読み飛ばしてもらっても構いません。

関数とは

関数型プログラミング言語に限らず、ほとんどのプログラミングでは関数が出てきます。では、関数型プログラミングでいうところの「関数」とはいったい何でしょうか。関数型プログラミングを勉強する際にはこの部分をしっかりと理解しておく必要があります。

関数型プログラミングでいうところの「関数」は、以下の特性を持つ数学的な概念を指します。

  1. すべての関数は引数を受け取らなければならない
  2. すべての関数は値を返却しなければならない
  3. 関数が同じ引数で呼び出されたときは常に同じ結果にならなければならない

上記は、与えられた入力のみから出力が決まるということを意味しています。3つ目の特徴は、参照透過性として有名です。

Pythonをはじめ、多くのプログラミング言語では、引数を受け取らない、または返却値を返さない関数を作ることができますが、これは数学的な意味での関数とは厳密には異なります。

関数型プログラミングの特徴

関数型プログラミングでは、プログラムは「関数」であるという見方をし、プログラムは関数合成(function composition)したものと捉えます。

関数合成とは、ある関数$f$と関数$g$があった時に、入力$x$に対して$(f \circ g)=f(g(x))$とできることを言います。$g$の値域(出力の取り得る値全体からなる集合)が、$f$の定義域(入力の取り得るの値全体からなる集合)におさまっていれば関数は合成できることになります。

関数型プログラミングでは、小さな関数から関数を合成していくことで大きなプログラムを実現していくことになるわけですが、以下のようないくつかの特徴があります。

  • プログラムを手続きではなく関数合成で表現する
  • 関数を変数として扱うことができる
  • 関数は入力値と返却値を持ち、返却値は入力にのみ依存する(参照透過性)
  • 関数の副作用を許可しない
  • 形式手法と定理証明でプログラムの数学的な証明ができる

以降では関数型プログラミングの上記の特徴について説明してみたいと思います。

プログラムを手続きではなく関数合成で表現する

関数型プログラミングがC言語などの手続き型言語と異なるのは、手続きを列挙するのではなく関数を順に適用した関数合成で宣言的に結果を実現する点です。

例えば、リストの中の数値をそれぞれ2乗して、すべてを足し合わせる問題を考えたとしましょう。Pythonで、手続き的にプログラミングする場合と、関数の合成でプログラミングする場合の例を以下に示します。

【手続きで書く場合】初期化、ループ、状態変更を用いる

target = [1, 2, 3, 4, 5]

# 手続きで書く場合
sum_value = 0
for d in target:
    sum_value += d**2
print(sum_value)
【実行結果】
55

【関数で書く場合】高階関数を用いてデータ変換と合成を行う

from functools import reduce


def add_nums(x, y):
    """値を足し算する"""
    return x + y


def square(x):
    """値を2乗する"""
    return x**2


target = [1, 2, 3, 4, 5]

# 関数で書く場合
sum_value = reduce(add_nums, map(square, target))
print(sum_value)
【実行結果】
55

上記のプログラムは、計算結果としてはどちらも55となります。

手続き的にプログラムを記載する場合には、合計用の変数sum_value0で初期化し、for文で、targetというリストの中の要素を順に変数dに取込み、dを2乗してsum_valueに足しこんでいくという書き方をしています。処理の過程で変数dsum_valueの値が変化していくのが特徴です。

一方で、関数で記載する場合の例では、値を足し算するadd_nums関数と値を2乗するsquare関数を用意しています。関数型プログラミングでは「targetの各要素をsquareで2乗し、結果をadd_numsで足し込む」を関数合成の要領で「reduce(add_nums, map(square, target))」という1行で記載しています。

関数合成

mapはリストの各要素に関数を適用するための関数で、reduceはリストの要素を使って関数で畳み込むための関数ですが、詳細は後に紹介します。

関数型の書き方では、状態を変化させるようなコードがないことが特徴的です。関数型プログラミングでは、入力に対する出力の性質を宣言的に書くことで、コードの見通しが良くなるという特徴があります。

関数を変数として扱うことができる

関数型プログラミングでは、値を変数に入れることと同じようにして関数を変数に入れて扱うことができます。変数の中身は値の場合もありますし、関数自体の場合もあります。

また、関数の引数として関数を渡したり、関数の返却値として関数を返すといったこともできます。このことは、高階関数やデコレータといった機能に関連します。

関数は入力値と返却値を持ち、返却値は入力にのみ依存する(参照透過性)

関数型プログラミングにおける関数は、数学的な意味での関数なので、引数として入力値と返却値を持ち、入力値にのみに依存します。このように入力値のみから返却値が決まる特徴を参照透過性と言い、関数型プログラミングで重要なものとなっています。

以下で参照透過である関数とそうでない関数を見てみましょう。

【参照透過である関数】

def square(x):
    """値を2乗する"""
    return x**2


if __name__ == "__main__":
    print(square(2))
    print(square(2))
    print(square(3))
    print(square(3))
【実行結果】
4
4
9
9

【参照透過ではない関数】

global_value = 0


def func():
    global global_value
    global_value += 1

    return global_value


if __name__ == "__main__":
    print(func())
    print(func())
    print(func())
【実行結果】
1
2
3

square関数は、引数として受け取ったxの値を2乗して返却する関数で、x=2であれば4ですし、x=3であれば9と常に同じ結果を返します。このような関数は参照透過であると言います。

一方で、参照透過ではないfunc関数は呼び出すたびに返却値の値が異なっています。このように呼び出しのたびに結果が変わるようなものは、数学的な意味での関数とは言えません。

関数の副作用を許可しない

プログラミングにおいて副作用というのは非常に重要な概念です。副作用とは、処理が外部の状態や環境に影響を与えるような変更を指します。変数の変更、ファイルの入出力、ネットワーク通信などは、代表的な副作用の例と言えます。例外送出も状態を変更することから一種の副作用と言えます。

関数型プログラミング言語では、こういった副作用を許可しないような仕組みが多く取り込まれています。関数型プログラミング言語では、変数に値を設定すると値を変更できないか、もしくは非常に限定されています。このような性質は値の不変性と言いますが、この特徴は副作用を避けるためのものと言えます。

関数型プログラミングでは、プログラミングの範囲を以下のように「副作用のないもの」「副作用のあるもの」で区別して考えることが重要です。副作用を持つような手続きは、数学的な意味での関数とは言えません。

どのように副作用のあるものを扱うかというと、例えば、純粋関数型言語のHaskellではモナドという仕組みを使って副作用を扱います。詳細は割愛しますが、ここでは関数型プログラミング言語では、副作用を許さないような仕組みで言語が成り立っているということを理解してもらえればと思います。

形式手法と定理証明でプログラムの数学的な証明ができる

関数型プログラミング言語に関連している事項として形式手法定理証明というものがあります。

形式手法とは、プログラムやシステムを形式的なモデルや仕様で記述する手法で、抽象的なレベルで設計や検証を行うことで問題やバグを事前に特定できるような技術です。特に鉄道分野、航空分野、金融・セキュリティ分野など、システムの不具合が人命や人の生活に多大に影響を及ぼすような分野で使用されています。

一方、定理証明とは、形式手法で作成されたモデルや仕様を元に証明を行うための技術です。定理証明を使用してプログラムの性質を確認することにより、形式手法で設計されたモデルや仕様の妥当性が検証され、プログラムが正しい振る舞いを持つことをテストを行わずに示すことができます。ただし、テストが不要という極端なことを言っているわけではありませんので注意してください。

一般に定理証明は簡単ではなく、十分に行えるプログラマも多くはないと言われています。私もこの領域のレベルの技術領域については知っているだけで、十分な理解は得られていません。数学に基づく言語であるからこそ、こういったことに対応できるというのが関数型プログラミング言語の大きな特徴と言えます。興味がある方は勉強してみるといいでしょう。

Pythonで関数型プログラミング

Pythonはマルチパラダイム言語であるため、関数型プログラミングスタイルを使うことができます。

関数型プログラミングには、プログラムの見通しが良くなるなどの特徴があり、紹介してきたような特徴を活かすため、Pythonでも関数型プログラミングを意識したコーディングができるようになることは、プログラムの質を向上させるための役に立つと思います。

ただし、Pythonが厳密な意味で関数型プログラミングの制約を課すわけではないことに注意が必要です。Pythonで関数型プログラミングをしようとすると、例えば関数としての制約を満たせているか、関数の定義域と値域の関係性が問題ないかなどをプログラマが考慮する必要があります。

一方で、Haskellなどの関数型プログラミング言語では、言語としての学習コストが高いものの、関数型プログラミングとしてのチェックを言語がしてくれるためプログラマがその点を考える必要はありません。そのため、本当の意味で関数型プログラミングをしたい場合は、Haskellなどの関数型プログラミング言語を採用するのが適切でしょう。

Pythonによる関数型プログラミング

以降では、Pythonで関数型スタイルでプログラミングする方法を説明します。独立したページで詳しく整理しているため、関連ページへのリンクを中心に紹介します。

ラムダ関数(無名関数)

関数型プログラミングとラムダ関数は、非常に密接な関係性を持つ概念です。関数中心のプログラミングスタイルである関数型プログラミングにおいてラムダ関数は核となる概念ともいえます。なお、ラムダ関数は、関数名を持たないことから無名関数とも呼ばれます。

ラムダ関数は、関数を直接表現する手段として関数型プログラミングにおいて非常に重要です。ラムダ関数を変数に代入して使用したり、関数を引数に取る高階関数の引数として渡して使うことができます。

Pythonにおけるラムダ関数は「ラムダ(lambda)関数:無名関数の使い方」を参考にしてください。

高階関数の使用

高階関数とは、関数自体を引数/返却値として扱う関数を言います。関数型プログラミングの領域でよく使われる高階関数は、Python組み込み関数やfunctoolsモジュールで実装がされています。以降では、それらの関数について紹介します。

map関数

map関数は、関数と処理対象のイテラブルなオブジェクト(リスト等)を受け取って、イテラブルなオブジェクトの各要素に関数を適用した結果を返却するPythonの組み込み関数です。map関数は関数型プログラミング言語で特徴的で重要な関数です。

map関数の詳細な使い方は「map関数の使い方の基本 ~リスト要素への関数適用~」を参考にしてください。

filter関数

filter関数は、関数と処理対象のイテラブルなオブジェクト(リスト等)を受け取って、特定の条件を満たす要素を抽出するPythonの組み込み関数です。filter関数は、関数型プログラミング言語でも特徴的で重要な関数です。

filter関数の詳細な使い方は「filter関数の使い方の基本 ~リストから条件を満たす要素を抽出~」を参考にしてください。

reduce関数

reduce関数は、関数と処理対象のイテラブルなオブジェクト(リスト等)を受け取って、リストの要素を指定した関数を使って畳み込む関数です。

もともとreduce関数は、Python2でmap関数やfilter関数同様にPythonの組み込み関数でしたが、Python3からはfunctoolsモジュールに移動しています。

reduce関数の詳細な使い方は「reduce関数の使い方の基本 ~リスト要素の畳み込み~」を参考にしてください。

partial関数

関数の部分適用というのは、数学やプログラミングにおいて重要な概念です。部分適用は、ある関数に対して一部の引数を固定して新しい関数を作る手続きのことを言います。

partial関数は、関数と固定する引数の値を受け取ることで関数の部分適用をすることができる関数です。partial関数は、functoolsモジュールの中に含まれています。

partial関数の詳細な使い方は「partial関数の使い方の基本 ~関数の部分適用~」を参考にしてください。

デコレータ

Pythonのデコレータは、関数型プログラミングのアイデアを利用して関数を変更したり、拡張したりするための仕組みです。「@関数名」として使うのがデコレータの表記となっています。ここでの関数は、引数に関数を受け取って関数を返却するような高階関数です。

デコレータの使い方はデコレータ(decorator)の基本的な使い方を参考にしてください。

ジェネレータ

ジェネレータは、プログラム内で逐次的に値を生成するための仕組みです。値を一つずつ取得することができ、途中で停止したり再開したりすることが可能なため、無限のデータ列を表現できます。関数型プログラミングでもジェネレータが利用される点で関連があります。

Pythonでジェネレータを使う方法は「ジェネレータ(generator)関数 ~yieldによる返却~」を参考にしてください。また、ジェネレータは内包表記で表現することも可能です。ジェネレータ内包表記の使い方は「ジェネレータ(generator)内包表記の使い方」を参考にしてください。

型ヒント(型アノテーション)

関数型プログラミングをする場合には型の情報というのは重要なものです。関数型プログラミング言語には、型検査や型推論といったものがあることからも重要性が分かります。

Pythonは動的型付け言語であるため、型の指定は必要はありません。そのため、型が適切に使われているかといった判断をすることは難しいです。

しかし、Pythonではバージョン3.5から型ヒントがサポートされており、変数や関数に型アノテーションとして型情報を指定できるようになっています。また、この型ヒントを元にした型チェックを行うmypyというサードパーティ製のライブラリも存在します。これらをうまく使うことで関数型プログラミング言語のように型チェックを行うことが可能です。

型ヒントについては「型ヒント(変数・関数の型アノテーション)の基本」を参考にしてください。また、mypyについても「mypyを使用した型チェックの方法」にまとめていますので興味があれば参考にしてください。

まとめ

Pythonはマルチパラダイム言語のため、関数型スタイルのプログラミングにも対応しています。本記事では、関数型プログラミング概要を紹介するとともにPythonで関数型スタイルのプログラミングをする方法について解説しました。

関数型プログラミングは、プログラムの見通しが良くなるなどのメリットがあるため、Pythonでも関数型プログラミングを意識したコーディングをすることで、プログラムの質が向上する可能性があります。

ただし、Pythonが厳密な意味で関数型プログラミングの制約を全面的に課すことができるわけではないことに注意が必要です。本当の意味で関数型プログラミングを追求したい場合は、Haskellなどの関数型プログラミング言語を採用を検討することをおすすめします。