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

関数プログラミングに取りかかりましょう

この記事は、DavidのPythonによる関数プログラミング (FP) に関する紹介記事の続きです。この紹介記事では、問題解決のプログラムの各種のパラダイムが示されており、そこでDavidは、中レベルから高レベルのFP概念をいくつか説明します。

David Mertz, Ph.D (mertz@gnosis.cx), Author, Gnosis Software, Inc.

Photo of David MertzDavid Mertz氏は多くの分野で活躍しています。ソフトウェア開発や、それについて著述もしています。その他、学術政策理念について分野を問わず、関係する雑誌に記事も書いています。かなり以前には、超限集合論、ロジック、モデル理論などを研究していました。その後、労働組合組織者として活動していました。そして、David Mertz氏自身は人生の半ばにもまだ達していないと思っているので、これから何かほかの仕事をするかもしれません。



2001年 4月 01日

関数プログラミングに関するわたしの前回の記事では、FPのいくつかの基本的な概念を紹介しました。今回の記事では、この非常に価値のある概念領域についてもう少し深く掘り下げてみます。この検討に関して、Bryn Kellerの "Xoltar Toolkit" が非常に参考になると思います。Kellerは、FPの長所を数多く集めて1つのすばらしい小モジュールに組み入れました。このモジュールでは各種技法がPythonだけで実現されています。functional モジュールに加え、Xoltar Toolkitにはlazy モジュールも含まれています。このモジュールは、「必要なときだけ」数値計算をする構造体をサポートします。多くの従来型関数言語もlazyな数値計算を行います。そのため、これらのコンポーネント間では、Xoltar Toolkitを使用することで、Haskellのような関数言語が持つ機能の多くを実行することができます。

バインディング

注意深い読者であれば、第1回記事で、関数技法には制限があると、わたしが指摘したのを覚えておられるでしょう。その制限とは、関数式を示すために使用する名前の再バインドを防止する手立てがPythonにはないということです。FPの場合、名前は長い式を短縮したものであると一般に理解されていますが、「同じ式は常に同じ結果を出す」というのが暗黙の約束です。指示名が再バインドされると、この約束が破られます。たとえば、以下のような、関数プログラムで使用するいくつかの簡略式を定義するとします。

リスト1. 誤動作を起こす再バインドを持つPython FPセッション
>>> car =lambda lst: lst[0]
>>> cdr =lambda lst: lst[1:]
>>> sum2 =lambda lst: car(lst)+car(cdr(lst))
>>> sum2(range(10))
1
>>> car =lambda lst: lst[2]
>>> sum2(range(10))
5

この式それ自体は可変変数を引数に一切使用していないのに、残念ながら、この同じ式sum2(range(10)) が、プログラムの2つの場所で2つの異なる結果を出しています。

幸いなことに、functional モジュールは、Bindings というクラスを提供しています。これは、わたしがKellerに勧めたもので、このような再バインドを防止します。しかし、これはあくまで不測の再バインドを防止するもので、故意に破壊しようと思っているプログラマーによるバインドは防止できません。Bindings を使用するには多少余分の構文が必要になりますが、これを使用すれば予期せぬエラーを起きる度合いを減らすことができます。functional モジュールの中の例で、Kellerは、Bindings インスタンスにlet という名前を付けています (MLファミリー言語のlet キーワードに由来しているのではないでしょうか)。たとえば、以下のように行うことができます。

リスト2. 保護された再バインドを持つPython FPセッション
>>>from functionalimport *
>>> 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.
>>> car(range(10))
0

プログラムでは、当然これらの "BindingError" について何らかの対応をしなければなりませんが、エラーが見つけ出されているという事実こそが、この種の問題の回避に役立っています。

Bindings の他にも、functional モジュールは、ネームスペース (実際はディクショナリー) をBindingsインスタンスから取り出すnamespace 関数を提供しています。この関数は、式の計算をBindings に定義された (不変の) ネームスペース内で行いたい場合に 役立ちます。Python関数eval() を使用すれば、ネームスペース内で数値を求めることができます。例をあげてそれを示しましょう。

