目次


魅力的なPython

Pythonでの関数プログラミング: 第3回

カリー化および他の高階関数

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: 魅力的なPython

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:魅力的なPython

このシリーズの続きに乞うご期待。

式結合

読者の1人、Richard Daviesは、部分的なソリューションに満足することなく、結合を単独の表現式にまで適用すべきかについての問題を提起しました。なぜそうしたくなるだろうかということについて簡単に検討し、comp.lang.python寄稿者によって提供された非常に素晴らしい表現方法もお見せしましょう。

まず、functionalモジュールのBindingsクラスを想起しましょう。そのクラスの属性を使うことによって、特定の名前が、ブロックスコープ内で唯一のものを意味すると保証することが出来ました。

リスト1:ガード付き再結合を含むPython FPセッション
>>> from  functional import  *
>>> let = Bindings()
>>> let.car = lambda  lst: lst[0]
>>> let.car = lambda  lst: lst[2]
Traceback (innermost last):
  File "<stdin>" , line 1, in  ?
  File "d:\tools\functional.py" , line 976, in  __setattr__
    raise  BindingError, "Binding '%s' cannot be modified."  % name
functional.BindingError:  Binding 'car'  cannot be modified.
>>> let.car(range(10))
0

Bindingsクラスは、モジュールまたは関数のdefのスコープにおいて要求を実現しますが、これを1つの式で機能させる方法はありません。しかしMLファミリーの言語では、以下のように1つの式の中で結合を作成することは自然なことです。

リスト2:Haskell式レベルの名前結合
 -- car (x:xs) = x  -- *could* create module-level binding list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
 -- 'where' clause for expression-level binding
 firsts1 = [car x | x <- list_of_list] where car (x:xs) = x
 -- 'let' clause for expression-level binding
 firsts2 = let car (x:xs) = x in [car x | x <- list_of_list]
 -- more idiomatic higher-order 'map' technique
 firsts3 = map car list_of_list where car (x:xs) = x
 -- Result: firsts1 == firsts2 == firsts3 == [1,4,7]

Greg Ewingは、Pythonのリスト内包を使うことによって同じ効果を得ることができると気づきましたが、次のとおりHaskell構文と同じくらい簡潔な方法でそれを実現することもできます。

リスト3:Python 2.0+式レベルの名前結合
>>> list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
>>> [car_x for  x in  list_of_list for  car_x in  (x[0],)]
 [1, 4, 7]

リスト内包において単項タプル内に式を置くというこのやり方では、高階関数を用いて式レベルの結合を使うという方法は利用していません。高階関数を使うには、依然として以下のようにブロック・レベルの結合を使う必要があります。

リスト4:「map( )」を用いたPythonのブロック・レベルの結合
>>> list_of_list = [[1,2,3],[4,5,6],[7,8,9]]
>>> let = Bindings()
>>> let.car = lambda  l: l[0]
>>> map(let.car,list_of_list)
 [1, 4, 7]

これも悪くはありませんが、map( )を使う必要がある場合は、結合の範囲は必要とするよりもわずかに広くなります。しかし、リストが最終的に必要なものでない場合でも、次のようにリスト内包を使って名前結合を実行させることは可能です。

リスト5: Pythonのリスト内包からの「ステップ・ダウン」
# result = func car_car
#          where
#              car (x:xs) = x
#              car_car = car (car list_of_list)
#              func x = x + x^2
 >>> [func for  x in  list_of_list
...       for  car in  (x[0],)
...       for  func in  (car+car**2,)][0]
2

list_of_listの最初の要素の最初の要素で算術計算を実行し、その一方で、(式の範囲内だけですが)算術計算にも名前を付けました。最後のインデックス0を持つ最初の要素だけを選ぶので、「最適化」を考慮して、初めに1つの要素よりも長いリストをわざわざ作成する必要はないと思います。

リスト6:リスト内包からの効率的なステップ・ダウン
>>> [func for x in list_of_list[:1]
...       for car in (x[0],)
...       for func in (car+car**2,)][0]
2

高階関数:カリー化

最も一般的な高階関数のうち、map( )reduce( )およびfilter( )という3つの関数がPythonに組み込まれています。これらの関数の働きは、引数(のいくつか)として他の関数を取ることで、これが「高階」と呼ばれるゆえんでもあります。これらの組み込み関数以外の高階関数は、関数オブジェクトを返します。

