Hatena::ブログ(Diary)

oct inaodu

 | 

2006-01-31

関数、オブジェクト、クロージャ

(thanks to id:koyachidel.icio.us/rtk2106)


OOPとFPと。

関数オブジェクトクロージャの使い分けについて考えます。


関数型が良いのか、オブジェクト指向が良いのか、知りたいと思っていました。

色々なページを読み、現時点で一応の答えを得ました。


カウンタを例にして、関数、スコープ、オブジェクトクロージャの順に見て行きます。

関数

関数は処理です。入力と出力があります。

関数型プログラミングでは、関数同士の入力と出力を連結しプログラムが構成されます。

var current = 0;

function next(v){ return v + 1 }

function previous(v){ return v - 1 }

ok( 1 == ( current = next(current) ) );
ok( 2 == ( current = next(current) ) );
ok( 1 == ( current = previous(current) ) );

関数は純粋に処理のみを提供し、変数は外部に切り離されて保存されます。

Counter.nextのようにクラスメソッドとして、別の名前空間に置かれることもありますが、本質的には単なる関数です。


関数は、データと処理を外部のロジックが引き合わせる必要があります。

データと処理が密接な関係を持つ場合は、お互いが予めくっついているオブジェクトの方が良いです。


  • 長所

入力と出力のみに気を配り処理を作成すればよいため、テストがしやすく、品質が高くなります。安く書けます。

その関数が知らないうちに、依存しているデータが書き換わっていることはありません。


関数の外部とはインターフェイスにより、きっぱりと隔てられているため、移植性と再利用性が高くなります。


スコープ

ちょっと処理から脱線して、スコープについて。

スコープはデータが保存されている場所です。


JavaScriptでは、関数ごとにスコープが分かれています。

ifブロックや、forブロックなど、ブロックごとのスコープはありません。

var scope = "GLOBAL";

(function local_1(){
  var scope = "LOCAL_1";
  
  (function local_2(){
    var scope = "LOCAL_2";
    
    ok(scope == "LOCAL_2");
  })()
  
  ok(scope == "LOCAL_1");
})()

ok(scope == "GLOBAL");

関数の中に関数を定義することで、入れ子のスコープを作れます。

入れ子のスコープ同士は結びつけられ、スコープチェーンを構成します。スコープの中で変数に対応する値が見つからない場合は、チェーンの中の一つ上のスコープで値が探されます。


オブジェクト

オブジェクトは、データに処理がくっついたものです。

オブジェクトに付いた関数は、メソッドと呼ばれます。

function Counter(){
  this.current = 0;
}

Counter.prototype.next = function(){
  return ++this.current;
}

Counter.prototype.previous = function(){
  return --this.current;
}

var counter = new Counter();

ok( 1 == counter.next() );
ok( 2 == counter.next() );
ok( 1 == counter.previous() );

Counterは、関数の時よりも、断然使いやすそうですね。


一つのスコープへ、複数の処理がアクセスするため、動きが複雑になり、作るのにお金がかかります。

関数と比べると、テストも難しいです。


  • 長所

処理とデータが一まとまりになり、外部のプログラムへ使いやすいインターフェースを提供できます。

また、状態を持つ物体としてイメージできるため、抽象的に考えやすくなります。


型が無い言語では、処理の引数の型を制限する働きもあります。

StringクラスのtoLowerCaseメソッドは、スコープを使わず、Stringオブジェクトの状態を変化させないため、単なる関数でも充分です。

しかし、Stringオブジェクトにくっつき、そこからのみ呼び出せる状態にすることで、異物が引数に入るのを防いでいます。


オブジェクトには、名前空間を区切る働きもあります。

名前空間を分けることで、同じ名前の関数を複数の場所で作ることができます。

また、たくさんの処理を分類し整理できるため、管理が楽になります。


クロージャ

クロージャは、処理にデータがくっついたものです。

クロージャは、オブジェクトの裏返しと言えます。


下のサンプルでは、カウントする関数に、生成元のcreateCounterのスコープがくっついています。

function createCounter(){
  var current = 0;
  
  return function(){
    return ++current;
  }
}

var counter = createCounter();

ok( 1 == counter() );
ok( 2 == counter() );
ok( 3 == counter() );

オブジェクトは、通常new演算子により、コンストラクタが呼び出され生成されます。

対してクロージャは、関数を生成するファクトリ関数により生成されます。


クロージャは、「処理が一つしかないオブジェクト」と考えることもできます。

ちょこっとだけ簡便的に利用するためのワンショットオブジェクトとも考えられます。


クロージャは、リッチな機能は提供できません。

カウンタのサンプルでは、オブジェクトの時にあった、前へ戻る機能が無くなっています。


  • 長所

オブジェクトに比べ、コードが短くなります。

ただ呼び出すだけの、シンプルなインターフェースを外部へ提供できます。

つまり、簡単なことが、簡単にできます。


まとめ

スコープと、関数オブジェクトクロージャの関係を、「大まかに」まとめると以下のようになります。



スコープあたりの処理の数で、クロージャオブジェクト関数を分類できます。


分類スコープの数処理の数
クロージャ1つのスコープ1個の処理
オブジェクト1つのスコープ複数の処理
関数スコープなし1個の処理

