はじめに
この記事はあくまでもさわりだけであり、真に理解することを目的にはしていませんし、関数型を真に理解することなど本を1冊読んでもできるものではありません。
ですが、どのようなものか知ることで、正しい関数型の理解へつなげて行けるはずだと思っています。
そもそも関数型って
関数って何?
そもそも関数って何でしょう。
# print「関数」?
print("Hello, World!")
# setattr「関数」?
o = object()
setattr(o, 'attr', 1)
# format「関数」?
format(2**10, ',')
じつは、この中で関数型の示す関数って1つしかありません。format関数だけです。なぜなら、print関数やsetattr関数には副作用があるからです。
「副作用」ってなんでしょう?例えば、数学的な関数
$$
f(x) = x^2 + 2
$$
を考えてください。$f(2)=6$です。この、$2\rightarrow6$という関係性を表すのが関数$f(x)$ということなんです。ところで、この関数を何回評価しても$2\rightarrow6$という結果は変わりません。
ですが、Pythonのprint関数やsetattr関数は評価するたびに動作が変わります。それか自身の動作は変えなくても、自身がほかの関数の動作を変更する可能性がありますよね?
その動作を書き換えるということこそが副作用の正体です。
o = object()
# -1が出力されるはず
print(getattr(o, 'val', -1))
# o.valに15を設定(副作用)
setattr(o, 'val', 15)
# 15が出力されるはず(結果が変わった)
print(getattr(o, 'val', -1))
関数型のメリットは何?
関数型のメリットって何でしょうか。次のようなものが考えられます。
- 副作用がない
- 文脈依存的に関数の動作が変わらない
- 設計段階で関数の引数や戻り値について検証できる
- 部分テストの結果がほぼ結合テストの結果になる
- ほかの関数への依存がない
- 関数を評価した環境の状態をほぼ考慮する必要がない
- フローを関数内で完結できる
- 小さなコードの集積にすることでコード管理の労力が減る
このように、関数型にはほかのパラダイムにはない大きなメリットがあります。一方で、デメリットも大きいです。
- 高い学習コスト
- ほかのパラダイムとはまるで違う基礎理念
- 数学的な論理への高い依存度
- ほかのパラダイムの資産を活用しにくい
- 大規模プロジェクトへの応用が難しい
- 関数型を扱える人材が少ない
- 関数型ができない人はまるで役に立たない
- 関数型ができる人がいなくなった時に管理不能なコードが量産される
このように、関数型を盲目的に採用することは非常に危険といえそうです。
とはいえ、このメリット/デメリットの対比は人材依存的なものでしかありません。ぶっちゃけると「優秀なエンジニアは素晴らしいコードを書くが、底辺エンジニアはウンコードを量産する」という話に収束します。
関数型を使ってみる
関数型を全体で使うことは難しくても、部分的な採用なら比較的簡単にできます。
関数に関数を渡してみる
関数に関数を渡すことができます。渡した関数には変換式や条件式など、様々な役割を持たせることができます。
たとえば変換式とは、引数をとって値を返す式のことです:
map(lambda x: x // 2, range(20))
のlambda x: x // 2
が変換式に当たります。値に何らかの処理をしたいとき、変換式を引数にとることは関数の自由度を上げることにつながります。
def getIterator():
c = 1
while 1:
yield c
c += c
def getIteratorWithConverter(converter):
c = 1
while 1:
yield c
c += converter(c)
getIterator
ではyieldするたびにcを2倍する動作しかできません。そこに動作をカスタマイズする余地はないのです。1づつ増やす関数をこの関数を使って実現するのは大変です。
しかし、getIteratorWithConverter
は引数を一つとって値を返す関数を渡すことで様々な動作をする関数に変えることができます。たとえば、
# 1づつ増やす
getIteratorWithConverter(lambda _: 1)
# 5で割った余りを足してゆく
getIteratorWithConverter(lambda x: x%5)
# 2のべき乗を足してゆく
getIteratorWithConverter(lambda x: 2**x)
このように、関数に関数を渡すことで動作を変えることは関数の汎用性を増すことになります。これは、副作用による方法と比べてもよほどスマートだと思います。
なぜなら、副作用と違ってもとの関数の動作を変えてしまっているわけではないですし、渡した関数ごとに画一性のある動作をするからです。
クロージャを使ってみる
さっき言ったことと矛盾するかもしれませんが、(純粋関数型を除けば1)関数型にも副作用はあります。これは人間がわかりやすくしたり、効率を上げたりするためです。
その最たるものの一つにクロージャがあります。
def getcounter():
count = 0
def countUp():
nonlocal count
count += 1
return count
return countUp
counter = getcounter()
counter()
counter()
counter()
counter()
# 出力:
# 1
# 2
# 3
# 4
この例では、counterを呼ぶたびに戻り値が変わっています。実は、クロージャというのはオブジェクトと何も変わらないのです。しかし、オブジェクトと違い、中の変数にアクセスする方法は大きく制限されていますので、予想外の動作というのはオブジェクトより減らすことができるでしょう。
Pythonで関数の部分適用をエミュレーションしてみる
部分適用って何でしょうか。簡単にいうとある関数の引数のうちいくつかを決め打ちにすることで、ある動作に特化した関数に変えることです。
def plus(a, b):
return a + b
def plus5(b):
return 5 + b
この例において、plus関数とplus5関数は似たような動作をします。では、plus6、plus7…と増えていったときに、それらの関数を全部定義しなければいけないのでしょうか?
そんなことはありません。
def plus(a, b):
return a + b
# pythonでは、一部の引数だけ与えての動作はできない
# TypeError!
plus(5)
# じゃあどうする?
# こうすればいいじゃない!
def plus(a):
def _plus(b):
return a + b
return _plus
# 5を足す関数を定義
plus5 = plus(5)
# 6を足す関数も同様に
plus6 = plus(6)
# 9を出力
plus5(4)
# これも9
plus6(3)
このように、関数を返す関数を定義すれば解決できます。このような関数を指して、ファクトリ関数ということもありますね。
実は標準ライブラリのfunctoolsには似たようなことをするための関数があります。
from functools import partial
def plus(a, b):
return a+b
plus5 = partial(plus, 5)
# 11を返す
plus5(6)
このように、関数の一般化と特殊化というのは使いようによっては非常に便利になります。
デコレータを使ってみる
デコレータというのは、Pythonにおける関数やクラスを修飾する機能です。
def map_decorator(func):
def _inner(array):
return list(map(func, array))
return _inner
@map_decorator
def exp(num):
return 2**num
exp([1, 2, 3, 6, 13, 12, 5, 8])
# 出力:
# [2, 4, 8, 64, 8192, 4096, 32, 256]
このように、関数の動作を画一的に変更することができます。デコレータは関数を引数に関数を返すファクトリ関数の一種です。
ほかにも、関数の動作を変えずにロギングしたり様々なデコレータが存在します。
最後に
Pythonは関数型言語ではありませんが、関数型のプログラミングができるということはわかっていただけたかと思います。関数型は万能薬でも何でもないですが、選択肢として持つことは生産性を高めたり、考え方を柔軟にしたりできるはずです。
何も考えず関数型を使うのではなく、よく吟味したうえで関数型を使うのならデメリットよりもはるかに大きなメリットを得ることができる考え方でもあります。プログラミングは結局何を作るのかに収束するのです。
P.S.
関数型に詳しい人がいたら、容赦なく突っ込みお願いします。
-
関数型本来の機能ではないので、当然純粋関数型には存在しません。 ↩