Pythonは、関数オブジェクトのステータスが最初のクラスであるおかげで、ユーザーに自作の高階関数を作成する機能を常に提供してきました。単純なケースとしては、以下のようなものがあります。

リスト7:単純なPython関数ファクトリー
>>> def   foo_factory ():
...     def   foo ():
...         print  "Foo function from factory" 
...     return  foo
... 
>>> f = foo_factory()
>>> f()
Foo function from factory

Xoltar Toolkitについてはこの連載の第2回で検討しましたが、これには素晴らしい高階関数群が付属しています。Xoltarのfunctionalモジュールが提供する関数のほとんどは、さまざまな従来の関数型言語で開発された関数であり、その有用性は長年にわたって証明されています。

おそらく最も有名であり最も重要な高階関数はcurry( )です。curry( )は、論理学者のHaskell Curryにちなんで名づけられました。また、そのファースト・ネームは、上述したプログラミング言語の名前としても使われています。「カリー化」の根底にある意味は、(ほぼ)すべての関数をたった1つの引数の部分関数として扱うことができるということです。カリー化を利用するのに必要なものは、関数自体への戻り値が関数であるということだけです。ただし、戻される関数は「限定された」または「完全に近い」ものです。これは、第2回で記述したクロージャーに非常によく似た機能を持っています。つまり、カリー化された戻り関数への各連続呼び出しは、最終的な計算に関係したいっそう多くのデータ(手順に付随したデータ)を「満たします」。

カリー化についての説明は、まずはHaskellで非常に単純な例を取り上げ、次に同じ例をもう1度、functionalモジュールを使って、Pythonで取り上げましょう。

リスト8:Haskell計算のカリー化
computation a b c d = (a + b^2+ c^3 + d^4)
check = 1 + 2^2 + 3^3 + 5^4
fillOne   = computation 1  -- specify "a"
 fillTwo   = fillOne 2      -- specify "b"
 fillThree = fillTwo 3      -- specify "c"
 answer    = fillThree 5    -- specify "d"
-- Result: check == answer == 657

一方、Pythonでは以下のようになります。

リスト9:Python計算のカリー化
>>> from  functional import  curry
>>> computation = lambda  a,b,c,d: (a + b**2 + c**3 + d**4)
>>> computation(1,2,3,5)
657
>>> fillZero  = curry(computation)
>>> fillOne   = fillZero(1)   # specify "a"
 >>> fillTwo   = fillOne(2)    # specify "b"
 >>> fillThree = fillTwo(3)    # specify "c"
 >>> answer    = fillThree(5)  # specify "d"
 >>> answer
657

これ以外に、第2回で使用したのと同じ単純な税金計算プログラムを取り上げて(今度はcurry( )を使って)、クロージャーを用いたプログラムを説明することが可能です。

リスト10:Pythonのカリー化された税金計算
 from  functional import  *
taxcalc = lambda  income,rate,deduct: (income-(deduct))*rate
taxCurry = curry(taxcalc)
taxCurry = taxCurry(50000)
taxCurry = taxCurry(0.30)
taxCurry = taxCurry(10000)
 print  "Curried taxes due =" ,taxCurry
 print  "Curried expression taxes due =" , \
      curry(taxcalc)(50000)(0.30)(10000)

クロージャーとは異なり、特定の順序(左から右へ)で引数をカリー化する必要があります。しかしfunctionalは、もう一方の側で(右から左へ)始まるrcurry( )クラスも含んでいることに注意してください。

あるレベルでは、上記の例の2番目のprintステートメントは、通常のtaxcalc(50000,0.30,10000)の単純呼び出しとは、単に綴りが違うだけです。しかし別のレベルでは、このステートメントによって、あらゆる関数はただ1つの引数の関数となりうるという概念がかなり明確になります。これは初めての人々にとっては、かなり意外な概念と思われます。

さまざまな高階関数

カリー化の「基本的な」操作以外に、functionalはさまざな興味深い高階関数を提供します。その上、実際に独自の高階関数を書くことは、functionalであろうとなかろうと、難しいことではありません。functionalにおける高階関数が興味深い考えを提供することだけは確かです。

