『Collection Pipeline』を読んだ

Collection Pipeline – martinfowloer.com

本ではなくWeb記事だけど、面白かったので読書メモ。

コレクションパイプラインとは

コレクション(配列、リスト等)に対する操作をつなげて書くプログラミンスタイル。たとえば、記事のリストから、「JavaScript」をタイトルに含む記事の総語数を取得したい場合、JavaScriptでは以下のように書ける。

articles.filter(a => a.title.includes('JavaScript'))
  .map(a => a.words)
  .reduce((p, c) => p + c, 0);

初めに記事のリストをフィルタリングし、次に語数の配列へ変換してから、語数を合計している。

これをループを使って書くとこうなる。

let result = 0;
for (let a of articles) {
  if (a.title.includes('JavaScript')) {
    result += a.words;
  }
}

ループの方が決定的に劣るということはないが、コレクションパイプラインを使うと配列に対する操作を宣言的に書くことができる。

コレクションパイプラインは、無名関数(言語によってはラムダ式などともいう)を多用することから、関数型のパラダイムや、関数型っぽい機能といわれることも多い(実際、僕もそう思っていた)。

しかし、本記事によると、コレクションパイプラインの発祥はSmalltalkであるらしい。

Smalltalkでは、コレクションに対する操作を以下のように書ける。構文は独特だが、コレクションに対する操作を連結するスタイルは同じ。

 ((someArticles 
      select: [ :each | each tags includes: #nosql])
      sortBy: [:a :b | a words > b words]) 
      copyFrom: 1 to: 3

一方、初期の代表的な関数型言語であるLispでは、こうなる。

(subseq
   (sort
      (remove-if-not
         (lambda (x) (member 'nosql (article-tags x)))
         (some-articles))
      (lambda (a b) (> (article-words a) (article-words b))))
 0 3)

素朴な関数型のスタイルでは、関数呼び出しが実際の処理の流れとは逆順になる点が、Smalltalkとは異なる。この点、モダンな関数型言語(Closureなど)では、関数呼び出しをシーケンシャルに書く機能が用意されていることが多い。

(1) コレクションのシーケンシャルな操作を連鎖させるというプログラミングスタイルは、初期のオブジェクト指向言語であるSmalltalkから生まれた

(2) 無名関数(ラムダ式)は関数型言語の専売特許ではない(C++、Javaといった代表的なオブジェクト指向言語がラムダ式を採用しなかっただけ)

というのが個人的に面白いと思ったポイント。

Collection Pipelineを避けるべき状況

(1) 言語がサポートしていない場合

本記事では、ラムダ式がなかった時代のJavaが例に挙がっている。他にも、Goにはラムダ式はあってもコレクションパイプラインは使えない。このような言語で無理してコレクションパイプラインを使うと、混乱の元になる。

(2) リスト内包表記を使える場合

Pythonなどの言語には [i for i in range(10) if i%2==0] のように、リスト内でコレクション操作を行える機能が備わっている。

リスト内包表記が使える場合は、 コレクションパイプラインではなくリスト内包表記を使った方が、短く読みやすいコードを書けることがある。

(3) コレクションに対する操作が複雑な場合

コレクションパイプラインで実装できる操作であっても、それが10行以上になるような複雑な操作であった場合、単一のコレクションパイプラインとして実装するのではなく、関数を抽出して、細かいステップに分けるべき。

関連書籍:Refactoring, 2nd Edition

Refactoring: Improving the Design of Existing Code (2nd Edition) (Addison-Wesley Signature Series (Fowler))

コメントをどうぞ

コメントを残す