リスト3. 不変のネームスペースを使用するPython FPセッション
>>> let = Bindings()# "Real world" function names
>>> let.r10 = range(10)
>>> let.car =lambda lst: lst[0]
>>> let.cdr =lambda lst: lst[1:]
>>> eval('car(r10)+car(cdr(r10))', namespace(let))
>>> inv = Bindings()# "Inverted list" function names
>>> inv.r10 = let.r10
>>> inv.car =lambda lst: lst[-1]
>>> inv.cdr =lambda lst: lst[:-1]
>>> eval('car(r10)+car(cdr(r10))', namespace(inv))
17

クロージャー

FPでの非常に面白い概念にクロージャー というものがあります。実際、クロージャーは、多くのデベロッパーにとって非常に興味を引く機能であり、PerlやRubyなどの一般の非関数言語にさえ、クロージャーが機能として組み込まれているほどです。さらに、Python 2.1は、クロージャーのほとんどの機能を提供する字句スコープ機能を追加する予定になっているようです。

さて、クロージャーとは何 なのでしょうか。Steve Majewskiは最近、Pythonニュースグループの中で、クロージャーの概念についてすばらしい特徴付けを行いました。

オブジェクトとは、処理が付加されたデータのことです。クロージャーとは、データが付加された処理のことです。

つまり、クロージャーとは、OOPでのハイドに対するFPのジキルのようなものです (あるいは、これらの役割は逆であるかもしれません)。オブジェクト・インスタンスと同じように、クロージャーは、データと機能を1束にくるんで運ぶ方法です。

ここで少し後へ戻って、オブジェクトとクロージャーがどんな問題を解決するのか、また、これらがない場合は、どのようにしてその問題を解決するのか検討しましょう。関数によって戻される結果は、通常、その計算で使用するコンテキストによって決定されます。このコンテキストを指定するための最も一般的な、そしておそらくは最も明白な方法は、演算で使用する値を引数として関数に渡すことです。しかし時として、"バックグラウンド" 引数と "フォアグラウンド" 引数を区別ができる場合があります。つまり、この関数がここで何を実行するかということと、この関数が複数の呼び出しのためにどのように "構成" されているかということの区別です。

フォアグラウンドに焦点を合わせながらバックグラウンドを処理するには、多くの方法があります。1つの方法は、単に“頑張ってやる”ことで、各呼び出しのたびに、関数に必要なすべての引数を渡すことです。この結果、しばしば起こることですが、呼び出し連鎖の中のどこかでこれらの値が必要になるかもしれないことを想定して、多くの値 (または複数のスロットを持つ構造体) をこの呼び出し連鎖のいたるところに配置してしまうことになります。平凡な例としては、次のようなものがあります。

リスト4. カーゴ変数を示すPythonセッション
>>>def a(n):
...     add7 = b(n)
...return add7
...
>>>def b(n):
...     i = 7
...     j = c(i,n)
...return j
...
>>>def c(i,n):
...return i+n
...
>>> a(10)# Pass cargo value for use downstream
17

このカーゴ例の場合、b() 内では、n は、c() に渡されるためだけに存在します。もう1つのオプションは、グローバル変数を使用することです。

リスト5. グローバル変数を示すPythonセッション
>>> N = 10
>>>def addN(i):
...global N
...return i+N
...
>>> addN(7)# Add global N to argument
17
>>> N = 20
>>> addN(6)# Add global N to argument
26

グローバル変数N は、addN() を呼び出す度に使用できますが、そのためにグローバルなバックグラウンド・コンテキストを明示的に渡す必要はありません。もうちょっとPythonらしいやり方は、以下のように、定義時にデフォルトの引数を使用して、変数を関数の中に"凍結" することです。

リスト6. 凍結された変数を示すPythonセッション
>>> N = 10
>>>def addN(i, n=N):
...return i+n
...
>>> addN(5)# Add 10
15
>>> N = 20
>>> addN(6)# Add 10 (current N doesn't matter)
16