ここまでを基にすると、こんな具合に関数オブジェクトクロージャの使い分けを考えられます。


カウンタは、スコープがあり状態を保持する必要があるため、関数は向いてないな。

簡単なカウンタだったらクロージャで充分で、リッチなカウンタだったらオブジェクトが便利だな、と考えられます。


コントロールに色を付け、徐々に色が薄くなっていくような処理だったらどうでしょうか?

必要な処理は、色を薄くする処理の一つだけなのでオブジェクトは使う必要はなさそうです。

クライアントにストップなどの処理を一つ渡す必要があるならクロージャ、そうではなくスタートだけなら関数で充分だと思います。


クロージャで良いところへオブジェクトを持ってくるのは大げさです。

複雑な状態を持つものを関数クロージャで書くのも無理があります。

それぞれのメリットを生かし、適材適所でミックスして使いプログラミングをするのが良いと思っています。


背景

上記までで、ひとまとめです。

全体的に、大幅な省略を行なっているため、不正確な部分がたくさんあります。

しかしその分、抽象的に考える上で、大事な部分ははっきりしたと思います。


私は、今まで(今でも)関数型プログラミングを知らなかったため、多くの誤解をしていました。

関数型の世界では、プログラムは全て関数で書かれ、クラスやオブジェクトを使うのは良くないことなのかな、と漠然とイメージしていました。


しかしMochikitをさわり、Pythonの記事を読み、これが間違っていることに気が付きました。

Mochikit.Iterは、関数型のスタイルで繰り返しを構成します。

しかし、イテレータは複数のメソッドを持つオブジェクトですし、繰り返しを止めるために例外オブジェクトも使用します。

つまり、オブジェクトと、関数がミックスされて、繰り返しのフレームワークが構成されているのです。


やはり、考えの軸になっているのはmap関数の置き所です。

map(array)なのかarray.map()なのか、関数なのかメソッドなのか、Prototype.jsなのかMochikitなのか、RubyなのかPythonなのかです。

mapは、スコープがいらないため、関数で実装できます。関数はテストが容易で、予想外の動きが少ないため、安く書けます。

map(array)は、ダックタイピングの考えに基づき、配列に似ているが配列ではないオブジェクト引数に取ることができます。array.map()の場合は、似ているだけのオブジェクトは一度配列へ変換する必要があります。

array.map()のように、後に後に処理を追加していく書き方は、順にコードを追えるため読みやすく、また書きやすいです。

しかし、array.map().inject().sortBy()と、ドット演算子でメソッドを続けて記述する場合は、一つ一つのメソッドは必ず結果を確定させる必要があります。map(array)の方式では、遅延評価を用い、関数同士でストリームを形成し効率よく処理を行なえるケースがあります。


上記に加えて、その他の多くの要因(サーバーフレームワークとの親和性、開発者のスキル、開発アプリケーションの特性)を絡めてトレードオフを行い、最も安くソフトウェアを作れる場所にmap関数が置かれることになるでしょう。

私も、今の自分の状況に合わせて、map(array)と、array.map()、どちらが良いのかを自分なりに決めてみました。


FPか、OOPか。

これは白黒はっきりとした選択では決してなく、両方の損得を理解したうえで中間点を決めるような度合いの問題だと感じています。


参考リンク

Schemeオブジェクトを書き、Cでクロージャを作る。

Schemeを学んでいく中で、クロージャで独自のオブジェクト・システムを作ることになるのが通例。スコープに対して複数の関数があるときは、クロージャよりもオブジェクトの方が効果的。」


オブジェクトとは、処理が付加されたデータのことです。クロージャーとは、データが付加された処理のことです。クロージャーとは、OOPでのハイドに対するFPのジキルのようなものです。」

関数プログラミングオブジェクト指向プログラミングは、 結局同じところに落ち着くのです。」


Lispも良いが、Googleに行きPythonの素晴らしさに出会った。GoogleにはPeter Norvigもやって来たが、彼もまたLispを捨て去っていた。」


Peter Norvig。LispPythonの比較。「Pythonは実用的なSchemeか、きれいなPerl。」


オブジェクト関数の両方を第一級の型として持つのが言語にとって良い。」

arinoarino 2006/01/31 19:00 function createCounter(){
var current = 0;
return { next: function()
{ return current++; },
previous: function() { return current--; }
};

}

みたいな例を考えると少し直行的に捉えすぎているような気がしますがいかがでしょう?
違いをはっきりさせる為にあえてそうしてるという部分もあるでしょうけど、例えば「オブジェクトを作るのに積極的にレキシカルスコープを使うべきか?」という問いには答えがでづらい捉え方な気がします。

zz 2008/02/25 01:56 一見クロージャを説明できているように感じますが、
実は、文科系出身者がそのままウェブ作成の道を歩んで、辿り着いた発想だと思います。クロージャに付いては、個人的には全く違ったイメージを持ってるんですけど、もしかしてイテレーターの延長とか何かで捉えてませんか?

brazilbrazil 2008/02/25 13:11 いや知らん。お前のイメージを自分のブログに書けや。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

 |