魅力的なPython: Pythonでの関数プログラミング: 第1回

お気に入りのスクリプト言語を活用する方法

ユーザーは一般に、Pythonのことをオブジェクト指向の手続き型言語であると考えていますが、実際には、完全な関数的アプローチのプログラミングに必要なすべてのものを備えています。この記事では、関数プログラミングの一般概念について説明し、Pythonで関数技法を実装するための方法を示します。

David Mertz, Ph.D, Author, Gnosis Software, Inc.

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



2001年 3月 01日

最も難しい質問から始めることにしましょう。「関数プログラミング (FP:Functional Programming) とは、いったい何でしょう?」 1つの答えとして、FPとは、Lisp、Scheme、Haskell、ML、OCAML、Clean、Mercury、またはErlangなどの言語 (あるいはその他のいくつかの言語) でプログラムを書くときに行うことである、ということが言えそうです。これは、間違った答えではありませんが、あまり明確な答えではありません。残念ながら、FPとは何かということについては、関数プログラミングのプログラマーからも、一貫性のある意見を得ることは困難です。群盲象をなでる、という言葉がぴったりです。FPと「命令型プログラミング」(C、Pascal、C++、Java、Perl、Awk、TCL、および、少なくともほとんどの部分について、他のほとんどの言語で行っていること) と対比するのも一つのアプローチです。

個人的には、大まかに言って、関数プログラミングとは、以下の特性のうちの少なくともいくつかを備えたものであると考えています。関数言語と呼ばれる言語は、以下のことを行いやすくし、その他のことを困難または不可能にします。

  • 関数は、ファーストクラス・オブジェクトである。つまり、「データ」について行えることはすべて、関数自体についても (関数を別の関数に渡すなどの方法で) 行うことができます。
  • 主要な制御構造として再帰が使用される。言語によっては、それ以外の「ループ」構成は存在しないことがあります。
  • リスト処理に主眼が置かれている (たとえば、Lisp という名前にもこれが表れています)。多くの場合、ループに代えて、リストがサブリストの再帰とともに使用されます。
  • 「純粋な」関数言語は副次作用を避ける。これにより、最初にある値を変数に割り当て、次に同じ変数に別の値を割り当ててプログラム状態を追跡するという、命令型言語に付き物と思われるパターンが除外されます。
  • FPはステートメント の使用を思いとどまらせるか、あるいは完全に拒絶し、代わりに式 (つまり、関数に引数を加えたもの) の評価を行います。純粋な場合、1つのプログラムは1つの式 (およびそれをサポートする定義) になります。
  • FPは、どのように 計算するのかということよりも、何を 計算するのかということを気にします。
  • 多くのFPは「高階」関数 (つまり、関数を操作する関数を、操作する関数) を利用します。

関数プログラミングの支持者は、これらすべての特性が、開発に時間を要しない、短く、バグの危険が少ないコードを生み出していると主張します。さらに、コンピューター・サイエンス、論理学、および数学の理論家たちは、関数式の言語およびプログラムの形式的な特性のほうが、命令型の言語およびプログラムの形式的な特性よりもはるかに証明しやすいと考えています。

Python固有の関数機能

Pythonは、Python 1.0以来、上に示したFPの特性のほとんどを備えてきました。しかし、ほとんどのPythonフィーチャーは、非常に混合した形の言語で存在してきました。Pythonのオブジェクト指向プログラミング・フィーチャーについては、必要なものを使用し、それ以外のものを (後で必要になるまで)無視することができます。Python 2.0では、リストの内包表記 という、きわめて 気の利いた「統語的甘言」が追加されました。リストの内包表記は、新規機能を何も付け加えませんが、古い機能の多くをとても 快適なものにしてくれます。

PythonにおけるFPの基本要素は、関数map()reduce()、およびfilter() と、演算子lambda です。Python 1.xでは、apply() 関数も、ある関数のリスト戻り値を別の関数に直接に適用するために役に立っていました。Python 2.0では、この目的のために、より進歩した構文が用意されました。読者はおそらく驚くことでしょうが、これらのごくわずかな関数 (および基本演算子) だけで、どのようなPythonプログラムを書く場合にも、ほぼ十分なのです。特に、フロー制御ステートメント (ifelifelseasserttryexceptfinallyforbreakcontinuewhiledef) はすべて、FPの関数とオペレーターだけを使用した関数スタイルで扱うことができます。実際にプログラム内のすべてのフロー制御コマンドを除去することは、(Lispとよく似たコードを使う)「不明瞭なPython」コンテストにでも参加する場合にしか、役に立たないのではないかと思われますが、FPが関数と再帰を使用してどのようにフロー制御を表現するのかを理解しておくことは、価値があります。