ほとんどの場合、高階関数は、標準のmap( )filter( )、およびreduce( )を「強化」したもののように感じられます。これらの関数のパターンの多くは、大まかに言って「1つまたは複数の関数と、引数としての複数のリストを取り、関数をリスト引数に適用する」ことです。このテーマを利用する方法には、興味深く有益なものが驚くほどたくさんあります。別のパターンは、「関数群を取り、それらの機能を結合した関数を作成する」ことです。これにもまた、数多くのバリエーションがあります。functionalが提供するものをいくつか見てみましょう。

関数sequential( )also( )は、両方ともコンポーネント関数列に基づいた関数を作成します。その後は、同じ引数でコンポーネント関数を呼び出すことができます。両関数の主な違いは、sequential( )が引数として1つのリストを要求するのに対して、also( )は引数のリストを取るということだけです。ほとんどの場合、両関数は関数の副次作用としては役に立ちますが、sequential( )では、オプションとして、結合された戻り値を提供する関数を選ぶことができます。

リスト11:(同じ引数での)関数への順次呼び出し
>>> def a(x):
...     print x,
...     return "a"

...
>>> def b(x):
...     print x*2,
...     return "b"
...
>>> def c(x):
...     print x*3,
...     return "c"
...
>>> r = also(a,b,c)
>>> r
<functional.sequential instance at 0xb86ac>
>>> r(5)
5 10 15
'a'
>>> sequential([a,b,c],main=c)('x')
x xx xxx
'c'

関数disjoin( )conjoin( )は、引数をいくつかのコンポーネント関数に適用する新しい関数を作成するという点で、sequential( )also( )に似ています。しかしdisjoin( )は、(引数が与えられると)いずれかのコンポーネント関数が真を返すかどうかを尋ね、conjoin( )は、すべてのコンポーネントが真を返すかどうかを尋ねます。可能なら論理ショートカットが適用されるため、disjoin( )に関しては起きない副次作用もあるようです。joinfuncs( )also( )と似ていますが、主な戻り値を選ぶのではなく、コンポーネントの戻り値のタプルを返します。

前の関数によって同じ引数で複数の関数を呼び出す場合、any( )all( )、およびnone_of( )によって、引数のリストではなく同じ関数を呼び出します。一般的な構造では、組み込み関数のmap( )reduce( )filter( )に若干似ています。しかしfunctionalのこうした特殊な高階関数は、戻り値群に関するブール式の質問をします。たとえば、

リスト12:戻り値群に関する問い合わせ
>>> from functional import *
>>> isEven = lambda n: (n%2 == 0)
>>> any([1,3,5,8], isEven)
1
>>> any([1,3,5,7], isEven)
0
>>> none_of([1,3,5,7], isEven)
1
>>> all([2,4,6,8], isEven)
1
>>> all([2,4,6,7], isEven)
0

多少数学の心得がある方にとって特に興味深い高階関数はcompose( )です。いくつかの関数からなる構成は、一つの関数の戻り値が次の関数の入力となっている連鎖です。いくつかの関数を作成するプログラマーは、出力と入力を確実に一致させる責任がありますが、これはプログラマーが戻り値を使う際に常に当てはまることです。以下に、わかりやすい例を挙げます。

リスト13:構成関数の作成
>>> def minus7(n): return n-7
...
>>> def times3(n): return n*3
...
>>> minus7(10)
3
>>> minustimes = compose(times3,minus7)
>>> minustimes(10)
9
>>> times3(minus7(10))
9
>>> timesminus = compose(minus7,times3)
>>> timesminus(10)
23
>>> minus7(times3(10))
23

次回まで

今回は高階関数の最新状況を紹介しましたが、これによってみなさんがある種の考え方に興味を持っていただければ幸いです。ぜひともそうした考え方になじんでください。自作の高階関数をいくつか作成してみてください。そうすれば、いくつかの関数が有効かつ強力であることが十分にご理解いただけるでしょう。体験した感想をお知らせください。おそらくこの特別な連載の後半では、みなさんから今後もご提供いただける斬新かつ魅力的なアイディアを後日取り上げることになるでしょう。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=229879
ArticleTitle=魅力的なPython: Pythonでの関数プログラミング: 第3回
publish-date=06012001