目次


高階関数

高階目的での引数、関数生成関数、および匿名関数としての関数の使用

Comments

コンピューター・プログラムでは、関数は強力なビルディング・ブロックとなり、関数を使用することで、開発者はコードを単純で管理しやすいステップに分解でき、プログラマーはプログラムを再利用可能なパーツに分解することができます。すばらしい関数に敬意を表して、この記事では、テンプレートに基づく新しい関数を実行時に作成する方法を示し、関数パラメーターを使用して実行時に構成できる関数を組み立てる方法を説明します。

この記事の例では、Schemeプログラミング言語とCを使用します。Schemeプログラミング言語の概要については、「効果的なリスト処理でプログラミングを改善する」(developerWorks、2005年1月)を参照してください(参考文献も参照。その他のSchemeの紹介とScheme関数リファレンスへのリンクがあります)。

では、早速、いくつかの関数を作成してみましょう。

匿名関数の作成

Schemeでは、関数はデフォルトでは名前を付けずに作成されます。lambdaスペシャル・フォームは、名前のない関数を作成して、値をエンクロージング・フォームまたは関数呼び出しに返します。エンクロージング・フォームは次のことを実施できます。

  • 値を参照するシンボルを設定します(そして名前を付けます)。
  • 後で使用するために、値をデータ構造に格納します。
  • 値をパラメーターとして関数に渡します。

ほとんどのプログラミング言語では、関数の定義と命名は同時に行われます。Schemeでは、これらの操作が個別の動作になっているため、初めてSchemeに触れるプログラマーの多くが戸惑います。しかし、Schemeメソッドは実は非常に簡単です。Schemeでは、関数は他の値とまったく同じように扱われるからです。

関数はlambdaによって作成されます。他の値とまったく同様に、これらの関数は、

  • 引数として渡すことができ、
  • 変数に格納することができ、
  • より大きなデータ構造の一部として格納することができます。

lambdaで作成されたScheme関数を他言語の関数のように動作させるには、グローバル変数に格納するだけです。こうすると、関数が他の関数から見えるようになり、名前でアクセスできるようになります。リスト1は、与えられた数を2乗するScheme匿名関数の例です。

リスト1. 無名の関数
(lambda (x)
   (* x x)
)

このコードは、1つの仮パラメーターx で関数を定義しています。この関数は、パラメーターを2乗して、値を返します。すでに述べたように、Schemeは明示的な戻り値を必要としません。Schemeは、関数で最後に評価されたフォームの結果を返すだけです。

では次に、リスト2のように関数に名前を付けましょう。

リスト2. 関数の命名
(define square
   (lambda (x)
      (* x x)))
(display (square 3))
(newline)
(display (square 4))
(newline)

これは、関数の扱い方としては最も単純かつ一般的な方法です。すなわち、関数に名前を付けて、その後の計算で使用します。しかし、Schemeでは、関数を使用する前に関数に名前を付けなければならないという規則はありません。

Schemeでは、プログラムのリストの頭部は、関数またはスペシャル・フォームでなければなりませんが、関数の名前でなければならないというわけではありません。lambdaスペシャル・フォームは関数を返すので、実際に関数呼び出しの中でlambda関数定義を直接使用することができます。

例えば、(square 4)と書く代わりに、( (lambda (x) (* x x)) 4)と書くこともできます。これは2要素のリストであり、最初の要素は関数の定義、そして2番目の要素は数です。このリストの頭部は、関数定義そのものです。これを実行すると、まず、リスト内のすべてを評価します。lambdaフォームの評価結果は関数定義になり、これはリストの頭部なので、2番目の要素で呼び出されます。このフォームの戻り値は、数4の2乗です。

関数に名前を付けたり値として渡すのではなく、定義してすぐに呼び出すというのは興味深い考えですが、非常に便利なわけではありません。しかし、値としての関数の考え方と、匿名関数がどのように機能するかがわかります。

では、関数の引数としての関数を見てみましょう。

関数の引数としての関数

無名関数の実用的な用途は、関数を作成して他の関数に引数として渡すことです。この好例は、Schemeの組み込み関数mapです。

map関数は2つの引数を取ります。すなわち、1つの変数の関数とリストです。次に、その関数をリストの各要素に適用して、それらの結果からなる新しいリストを返します。例えば、この2乗関数を使用すると、数のリストを取って、それらを2乗することができます(リスト3)。