フロー制御ステートメントの除去

フロー制御ステートメントを除去するにあたって最初に考えるべきことは、Pythonがブール式の評価を「短絡」させている、ということです。これにより、if/elif/else ブロックを式にしたものが提供されます (これは、それぞれのブロックが1つの関数を呼び出し、その関数がいつでも配置可能であることを前提とします)。その仕組みを次に示します。

リスト1. Pythonにおける「短絡」条件付き呼び出し
# Normal statement-based flow control
if <cond1>:   func1()
elif <cond2>: func2()
else:         func3()
# Equivalent "short circuit" expression
(<cond1>and func1())or (<cond2>and func2())or (func3())
# Example "short circuit" expression
>>> x = 3
>>>def pr(s):return s
>>> (x==1and pr('one'))or (x==2and pr('two'))or (pr('other'))
'other'
>>> x = 2
>>> (x==1and pr('one'))or (x==2and pr('two'))or (pr('other'))
'two'

式を使用した条件付き呼び出しは、単なる隠し芸の域を出ないように思えるかもしれませんが、lambda 演算子が式を戻さなければならないことに注目すると、さらに興味深いものになります。上に示したように、式には短絡によって条件ブロックを含めることができるため、lambda 式は、条件付き戻り値を表現するものとして、ごく一般的なものです。私たちの例を基に、次のようなlambda式が可能になります。

リスト2. Pythonにおける短絡を使用したlambda
>>> pr =lambda s:s
>>> namenum =lambda x: (x==1and pr("one")) \
....or (x==2and pr("two")) \
....or (pr("other"))
>>> namenum(1)
'one'
>>> namenum(2)
'two'
>>> namenum(3)
'other'

ファーストクラス・オブジェクトとしての関数

上の例はすでに、捕らえがたい形ではありますが、Pythonにおける関数のファーストクラスの状況を表しています。lambda 演算を使用して関数オブジェクト を作成するときには、非常に一般的な操作を行っています。したがって、数値23やストリング "spam" を "pr" や "namenum" などの名前に結合するのとまったく同じ方法で、オブジェクトをこれらの名前に結合することができたのです。しかし、どの名前とも結合しないで (つまり、関数の引数として) 数値23を使用できるのとまったく同様に、lambda で作成した関数オブジェクトを、どの名前とも結合しないで使用することができます。関数も、Pythonではよく使用されます。

ファーストクラス・オブジェクトについて行う主なことは、それらをFP組み込み関数map()reduce()、およびfilter() に渡すことです。これらの各関数は、最初の引数として関数オブジェクトを受け入れます。

  • map() は、渡された関数を、指定された (1つまたは複数の) リスト内の対応する項目で実行し、結果のリストを戻します。
  • reduce() は、渡された関数を、後続の各項目および最終結果の内部アキュムレーターで実行します。たとえば、reduce(lambda n,m:n*m, range(1,10)) は、「10の階乗」(つまり、各項目に、前の乗算の積をかけること) を意味します。
  • filter() は、渡された関数を使用してリスト内の各項目を「評価」し、関数テストに合格した項目のリストを戻します。

また、関数オブジェクトをカスタム関数に渡すこともよく行われますが、通常は上記の組み込み関数の組み合わせによっても同じことが可能です。

これらの3つのFP組み込み関数を結合することにより、きわめて広範囲の「フロー」操作を (ステートメントを使用しないで、式だけで) 行うことができます。


Pythonによる関数ループ

ループの置き換えは、条件付きブロックの置換と同じように単純に行えます。formap() に直接変換することができます。私たちの条件付き実行もそうですが、ステートメント・ブロックを単一関数呼び出しに単純化する必要があります (これは、まもなく、一般的に行えるようになります)。

リスト3. Pythonにおける関数的な 'for' ループ
for ein lst:  func(e)# statement-based loop
map(func,lst)# map()-based loop

ところで、関数的なアプローチで順次プログラム・フローを制御するためにも、類似の技法を使用することができます。つまり、命令型プログラミングは多くの場合、「これを行って、次にそれを行って、その後で別のことを行いなさい」ということを指示するステートメントからなっています。map() を使用すると、それを行うことができます。

