コールバック (情報工学)

背景

コールバックを使う意義を理解するため、連結リスト上の各要素に対して様々な処理を行うという問題を考える。ひとつの手法として、リスト上でのイテレータで各オブジェクトについて処理をするという方法がある。これは実際、最も一般的な手法だが、理想的な方法というわけではない。イテレータを制御するコード（例えば for 文）はリストを辿る処理が存在すると、その度に複製が必要となる。さらに、リストの更新が非同期プロセスで行われている場合、イテレータでリストを辿っている間に要素を飛ばしてしまったり、リストを辿れなくなったりする可能性がある。
代替手法として、新たにライブラリ関数を作り、適当な同期を施して必要な処理を行うようにする。この手法でもリストを辿る必要が生じる度に同様の関数を呼び出す必要がある。この方式は様々なアプリケーションで使われる汎用ライブラリにはふさわしくない。ライブラリ開発ではあらゆるアプリケーションのニーズを予測することはできないし、アプリケーション開発ではライブラリの実装の詳細を知る必要がないのが望ましい。
コールバックが、この問題の解決策となる。リストを辿るプロシージャを書くとき、そのプロシージャがアプリケーションが各要素についての処理を行うコードを提供するようにする。これにより、柔軟性を損なわずに明確にライブラリとアプリケーションを区別することができる。
コールバックは実行時束縛の一種と見ることもできる。

例

以下のC言語コードは、配列を検索して 5 より大きい値を探す処理を行うものである。まず、イテレータを使ったコードを示す。
 int i;
 for (i = 0; i < length; i++) {
     if (array[i] > 5) { 
         break;
     }
 }
 
 if (i < length) {
     printf("Item %d\n", i);
 } else {
     printf("Not found\n");
 }
次に、コールバックを使ったコードを示す。
 /* ライブラリコード */
 int traverseWith(int array[], size_t length, 
                  int (*callback)(int index, int item, void *param), 
                  void *param)
 {
     int exitCode = 0;
     for (int i = 0; i < length; i++) {
         exitCode = callback(i, array[i], param);
         if (exitCode) { 
             break;
         }
     }
     return exitCode;
 }
  
 /* アプリケーションコード */
 int search (int index, int item, void *param)
 {
     if (item > 5) {
         *(int *)param = index;
         return 1;
     } else {
         return 0;
     }
 }
 
 /* ライブラリを呼び出す本体 */
 int index;
 int found;
 found = traverseWith(array, length, search, &index);
 if (found) {
     printf("Item %d\n", index);
 } else {
     printf("Not found\n");
 }
コールバック関数 search の if 文の条件を変更すれば、「5より大きい」以外の要素を検索するのにも使える。traverseWith には、コールバックが自身の目的のために受け取る追加の引数 param がある点に注意されたい。通常のコールバックでは、そのような引数をスコープ外のアプリケーションデータへのポインタに利用する。これは静的スコープ方式の言語（C や C++）でのみ必要とされる（ただし、C++ を含めたオブジェクト指向言語には別の解決策がある）。動的スコープの言語（関数型言語など）ではクロージャによって自動的にアプリケーションデータへのアクセスが可能となる。例として同じプログラムを LISP で書いた場合を示す。
 ; ライブラリコード
 (defun traverseWith (array callback)
   (let ((exitCode nil)
         (i 0))
     (while (and (not exitCode) (< i (length array)))
       (setq exitCode (callback i (aref array i)))
       (setq i (+ i 1)))
     exitCode))
 
 ; アプリケーションコード
 (let (index found)
   (setq found (traverseWith array (lambda (idx item)
                                     (if (<= item 5) nil
                                       (setq index idx)
                                       t)))))
この場合、コールバック関数は使う時点で定義されており、"index" を名前で参照している。これらの例では同期に関する考慮は省略されているが、traverseWith 関数を同期できるように対処するのは容易である。さらに重要なことは、同期するかしないかをその関数の修正だけで対処できる点である。

特殊な例

コールバック関数は、例外処理を実現する手段としてもよく使われ、状況によって副作用を伴う処理を可能としたり、何らかの処理途中の情報を収集するのに使われたりする。割り込みハンドラは、オペレーティングシステム (OS) でハードウェアの何らかの状況に対応するのに使われる。また、シグナルハンドラはアプリケーションが OS に登録し、OS が呼び出す。イベントハンドラは、プログラムが受信した非同期的な入力を処理する。
副作用のないコールバック関数を「純粋コールバック関数; pure callback function」と呼ぶ。場合によっては、純粋コールバック関数が必要とされることもある。
特殊なコールバックとして「述語コールバック; predicate callback」がある。これは純粋コールバック関数の一種で、引数は1つだけで、リターン値はブーリアン型である。これは、データの集まりからある条件に適合するものだけを選別するときに使われる。
イベント駆動型プログラミングでは、Observer パターン的な方式がよく使われ、マルチキャスト型のコールバックが可能となっている。この場合、コールバックは予め登録され、対応するイベントが発生したときに呼び出される。プログラミング言語によっては、この機構を直接サポートしている場合もある（例えば、.NET の delegate、Qt の signal と slot）。