リスト3. Schemeで関数を引数として関数に渡す
;Our function
(define square (lambda (x) (* x x)))
;Our data
(define my-test-data '(1 2 3 4 5 6))
;Create a new list as the squares of my-test-data
(define list-of-squares (map square my-test-data))
;Display results
(display list-of-squares)
(newline)

これは他の言語に比べるとかなり簡潔なプログラムです。例えば、Cでこのコードに相当するコードはリスト4のようになります。

リスト4. Cで関数を引数として関数に渡す
int square(int x)
{
   return x * x;
}
int main()
{
   int my_test_data[6] = {1, 2, 3, 4, 5, 6};;
   int list_of_squares[6];
   int i;
   /* Note that this would only work if C had a function
    * called map which worked with integer functions.
    * It doesn't, so this is largely theoretical.  Also remember
    * that C doesn't know the size of the arrays, so you have to
    * pass it explicitly.  Also, list_of_squares has to be passed
    * as a parameter in C.
    */
   map(square, my_test_data, list_of_squares, 6);
   printf("(");
   for(i=0; i<6; i++)
   {
      printf("%d ", list_of_squares[i]);
   }
   printf(")\n");
}

もちろん、Cでは、「効果的なリスト処理でプログラミングを改善する」の記事で説明したようなtypetagメカニズムを使用しない限り、mapで使用する値の型ごとに異なる関数が必要です。

Cより簡潔であることに加えて、このような関数をSchemeでコーディングすることには、もう1つのメリットがあります。それは、関数にsquareという名前を付ける必要がないことです。Schemeでは、他の値とまったく同じように関数を作成して渡すことができるので、コードの中核を修正することができ、2乗関数に名前を付ける必要さえなくすことができます。定義して、mapに渡すだけでよいのです。変数名squareの代わりに、lambdaフォームとして書いた関数の定義を使います。リスト5に、そのコードを示します。

リスト5. 匿名関数を値として渡す
(define list-of-squares (map (lambda (x) (* x x)) '(1 2 3 4 5 6)))
(display list-of-squares)
(newline)

では、このようにする理由は何でしょう。関数が一度しか使用されない場合、匿名関数には、名前付き関数にはない、いくつかのメリットがあります。

  • プログラムの名前空間が、一度しか使用されない多くの関数で汚されることがありません。
  • 関数のコードをそれが使用される正確な場所に置くことができるので、プログラマーは1回限りの小さな関数のコードを探し回る必要がありません。
  • 関数とそれが渡される関数とを明確に関連付けることができるので、関数が使用されるコンテキストが他のプログラマーに分かりやすくなります。1回限りの関数がそれを呼び出す関数と離れた場所にあった場合、呼び出す関数が後から削除された場合、1回限りの関数も削除する必要があるのかどうか明確ではありません。

要するに、匿名関数を使用するとコードが簡潔で分かりやすくなり、関数が1回のインスタンスや特定用途に特化されていることが他のプログラマーに分かりやすくなります。

また、map関数に見られるように、関数を引数として渡すことで、データがその後どうなるかをきめ細かに制御できます。この例では、map関数はリスト内のそれぞれの値を反復し、関数を使用して、そのリスト値のその後の処理を管理しています。これは、関数の作成者と使用者に大きな柔軟性を与えます。すなわち、使用者が関数を引数として渡すことによって機能性を付加できる関数を作成することができます。

mapのほかにも、関数をパラメーターとして取る汎用リスト処理関数がいくつかあります。これらの関数とこれらに類似の関数は、あらゆるデータ処理アプリケーションのバックボーンです。これらの汎用関数を使用すると、プログラミングの反復をかなり削減することができます。基本的には同じように作用するループを次から次へと書く必要もなくなります。時間を節約できるだけでなく、バグが紛れ込む可能性も低くなります。

最も一般的な汎用リスト処理関数としては、次のものがあります。

  • mapは、リストを取り、それぞれのリスト・メンバーにユーザーによって与えられた関数を適用することによって新しいリストを生成します。
  • filterは、リストを取り、そのリストのメンバーのうち、ユーザーによって指定された条件に一致するメンバーを含んだ新しいリストを生成します。
  • reduceは、リストを取り、それらの値を結合して1つの値にします。結合手続きとしては、合計、最小値、最大値などがあります。これを正しく使用するためには、閉包を使用する必要があります(「実行時の関数の作成」を参照してください)。

他にも汎用リンク処理関数はありますが、これらは最も覚えやすく、最も広く使用されている基本的なものです。これらの関数の詳しい説明については、「参考文献」のリンクをクリックしてください。

関数を他の関数に引数として渡す方法を学んだので、次は、関数をパラメーターとして取る関数を作成する方法を見てみましょう。

引数としての関数の使用

初心者プログラマーは、どのような場合に関数を引数として渡したらよいのかわからないことが多いようです。多くのプログラムでは、少しずつバリエーションが加えられているものの、基本的には同じアルゴリズムが何度も使われているということが1、2例あります。このような場合に、関数を引数として使用できます。

アルゴリズムの基本構造を1つの関数にするのです。そうすると、あちこちに散らばっているコードの断片を関数のパラメーターとして1つにまとめることができます。これにより、関数のカスタマイズの幅が大きくなります。将来の拡張のために、関数パラメーターを追加することもできます。このように関数を組み合わせると、プログラムを冗長でないものにすることができ、拡張によって、プログラムの中で同じアルゴリズムを何度も繰り返して書く場合に起こりがちな誤りを減らすことができます。

次のようなパーツからなる受注処理アルゴリズムがあるとします。

  • 注文書の各行を処理して、合計を求めます。
  • 送料を計算します。
  • 購入者のクレジットライン(与信限度枠)を検証します。
  • 問題がなければ、代金の支払請求を行い、受注確認を送信し、データベースに記録します。

では、顧客ごとに送料の計算が異なり、クレジットラインの計算が異なり、代金の請求方法が異なる場合を考えてみましょう。例えば、送料計算がクライアントのサービス・プロバイダーごとに異なる場合があります。クレジットラインの確認も、一部の顧客については自社で行い、その他の顧客についてはクレジットカード会社を通じて行う場合があります。代金の請求方法は、通常の請求書による場合、クレジット・カードによる場合、自動引き落としによる場合などによって大きく変わります。顧客ごとに、これらの各段階で必要な処理が違ってきます。

このような関数をコード化するひとつの方法は、単純に、受注処理アルゴリズムの中の可能な経路のすべてを直接ハードコード化することです。その場合、この関数の呼び出しには、どの処理形式を要求するかを示すフラグのリストが含まれることになるでしょう。しかし、可能性の数が多くなるにつれて、受注処理アルゴリズムが巨大になってしまいます。

もうひとつの方法は、個別の関数をアルゴリズムに渡すことによって、これらの段階を個別に処理することです。この場合、受注処理アルゴリズムに必要なのは、直接コード化されたアルゴリズムの一般的な流れだけです。それぞれの主要段階の詳細は、渡される関数によって処理されます。このようなアルゴリズムに必要なのは、送料計算関数、クレジット検証関数、および代金請求関数のパラメーターです。

リスト6に、このようなアルゴリズムをコード化するSchemeの関数を示します。

リスト6. 受注処理での引数としての関数の使用
(define process-order
   ;;The function has one data record for the order and three functions as parameters
   (lambda (order ship-calc-func credit-validate-func charge-order-func)
      (let
         (
            ;;Here you execute functions to get the fields
            ;;from the order that you will need for later
            ;;processing
            (order-lines (get-order-lines order))
            (origin (get-origin-address order))
            (destination (get-destination-address order))
            (delivery-time (get-delivery-time order))
            (customer-email (get-customer-email order))
            ;;These values will be calculated when you
            ;;process the order
            (weight 0.0)
            (subtotal 0.0)
            (total 0.0)
            (shipping 0.0)
            (tax 0.0))
         ;;Iterate through each order line, running a function on each line
         ;;to add the weight and price to the weight and price of the order
         (for-each
            ;;Note that anonymous functions created within a function
            ;;have full access to the enclosing function's variables
            (lambda (order-line)
               (set! weight (+ weight (get-weight order-line)))
               (set! subtotal (+ total (get-price order-line))))
            order-lines)
         ;;This uses your shipping calculation function that was passed as a parameter.
         ;;Remember, this could be any number of things, such as calculating from
         ;;a shipping provider's remote database, calculating based on static
         ;;customer-specific tables, or any other method you wish.
         (set! shipping (ship-calc-func weight origin destination delivery-time))
         ;;For this exercise, tax will be considered fairly uniform, so you didn't pass
         ;;a tax-calculating function
         (set! tax (calculate-tax destination subtotal))
         ;;Now record the total
         (set! total (+ subtotal shipping tax))
         ;;Validate the user's credit line, according to the validation parameter
         (if (credit-validate-func total)
            (begin
               ;;Charge the order according to the validation parameter
               (charge-order-func total)
               (send-confirmation customer-email order-lines shipping tax total)
               (mark-as-charged order))
            (begin
               ;;Cancel the order
               (send-failure-message customer-email)
               (mark-as-failed order))))))

関数であるパラメーターは、他のパラメーターとまったく同じように渡されます。プログラムでの使われ方が違うだけです。このテクニックによって、アルゴリズムを一般的な制御の流れとしてコード化でき、しかも、処理の詳細をパラメーター化できます。このテクニックを知らない多くのプログラマーは、処理を制御するフラグを多用します。処理のバリエーションが少ない場合は、この方法でもかまいません。しかし、バリエーションが増えるにつれて煩雑になっていきます。

どの時点で、関数での処理を特殊ケースやフラグで制御するよりも関数をパラメーターとして渡した方がよいかを判断するのは簡単ではありません。しかし、いくつかのガイドラインがあります。

  • オプションが少なく、具体的なときには、特殊ケースの方がよいでしょう。
  • オプションがアルゴリズムときわめて密接に結びついていて、関数がアルゴリズムのローカル変数のほとんどを使用して目的のオプションを生成している場合には、おそらく特殊ケースの方がよいでしょう。
  • 答えを出すために各オプションに必要な変数のセットがまったく異なる場合には、おそらく特殊ケースの方がよいでしょう。
  • オプションが多い場合や、オプションが増える可能性がある場合は、関数をパラメーターとして渡す方がよいでしょう。
  • オプションが非常に明確で、コードから論理的に切り離されている場合は、関数をパラメーターとして渡す方がよいでしょう。

C言語でも、関数をパラメーターとして渡すことができますが、Cで関数のパラメーター仕様を書くのは、やや面倒です。Cは静的な型指定言語です。すなわち、コンパイル時に型がチェックされますが、Schemeはそうではありません。したがって、関数を保持するものとして変数またはパラメーターを宣言するには、その関数が取るパラメーターの型を指定する必要があります。

例えば、すでに述べたmap関数をコード化する方法を見てみましょう。この関数は、やはり整数を返す1つの整数の関数をパラメーターとして取る必要がありました。square関数のプロトタイプは、int square(int x) でした。したがって、このような関数のポインターを保持する変数を宣言するには、次のようなコードを書きます。

リスト7. Cでの関数ポインターの変数宣言
/* This creates a variable called "the_function" which you can store
 * pointers to functions
 */
int (*the_function)(int);

最初のintは、戻り値が整数であることを示します。次の括弧内は、変数のポインタ表記と名前です。関数名の後のもう1つの括弧内は、関数が取る引数の型のコンマ区切りリストです。

関数の代入と使用は、実際には非常に簡単です。Cではすべての関数が実際には関数ポインターだからです。

リスト8. Cでの関数ポインターの操作
int main()
{
   /* Declare function pointer */
   int (*the_function)(int);
   /* assign function pointer */
   the_function = square;
   /* call function */
   printf("The square of 2 is %d\n", the_function(2));
   return 0;
}

Cでの関数ポインターに関するこの知識を使用すれば、Schemeのmap関数のC版を簡単に実装できます。しかし、Schemeでは任意の型のデータとリンク・リンスを扱えますが、C版は整数と整数配列しか扱えないので、機能が限られます。

リスト9. Cでのmap関数の実装
void map(int (*mapping_function)(int), int src[], int dst[], int count)
{
   int i;
   for(i = 0; i < count; i++)
   {
      dst[i] = mapping_function(src[i]);
   }
}

さらに一般的なアプローチとして、「効果的なリスト処理でプログラミングを改善する」で述べたtypetagメカニズムを使用し、マッピング関数、ソース配列、およびデスティネーション配列のすべてをdata型にすることができます。

サードパーティのライブラリを使用するときには、ライブラリに引数として渡される関数は、通常、コールバック関数と呼ばれます。これは、ライブラリからプログラマーのカスタム・コードを逆に呼び出すために使用されるからです。このため、ライブラリ作成者は、さらに具体的な処理のためにプログラマーのコードを逆に呼び出す非常に汎用的な関数を書くことができます。

コールバック関数を使用することによって、プログラマーはソース・コードを直接修正しなくても、ライブラリを拡張することができます。例えば、ほとんどのGUIツールボックスAPIはイベント処理にコールバック関数を使用します。GUI APIに渡されたコールバック関数は、ユーザーが、マウスをクリックしたりボタンを押すなどのイベントでトリガーするまで格納されます。GUI APIは、これらのイベントに応答してプログラムが取るべきアクションを知っているわけではないので、呼び出したプログラムによって与えられたコールバック関数を使用して、アクションの道筋を示すだけです。

パラメーターとして渡すことができる関数を見たところで、さらに一歩進んで、プログラマーによって定義されたテンプレートに従って作成できる関数について説明します。

実行時の関数の作成

Schemeでは、lambdaが関数を作成します。ただし、Schemeでの関数の概念は、他の多くの言語よりも「大きい」です。

その違いを理解するには、まず、Schemeでの変数の扱い方を理解する必要があります。Schemeでは、変数は環境によって分けられます。環境には、ローカル環境とグローバル環境の2つがあります。グローバル環境は、関数のグローバル変数のすべてで構成されます。Schemeでのグローバル環境の扱いは、他のほとんどのプログラミング言語と基本的に同じです。

グローバル環境のほかに、Schemeには多数のローカル環境があります。ローカル環境は、プログラムの特定の時点で可視となるローカル変数と仮パラメーターの集まりです。しかし、ローカル環境は単なる変数の名前以上のものです。変数が参照するストレージ/値でもあります。他のプログラミング言語でのローカル環境と違って、Schemeのローカル環境は保存して後で使用することができます。

では、どのようにしてローカル環境を保存するのでしょう。関数とどのような関係にあるのでしょうか。Schemeでは、関数が定義されるときにローカル環境内で定義された場合、そのローカル環境と関数定義が永続的に結び付けられます。したがって、関数は、関数の作成時にアクティブでありスコープ内にあったすべての変数にアクセスできます。それらの変数を作成した関数が、その後、リターンしていてもアクセスできます。ローカル環境が結び付けられた関数の例を見てみましょう。

リスト10. ローカル環境内で宣言された関数の例
;Environment test is a function taking one argument.  It will then return
;a function which remembers that argument
(define make-echo-function
   (lambda (x)
      ;The local environment now consists of "x"
      ;Create a function which takes no parameters and return it
      (lambda ()
         ;This function returns the x defined in the enclosing
         ;local environment
         x)))
(define echo1 (make-echo-function 1))
(display (echo1)) ;should display 1
(newline)
(define echo2 (make-echo-function 2))
(display (echo2)) ;should display 2
(newline)
(display (echo1)) ;should display 1

(newline)

まず、echo1echo2が関数であることに注目してください。echo1は、グローバル変数を使用せず、パラメーターが渡されなくても、常に1を返します。これは、関数を作成するときにローカル環境が保存されるからです。この例では、ローカル環境はxからなります。したがって、Cなどの言語の関数と違って、make-echo-functionのそれぞれの呼び出しで作成された変数は、make-echo-functionの終了時点を過ぎても有効です。make-echo-functionから返された関数が有効な限り、ローカル環境も有効です。

実際、echo2を定義するときには、make-echo-functionには2つのローカル環境がアクティブなままです。変数名は同じですが、それらの変数の値は違います。lambdaによって関数が作成されるときに環境が結び付けられるので、コンピューターには使用すべき値が分かっています。このような環境と関数の奇妙な結合を閉包といいます。

もう少し複雑な閉包を見てみましょう。特定の数から始まり、呼び出すたびに1だけ多い数を返す関数を作成します。

リスト11. 関数生成関数の例を示すナンバー・カウンター・プログラム
(define make-counter
   (lambda (curval)
      (lambda ()
         (set! curval (+ curval 1))
         curval)))
(define my-counter (make-counter 0))
(display (my-counter)) ;writes 1
(newline)
(display (my-counter)) ;writes 2
(newline)
(define my-other-counter (make-counter 25))
(display (my-other-counter)) ;writes 26
(newline)
(display (my-counter)) ;writes 3
(newline)
(display (my-other-counter)) ;writes 27
(newline)

この種類の関数は、次回の呼び出しまで状態を維持する関数を作成するのに便利です。関数パラメーターと組み合わせると、強力なプログラミング技法となります。Schemeの閉包では関数とともに状態(ローカル環境)も渡すことができるので、パラメーターとして渡される関数をさらに強力にできます。Cなどのプログラミング言語では、引数として渡された関数で使用される状態は、グローバル変数に存在するか、明示的に渡される必要があります。

プロシージャー内で関数パラメーターを使用する利点は、プログラマーが最初に考えていた範囲を超えて拡張できる点にあります。しかし、関数で使用したいデータをすべて明示的に渡さなければならないとすれば、その時点ですでにこの目的にそぐわないことになります。(この問題の解決策については後ほど取り上げますが、その解決策も、ローカル環境を関数に結び付けるというSchemeでの閉包の使用ほどエレガントではありません。)

環境内に関数定義を置くということは、要するに、関数のテンプレートを作成するということです。lambdaフォームが評価されるたびに、そのテンプレートから、現在のローカル環境を使用して未知のスロットを満たす新しい関数が作成されます。

コールバック関数は、閉包を手軽なものにします。APIを書くときに、どのような型のデータを扱うことになるか、すべて分かっていることはまれです。したがって、API作成者には、コールバック関数が機能するためにはどんな種類のデータを渡す必要があるかは分かりません。

閉包では、使いたいデータのすべてをコールバック関数に直接詰め込むことができます。例えば、APIで、ボタンを作成して、そのボタンが押されるたびに呼び出される、パラメーターなしのコールバック関数を使いたいとします。これには問題があるように思えるかもしれません。特に、複数のボタンがコールバック関数を共有しているときにはそうです。APIが何もデータを渡さなければ、どうすれば、どのボタンが押されたのか分かるのでしょうか。

閉包を使用すると、コールバックを有効にするために必要な情報を含んだ環境の中でコールバック関数を定義することができます。呼び出しを受信し、コールバックを呼び出すAPIは、コールバックについて何も知る必要はありません。APIにとっては通常の関数と同じように見えます。

閉包を使用して、他の関数の特別バージョンを作成する関数を作成することもできます。例えば、リストを取り、そのリストからすべての偶数を返すfilter-evensという関数があるとします。この関数は、次のようになります。

リスト12. リストから偶数を抽出する
(define filter-evens
   (lambda (the-list)
      (let
         (
            ;This will hold the filtered elements
            (new-list '()))
         ;Cycle through the elements
         (for-each
            (lambda (entry)
               ;if it's even, add it to new-list
               (if (even? entry)
                  (set! new-list (cons entry new-list))))
            the-list)
         ;return new-list (note that this will be in
         ;reverse order that the items were in the
         ;original list)
         new-list)))
(display (filter-evens '(1 2 3 4 5 6 7 8 9))) ;should display (8 6 4 2)
(newline)

まず、定義して、for-eachに渡す関数は、閉包として使用されることに注目してください。new-listは、エンクロージング環境で定義されています。new-listは、filter-evensが呼び出されるたびに違う値になりますが、リストを作成するためには、for-eachの反復のたびに同じ値が必要になります。

偶数の抽出はよい練習問題ですが、他のもの、例えば奇数や20未満の数を抽出したい場合や、数以外のリスト(オクラホマ州の全住所など)を抽出したい場合は、抽出アルゴリズムの各インスタンスがほとんど同じであっても、基本的にはアルゴリズムを書き直す必要があります。そのため、すでに述べた基準により、抽出選択を行うコードは関数パラメーターを作成する絶好の機会となります。

抽出選択を独自の関数に変えることができます。この関数は、要素がリスト内にあれば真を、そうでない場合は偽を返します。これを関数のパラメーターにすれば、別のフィルターにも応用できます。その方法を以下に示します。

リスト13. 汎用フィルター関数
(define filter
   (lambda (filter-func the-list)
      (let
         (
            ;This will hold the filtered elements
            (new-list '()))
         ;Cycle through the elements
         (for-each
            (lambda (entry)
               ;if it matches the filter function
               ;add it to the result list
               (if (filter-func entry)
                  (set! new-list (cons entry new-list))))
            the-list)
         ;return new-list (note that this will be in
         ;reverse order that the items were in the
         ;original list)
         new-list)))
(define number-list '(1 2 3 4 5 6 7 8 9))
(display (filter even? number-list)) ;should display (8 6 4 2)
(newline)
(display (filter odd? number-list)) ;should display (9 7 5 3 1)
(newline)
(display (filter (lambda (x) (< x 5)) number-list)) ;should display (4 3 2 1)
(newline)

これで、かなり便利な関数ができました。しかし、使うたびに同じことを書く必要がないように、フィルター関数をあらかじめパッケージ化しておくと便利な場合があります。関数パラメーターが長いlambda式の場合は、特にそうです。特化した抽出関数の新規作成を容易にするために、新しい抽出関数を生成する新しい関数を定義できます。この関数をmake-filterと呼ぶことにしましょう(リスト14)。

リスト14. フィルター生成関数
(define make-filter
   (lambda (comparison)
      (lambda (the-list)
         (filter comparison the-list))))
(define filter-evens (make-filter even?))
(define filter-odds (make-filter odd?))
(define filter-under20 (make-filter (lambda (x) (< x 20))))

これらはごく些細な例ですが、さらに複雑な抽出手順を行うときには、make-filterによって時間を短縮し、ミスを減らすことができます。

すでに述べたように、Cプログラミング言語では、閉包が直接はサポートされていません。C言語関数が定義されるときには、データは関連付けられません。静的変数を使用できますが、静的変数の値は一度に1つだけです。閉包では、各閉包に独自の環境が結び付けられるので、プログラマーはそれぞれ独自のローカル環境を持つ関数の複数のインスタンスを使用できます。

本質的には、Schemeの閉包は2つのポインターを含んだ構造体です。1つは関数そのものを指し、もう1つは環境を指します。このため、2つのポインター(関数ポインターと空のポインター)を持つ構造体を使えば、Cで閉包をシミュレートすることができます。空のポインターを使用するのは、環境がどのようなものであるかを心配する必要がないようにするためです。プログラマーに選択の自由を与えるのです。

関数ポインターは、型変換なしで多くの関数をサポートするために、ごく一般的な方法で宣言されます。閉包構造体は、次のようになります。

リスト15. Cでの閉包の定義
typedef void * (*generic_function)(void *, ...);
typedef struct {
   generic_function function;
   void *environment;
} closure;

さまざまな状況で最も一般的な呼び出しインターフェースが使用されるように、generic_function型が作成されています。これにより、閉包内で使用される他の関数型の型変換も簡単になります。環境は空のポインターであり、やはり型変換での使用を容易にするためです。環境のレイアウトの判別は、環境を作成して消費するプログラム部分が受け持ちます。closure構造体は、ポインターを保持するだけです。閉包用APIのなかには、閉包の作成と使用のためのマーシャリング・システム全体を定義しているものもありますが(「参考文献」のリンクを参照)、基本的な実装には、これだけで十分です。

C版のカウンター・プログラムで、これらがどのように機能するかを見てみましょう。

リスト16. Cでのカウンター閉包
#include <stdio.h>
#include <stdlib.h>
/* Closure definitions */
typedef void *(*generic_function)(void *p, ...);
typedef struct {
    generic_function function;
   void *environment;
} closure;
int nextval(void *environment);
/* This is the function that creates the closure */
closure make_counter(int startval)
{
   closure c;
   /* The data is very simple.  You are just storing an int,
      so all you need is a pointer to an int.
    */
   int *value = malloc(sizeof(int));
   *value = startval;
   /* Setup the closure */
   c.function = (generic_function)nextval;
   c.environment = value;
   /* Return the closure */
   return c;
}
/* This is the function that is used for the closure */
int nextval(void *environment)
{
   /* convert the environment data back into the form used
    * by your functions
    */
   int *value = environment;
   /* Increment */
   (*value)++;
   /* Return the result */
   return (*value);
}
int main()
{
   /* Create the two closures */
   closure my_counter = make_counter(2);
   closure my_other_counter = make_counter(3);
   /* Run the closures */
   printf("The next value is %d\n", ((generic_function)my_counter.function)
                                     (my_counter.environment))
   printf("The next value is %d\n", ((generic_function)my_other_counter.function)
                                     (my_other_counter.environment));
   printf("The next value is %d\n", ((generic_function)my_counter.function)
                                     (my_counter.environment));
   return 0;
}

ここではクリーンアップが欠けていますが、「メモリー管理の内側」(「参考文献」を参照)で説明されているガーベッジ・コレクションを使用すれば問題ありません。

多くのAPIは、ここで説明したのとは違うやり方で閉包を行っています。関数ポインターと環境ポインターの両方を含む閉包構造を定義する代わりに、閉包が必要なときに、そのつど、関数ポインターと環境ポインターを関数呼び出しで渡します。いずれにしても、Cで閉包をまねても、渡される環境が複雑になるにつれて、管理が難しくなっていきます。Scheme言語がプログラマーの代わりにすべての作業を実施してくれるのに、これを利用しない手はありません。

では次に、Schemeの閉包とオブジェクト指向言語のオブジェクトの関係を見てみましょう。

関数とオブジェクト指向プログラミング

すぐには分からないかもしれませんが、Schemeの閉包とオブジェクト指向言語のオブジェクトの間には、直接の関係があります。カウンター関数を作成したときのことを思い出してください。どのようなコンポーネントがあったでしょうか。ローカル変数のセットを作成して、それらの変数に(環境に応じて)作用する1つの関数(閉包)を返す関数を作成しました。これに該当するオブジェクト指向プログラミングの部分を見てみましょう。

  • 関数を作成した関数は、コンストラクターとまったく同じように動作します。
  • コンストラクターによって環境内で定義されたローカル変数は、オブジェクトのインスタンス/メンバー変数とまったく同じように振る舞います。
  • 返された関数は、メンバー関数と同じように振る舞います。

唯一欠けているのは、複数のメンバー関数、デストラクター、よりオブジェクト指向な構文を宣言する機能だけです。事実、オブジェクトは、同じローカル環境で定義された関数の集合と考えることができます。あるいは、可能な実装のヒントとして、同じローカル環境で定義された関数のベクトルと呼ぶこともできるでしょう。

先ほどのカウンターがC++などのオブジェクト指向言語ではどのようになるか見てみましょう。

リスト17. クラスとして書き直されたカウンター関数
class Counter
{
   private:
   int value;
   public:
   Counter(int initial_value)
   {
      value = initial_value;
   }
   int nextValue()
   {
      value++;
      return value;
   }
};

もちろん、すでに述べたように、本当のオブジェクト指向プログラミングにするためには、同じ閉包で関数のベクトルを定義できなければなりません。では、やってみましょう。同じカウンター・コードにsetValue()メソッドを追加して、現在値を任意の値に設定します。

リスト18. オブジェクトとして書かれたカウンター関数
(define make-counter
   (lambda (value)
      (vector
         (lambda ()
            (set! value (+ value 1))
            value)
         (lambda (new-value)
            (set! value new-value)
            value))))
(define nextValue (lambda (obj) (vector-ref obj 0)))
(define setValue (lambda (obj) (vector-ref obj 1)))
(define my-counter (make-counter 3))
(display ((nextValue my-counter))) ;displays 4
(newline)
((setValue my-counter) 25) ;now my-counter's value is 25
(display ((nextValue my-counter))) ;displays 26
(newline)

make-counter関数に見られるように、ローカル環境で関数のベクトルを定義するためには、同じ環境で定義された関数が各メンバーであるようなベクトルを定義するだけです。ただし、ベクトルは位置によって参照されるので、ベクトル内の関数を参照するのが困難です。複数のクラスがある場合は特に、ベクトル内の各関数のオフセットを記憶しておくことが悩みの種になるでしょう。例えば、((vector-ref my-counter 0))という呼び出しは非常に分かりにくいのです。したがって、これらのインデックスを自動的に検索するヘルパー関数を定義しました。

このプログラムでは、メソッド呼び出しの前後に余分と思われる括弧があります。余分な括弧が使われているのは、ヘルパー関数は関数を検索するだけで、実際に関数を呼び出すわけではないからです。nextValueはオブジェクトのnextvalue関数を検索するだけです。検索した関数を呼び出す必要があります。2組目の括弧が、実際の呼び出しを行います。この実装では、2つの個別の関数呼び出しになっているので、2段階のプロセスが分かりやすくなっています。

オブジェクト・メソッドを、ワンステップの関数呼び出しで呼び出すように書き直したプログラムを次に示します。

リスト19. 検索ステップと呼び出しステップを組み合わせたオブジェクトとして書かれたカウンター関数
(define make-counter
   (lambda (value)
      (vector
         (lambda ()
            (set! value (+ value 1))
            value)
         (lambda (new-value)
            (set! value new-value)
            value))))
(define nextValue (lambda (obj) ((vector-ref obj 0))))
(define setValue (lambda (obj value) ((vector-ref obj 1) value)))
(define my-counter (make-counter 3))
(display (nextValue my-counter)) ;displays 4
(newline)
(setValue my-counter 25) ;now my-counter's value is 25
(display (nextValue my-counter)) ;displays 26
(newline)

このメカニズムでは、単一継承も可能なことに注目してください。関数の検索は配列索引に基づくので、継承は、互換性のあるクラスとサブクラス内の関数に対して、対応する関数が同じ位置にあることに依存します。多重継承を行う場合は、複数の関数のそれぞれが同じスロットになければなりません。多重継承も(または単なるJava形式の継承でも)可能ですが、別の関数検索メカニズムが必要であり、実装がはるかに難しくなります。

最高階から

では、すばらしいオブジェクト指向言語とSchemeのオブジェクト指向拡張がすでにあるのに、なぜ、オブジェクトを定義するために、このような面倒なことをしなければならないのでしょうか。実はそのようなことはするべきではないのですが、オブジェクトと閉包の基本的な類似性を指摘させていただきたかったのです。実際、プログラマーがSchemeを学んでいるときに、閉包を使用して自分独自のオブジェクト・システムを構築するのは、ほとんど通過儀礼となっています。

閉包とオブジェクトという、ある意味では同じものを、なぜ両方とも使用するのでしょうか。

  • 小さなシステムでは、閉包は、ほぼ間違いなくオブジェクトよりも扱いが容易です。オブジェクトのクラスを作成するためのコードの量に比べれば、ローカル環境で匿名関数を作成するために必要なコードは、本当に小さなものです。
  • しかし、ローカル環境に対応して機能する複数の関数(おそらく3つか4つ以上)を定義する必要があるときには、オブジェクトの方が効果的です。

両方のシステムの仕組みが分かったので、これからは、使用中の言語の一方が欠けている場合は、あるものを利用して、ないものをシミュレートすることができます。


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


関連トピック

  • Teach Yourself Scheme in Fixnum Daysは、Scheme言語に関する素晴らしいオンライン・チュートリアルです。
  • The Function Pointer Tutorialsサイトでは、C/C++での関数ポインターの使用に関して、素晴らしい議論が行われています。
  • 最近のプログラミング手法をマスターした人であっても、How to Design Programsでコンピューター・サイエンスを再度学習してみると、関数やファンクショナル・プログラミングの表現力にあらためて目を見張るでしょう。
  • Essentials of Programming Languagesの第5章では、オブジェクト指向Schemeの実装として4つの異なる方式を、それぞれの長所短所の詳しい説明を含めて解説しています
  • SRFI 1には、便利な関数がリストに挙げるほど大量に含まれています。その多くは関数パラメーターを使って処理を拡張しています(SRFIは、Schemeに対するRFCのようなものです)。
  • Gtk+ 2.0 APIには、あらゆる閉包APIが含まれています。これらは、よく調べてみるだけの価値のあるものです。
  • メモリー管理の内側(developerWorks, 2004年11月)は、メモリー管理がどのように動作するかの詳細を解説しています。メモリーを手動管理する方法、参照カウントやプールを使って半手動で管理する方法、ガーベジ・コレクションを使って自動管理する方法などが紹介されています。
  • 効果的なリスト処理でプログラミングを改善する(developerWorks, 2005年1月)は、単一リンク・リストとそのプロセスをハイライトし、Schemeを検証しています。
  • Functional programming in the Java language(developerWorks, 2004年7月)は、閉包や高階関数などのファンクショナル・プログラミング構成体を使って、適正な構成を持つモジュラーなコードをJavaで書くための方法を解説しています。
  • 魅力的なPython: Pythonでの関数プログラミング: 第1回(developerWorks, 2001年3月)は、ファンクショナル・プログラミングに関する基本的な概念を紹介し、Pythonでファンクショナル・プログラミング手法を実装する方法を解説しています。
  • 魅力的なPython:Pythonでの関数プログラミング: 第2回(developerWorks, 2001年4月)はPythonでの閉包を解説しています。
  • 魅力的なPython: Pythonでの関数プログラミング: 第3回(developerWorks, 2001年6月)は、カレー化を含めた高階関数を解説しています(カレー化は論理学者Haskell Curryの名前からとられたものです。カレー化を使うと、関数自身への戻り値を関数にすることができますが、戻る関数は「限定されたものに」なります)。
  • developerWorksのLinuxゾーンには、他にもLinux開発者のための資料が豊富に用意されています。

コメント

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=228550
ArticleTitle=高階関数
publish-date=03312005