リスト4. Pythonにおける関数的な順次アクション
# let's create an execution utility function
do_it =lambda f: f()
# let f1, f2, f3 (etc) be functions that perform actions
map(do_it, [f1,f2,f3])# map()-based action sequence

一般に、私たちのメインプログラム全体は、プログラムを完了させるために実行する必要のある関数のリストを含むmap() 式と考えることができます。ファーストクラス関数の、別の便利な特徴として、クラス関数をリストに入れられることが挙げられます。

while の変換は、やや複雑ですが、それでも、次のように直接行うことができます。

リスト5. Pythonにおける関数的な 'while' ループ
# statement-based while loop
while <cond>:
    <pre-suite>
if <break_condition>:
breakelse:
        <suite>
# FP-style recursive while loopp
def while_block():
    <pre-suite>
if <break_condition>:
return 1
else:
        <suite>
return 0

while_FP =lambda: (<cond>and while_block())or while_FP()
while_FP()

私たちのwhile 変換は、まだ、式だけでなくステートメントを含む可能性のあるwhile_block() 関数を必要とします。しかし、この関数にさらに (テンプレート内でのif/else の短絡などの) 除去を適用することができます。また、ループ本体が (その設計上) 変数値を変更することができないため、<cond> をwhile myvar==7 などの通常のテストで利用することは困難です (ただし、グローバル変数はwhile_block() で変更することができます)。より役に立つ条件を追加する方法の1つとして、while_block() にもっと面白い値を戻させて その戻り値を終了条件と比較するやり方があります。ステートメントを除去する具体的な例を示します。

リスト6. Pythonにおける関数的な 'echo' ループ
# imperative version of "echo()"
def echo_IMP():
while 1:
        x = raw_input("IMP -- ")
if x =='quit':
breakelseprint x
echo_IMP()
# utility function for "identity with side-effect"
def monadic_print(x):
print x
return x
# FP version of "echo()"
echo_FP =lambda: monadic_print(raw_input("FP -- "))=='quit'or echo_FP()
echo_FP()

ここまでで行ったことは、再帰を含む純粋な式として、(実際には、必要に応じてどこかに渡すことのできる関数オブジェクトとして) 入出力、ループ、および条件ステートメントを含む小さなプログラムを表したことです。それでもユーティリティー関数monadic_print() は使用しています。しかし、この関数は完全に汎用的なものであり、後で作成するそれぞれの関数プログラム式で再利用することができます (つまり、一回限りのコストで済みます)。monadic_print(x) を含む式は、単なるx を含む式と同じ結果になります。FP (特にHaskell) では、「何も行わず、プロセス内で副次作用を持つ」関数のことを「モナド」と言います。


副次作用の除去

結局、私たちの作業は、完全に理解できるステートメントを除去し、それらに代えて不明瞭にネストされた式を置き換えていることになります。なぜこのようなことをする必要があるのでしょうか。私のFPの説明は、すべてPythonで行われています。しかし、最も重要な (そしてまた、具体的に役立つと思われる) 特性は、副次作用を除去すること (あるいは、少なくともモナドなどの特別な領域に封じ込めること) です。プログラム・エラー (およびプログラマーがデバッグしなければならない問題) のうち、プログラム実行中に変数が予期しない値を獲得するために起こるものが、きわめて多くの割合を占めています。関数プログラムはこの問題を、変数に値を割り当てないという単純な方法で回避します。

よく見かけるたぐいの命令型コードを見てみましょう ここでの目標は、掛け合わせた積が25を超える数のペアのリストを印刷することです。それらのペアを構成する数値自体は、他の2つのリストから得られます。こうしたことは、実際にプログラマーがプログラムのセグメントで行っていることとかなり似ています。命令型のアプローチでは、次のようにしてこの目標を達することになります。

リスト7. 「大きな積の印刷」を行う命令型Pythonコード
# Nested loop procedural style for finding big products
xs = (1,2,3,4)
ys = (10,15,3,22)
bigmuls = []
# ...more stuff...
for xin xs:
for yin ys:
# ...more stuff...
if x*y > 25:
            bigmuls.append((x,y))
# ...more stuff...
# ...more stuff...
print bigmuls