凍結された変数は基本的にはクロージャーです。一部のデータは、addN() 関数に "付加" されます。完全なクロージャーの場合、addN() の定義時に提供されたすべてのデータが、呼び出し時に使用可能になります。ただし、この例 (およびそれより堅固な多くの例) では、デフォルトの引数で十分なデータ 使用可能になるようにするのが簡単です。addN() で使用されない変数は、その計算に影響を与えません。

次に、もう少し実際的な問題に対するOOPアプローチを検討しましょう。今年も税金の時期になりましたので、"インタビュー" スタイルの税金計算プログラムについて考えてみます。このプログラムでは、各種のデー タを収集し (必ずしも特定の順序ではない)、最終的にはそれらのすべてのデータを計算に使用します。これの簡単なバージョンを作って見ましょう。

リスト7. Pythonスタイルの税額計算クラス / インスタンス
class TaxCalc:
def taxdue(self):
return (self.income-self.deduct)*self.rate
taxclass = TaxCalc()
taxclass.income = 50000
taxclass.rate = 0.30
taxclass.deduct = 10000
print"Pythonic OOP taxes due =", taxclass.taxdue()

この例のTaxCalc クラスでは (またはむしろ、そのインスタンスでは)、一部のデータを収集することができ (任意の順序で)、必要なすべてのエレメントを取得したら、このオブジェクトのメソッドを呼び出してこのデータの束について計算を行うことができます。すべてのものがこのインスタンス内に一緒になって入っており、さらに、別のインスタンスは別のデータの束を持つことができます。データだけが異なる複数のインスタンスの作成は、"グローバル変数" または "凍結された変数" のアプローチで実現できなかったものです。"カーゴ" アプローチではこれを処理することができますが、拡張例の場合は、数値を渡す必要があるかもしれません。ここで、メッセージを渡すOOPスタイルがこれをどのように処理するか見てみるのも一興です (SmalltalkやSelfはこれと似ていますが、わたしがこれまで使用してきたいくつかのOOP xBase変数もこれと似ています)。

リスト8. Smalltalkスタイル (Python) 税額計算
class TaxCalc:
def taxdue(self):
return (self.income-self.deduct)*self.rate
def setIncome(self,income):
        self.income = income
return self
def setDeduct(self,deduct):
        self.deduct = deduct
return self
def setRate(self,rate):
        self.rate = rate
return self
print"Smalltalk-style taxes due =",\
      TaxCalc().setIncome(50000).setRate(0.30).setDeduct(10000).taxdue()

self にそれぞれの "setter" を付けて戻すと、"current" なものをすべてのメソッド・アプリケーションの結果として扱うことができます。この方法には、FPクロージャー・アプローチとのいくつかの興味深い類似点があります。

Xoltar Toolkitを使用すれば、データと関数を結合した望みの特性を持つ完全なクロージャーを作成でき、また、複数のクロージャー (以前のオブジェクト) に以下のような異なるバンドルを含めることができます。

リスト9. Python関数スタイルの税額計算
from functionalimport *
taxdue        =lambda: (income-deduct)*rate
incomeClosure =lambda income,taxdue: closure(taxdue)
deductClosure =lambda deduct,taxdue: closure(taxdue)
rateClosure   =lambda rate,taxdue: closure(taxdue)
taxFP = taxdue
taxFP = incomeClosure(50000,taxFP)
taxFP = rateClosure(0.30,taxFP)
taxFP = deductClosure(10000,taxFP)
print"Functional taxes due =",taxFP()
print"Lisp-style taxes due =",\
      incomeClosure(50000,
          rateClosure(0.30,
              deductClosure(10000, taxdue)))()

上で定義した各クロージャー関数は、関数のスコープ内に定義されている任意の値を取り、それらの値を関数オブジェクトのグローバル・スコープにバインドします。ただし、関数のグローバル・スコープとして現れたものが、必ずしも真のモジュール・グローバル・スコープであるとは限らず、また別のクロージャーの "グローバル" スコープと同一であるとも限りません。クロージャーは、自分と一緒に "データを運ぶ" だけです。