このプロジェクトは小さいため、まずいことはなさそうです。しかし、私たちの目標は、コードの中に組み込こまれることになり、そのコードは、同時に他のいくつもの目標を達成することになると思われます。"more stuff" というコメントが付いたセクションでは、副次作用によってバグが生じる可能性があります。これらのどの個所でも、変数xsysbigmulsxy が、仮に省略して書いたコード中で、予期しない値を獲得する可能性があります。さらに、このコードが実行された後で、すべての変数に入る値が、必ずしも後のコードで予期されたり必要とされたりするわけではありません。関数 / インスタンスでカプセル化を使用したり、有効範囲を管理したりすることにより、こうしたエラーを防ぐことが期待できます。また、使い終わった変数は常にdel することができます。しかし実際には、示されるエラーのタイプは共通です。

私たちの目標への関数的なアプローチでは、こうした副次作用のエラーをまとめて除去することができます。コードの例を次に示します。

リスト8. 「大きな積の印刷」を行う関数的Pythonコード
bigmuls =lambda xs,ys: filter(lambda (x,y):x*y > 25, combine(xs,ys))
combine =lambda xs,ys: map(None, xs*len(ys), dupelms(ys,len(xs)))
dupelms =lambda lst,n: reduce(lambda s,t:s+t, map(lambda l,n=n: [l]*n, lst))
print bigmuls((1,2,3,4),(10,15,3,22))

この例では無名 (lambda) 関数オブジェクトを名前にバインドしていますが、これは必ずしも必要ではありません。代わりに、定義をネストするという簡単な方法があります。この方法を使用したのは読みやすさを考慮したためですが、combine() という便利なユーティリティー関数を利用できるためでもあります (これは、2つの 入力リストから得られる要素のペアをすべてリストします)。dupelms() は、主としてcombine() を支援する手段として使用されます。この関数の例は、命令型の例よりも冗長ですが、ユーティリティー関数を再利用することにより、bigmuls() 内の新規コードは命令型の場合よりも若干短くなると思われます。

この関数の例の本当の利点は、その中のどの変数の値も絶対に変更されないことです。後の部分のコードで (あるいは前の部分のコードが原因で) 予期しない副次作用が起こる可能性はありません。もちろん、副次作用がないということ自体は、コードが適切 であることを保証するものではありませんが、利点であることは確かです。ただし、Pythonが (多くの関数言語とは異なり) 名前bigmulscombine、およびdupelms の再バインドを禁止していない ことに注意してください。combine() がプログラム内の後の部分で異なる意味を持ち始めると、すべてが台無しになってします。そのような不変のバインディング (たとえばs.bigmuls など) を含むSingletonクラスを使用することもできますが、それを説明するための紙面の余裕はありません。

私たちの目標が、Python 2の新規フィーチャーに適したものであることは、注意する価値があります。最良の (そして関数の) 技法は、ここに示した命令型の例でも関数の例でもなく、次のような方法です。

リスト9. "bigmuls" に代わるPythonのリスト内包表記コード
print [(x,y)for xin (1,2,3,4)for yin (10,15,3,22)if x*y > 25]

結論

以上で、Pythonのそれぞれのフロー制御構成を (プロセス内の副次作用を抑えながら) 相当する関数の構成によって置き換える方法を示しました。特定のプログラムを効果的に変換するためには、いくつかの追加考慮事項が必要ですが、組み込み関数が一般的で、完全なものであることは確認できました。この後のコラムでは、関数プログラミングのためのさらに進んだ技法を見ていく予定です。また、できれば、関数的スタイルに関する賛否両論についても、もう少し検討できると思います。

参考文献

  • Bryn Kellerの"xoltar toolkit" には、functional モジュールが組み込まれ、Pythonに対する多くの有益なFP拡張が追加されています。functional モジュール自体が、すべてPythonで書かれていますので、このモジュールの働きは、すでにPythonで可能であったことです。しかし、Kellerは、非常に巧みに統合された拡張機能を考案し、それらに簡潔な定義を与えています。
  • Peter Norvigは、Python for Lisp Programmers という興味深い記事を書いています。彼の主眼点は、私のコラムとは逆の方向に向けられていますが、PythonとLispに関する、非常に優れた一般的な比較を行っています。
  • 関数プログラミングを開始するにあたっては、Frequently Asked Questions for comp.lang.functional を参照することをお勧めします。
  • 私は、Lisp/Schemeよりも (Schemeは、少なくともEmacsでは、広く使用されていますが)、Haskell 言語を使用したほうが、関数プログラミングの概要を把握しやすいと思いました。括弧や接頭部 (ポーランド式) 演算子があまり多くないため、他の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=229877
ArticleTitle=魅力的なPython: Pythonでの関数プログラミング: 第1回
publish-date=03012001