この例では、いくつかの特定の関数を使用して、特定のバインディングをクロージャーのスコープ (収入、控除、比率) に入れています。任意のバインディングをスコープに入れるように設計変更するのは、簡単です。わたしたちも遊び心で、この少し異なる2つの関数スタイルを例に使用しています。最初の例では、継続的に追加の値をクロージャー・スコープにバインドしています。つまり、taxFP を絶えず変えられるようにすることによって、これらの "クロージャーに追加する" 行を任意の順序で記述することができます。しかし、tax_with_Income のような不変の名前を使用する場合は、バインディング行を特定の順序に配置し、先のバインディングをその次のバインディングに渡す必要があります。いずれにしても、必要なものがすべてクロージャー・スコープにバインドされた後で、"種がまかれた" 関数を呼び出すことができます。

2番目のスタイルは、よりLispに似ているように、わたしには見えます (主に括弧の部分)。見た目の他にも2つの面白い事象が2番目のスタイルで発生します。最初の事象は、名前のバインディングが完全に回避されることです。この2番目のスタイルは単一の式であり、ステートメントを使用しません (このことがなぜ問題になるかの議論については、「第1回」を 参照してください)。

クロージャーを "Lispスタイル" で使用することについてのもう1つの面白い点は、それが上記の "Smalltalkスタイル" のメッセージ渡しメソッドとよく似ているということです。どちらも基本的に、taxdue() 関数 / メソッド呼び出しで値を累積します (正しいデータが使用できなければ、どちらもこれらの未加工バージョンでエラーを起こします)。"Smalltalkスタイル" は各ステップでオブジェクトを渡しますが、他方 "Lispスタイル" は継続を渡します。しかし本当のところは、関数プログラミングとオブジェクト指向プログラミングは、結局同じところに落ち着くのです。


末尾再帰

今回の記事では、関数プログラミングのさらに大きな分野を片付けました。残った部分が、以前より少なく (また、多分簡単に?) なっています (このセクションのタイトルは軽いジョークです。残念なことに、その概念はここまででまだ説明されていませんが)。functional モジュールのソースを読むのが、多くのFP概念を調べるための優れた方法です。モジュール類は非常によくコメントされており、またその関数 / クラスの多くについて例が示されています。この記事では、多くの単純化メタ関数 (他の関数との組み合わせや対話を処理しやすくする) については説明していません。これらのメタ関数は、関数パラダイムを調べようとするPythonプログラマーにとって、非常に検討の価値のあるものです。

参考文献

  • Davidによる、Pythonにおける関数プログラミングに関する前回の記事は、このトピックの導入部です。
  • Bryn Keller著"xoltar toolkit" では、functional モジュールを組み込んでいて、多くの有用なFP拡張機能をPythonに付け加えています。functional モジュールそれ自体はすべてPythonで書かれており、その機能はすでにPythonそのもので使用可能になっていたものです。しかしKellerは、非常によく統合した拡張機能セットを考え出して、多くのパワーをコンパクトな定義にまとめ上げました。
  • Peter Norvigは、「Python for Lisp Programmers」という面白い記事を書いています。そこで焦点を当てているのは、わたしの記事とは多少逆になりますが、PythonとLispの一般的な比較を非常にうまく行なっています。
  • 関数プログラミングを行うための適切な開始点は、「 Frequently Asked Questions for comp.lang.functional」です。
  • 関数プログラミングを理解するには、Lisp/Scheme言語よりも Haskell 言語のほうがはるかに容易であることが分かりました (ただ、Emacsに限って言えば、おそらく前者の方が広く使用されていると思われます)。他のPythonプログラマーも同様に、それほど多くの括弧やプレフィックス (ポーランド記法) 演算子を使わなくてもよいでしょう。
  • 優れた入門書は、Haskell著「The Craft of Functional Programming (2nd Edition)」 (Simon Thompson (Addison-Wesley, 1999)) です。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=229878
ArticleTitle=Pythonでの関数プログラミング: 第2回
publish-date=04012001