Java.next: シノニムのややこしさを克服する

Java.net 言語間で同様の関数型構成体を認識する

前回の Java.next の記事 (「関数型のコーディング・スタイル」) では、Scala、Groovy、Clojure といった 3 つの言語での関数型コーディング・スタイルを比較対照しました。今回の記事では、連載の著者である Neal Ford が Java.next 言語でのフィルター関数、マップ関数、および集約関数について、さらに深く掘り下げます。3 つの言語では、これらの重要な関数型構成体にさまざまな名前が付けられていますが、この若干ややこしい言語間での違いを整理できるように、一連の簡潔なコーディング・サンプルを記載します。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks

Photo of Neal fordNeal Ford は世界的な IT コンサルティング企業 ThoughtWorks のディレクターであり、ソフトウェア・アーキテクトであり、Meme Wrangler でもあります。また彼は、アプリケーション、教育資料、雑誌記事、コースウェア、ビデオや DVD によるプレゼンテーションなどの設計と開発も行っています。さまざまな技術に関する本の著者、編集者でもあり、最新の著作は『Presentation Patterns』です。彼は大規模なエンタープライズ・アプリケーションの設計や構築を専門にしています。また彼は世界各地で開催される開発者会議での講演者としても国際的に有名です。彼の Web サイトをご覧ください。



2014年 5月 15日

この連載について

Java の遺産となるのは、プラットフォームであって、言語ではないでしょう。200 を超える言語が JVM 上で実行されている今、最終的にこれらの言語の 1 つが JVM のプログラミングに最適な方法として Java 言語に取って代わることは避けられません。この連載では、Java 開発者が自分たちの近い将来を垣間見ることができるように、3 つの次世代 JVM 言語 ― Groovy、Scala、Clojure ― について、新しい機能やパラダイムを比較対照することで、詳しく探ります。

関数型プログラミング言語には、共通する関数ファミリーがいくつかあります。けれども、馴染み深いはずの関数に馴染みのない名前が付けられているために、開発者がある言語から別の言語に切り替えるのに苦労することがあります。関数型言語ではこれらの共通する関数に、関数型パラダイムに基づく名前を付ける傾向があります。一方、スクリプト言語から派生した言語では、説明的な名前を使用する傾向があり、同じ関数を指す複数の名前をいくつかのエイリアスとともに使用する場合もあります。

今回の記事では引き続き 3 つの重要な関数 (フィルター、マップ、集約) の有用性を検討し、3 つの Java.next 言語それぞれでの実装の詳細を明らかにします。この記事の説明とサンプル・コードは、3 つの Java.next 言語で同様の関数型構成体に付けられている名前に一貫性がないことで生じる混乱を軽減するのを目的として作成されています。

フィルター

フィルター関数には、コレクションに適用するブール型のフィルター基準を (通常は、高階関数の形で) 指定することができます。すると関数から、その基準を満たす要素からなるコレクションのサブセットが返されます。フィルタリングは、コレクション内で最初に一致した要素を返す、検索系の関数と密接に関係します。

Scala

Scala には、フィルター関数のバリエーションが数多くあります。最も単純なものは、渡された条件に基づいてリストをフィルタリングします。この最初の例では、まず数値のリストを作成します。次に、filter() 関数を適用して、すべての要素は 3 で割り切れなければならないという条件を指定したコード・ブロックを渡します。

val numbers = List.range(1, 11)
numbers filter (x => x % 3 == 0)
// List(3, 6, 9)

暗黙パラメーターを使用すれば、上記のコード・ブロックをさらに簡潔にすることができます。

numbers filter (_ % 3 == 0)
// List(3, 6, 9)

この 2 番目のバージョンが簡潔になっている理由は、Scala ではパラメーターをアンダースコアで置き換えることができるためです。どちらのバージョンも、結果は同じです。

フィルタリング処理では、数値を使用する例が多いとは言え、filter() はどのような要素で構成されるコレクションにも適用することができます。以下は、単語のリストに filter() を適用して、3 文字からなる単語を判別する例です。

val words = List("the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog")
words filter (_.length == 3)
// List(the, fox, the, dog)

Scala でのフィルター関数のバリエーションには、partition() 関数もあります。この関数は、コレクションを分割して複数の部分に変更する関数です。分割は、分離基準を決定するために渡す高階関数に基づいて行われます。以下の partition() 関数は、3 で割り切れるリスト・メンバーに応じて分割された 2 つのリストを返します。

numbers partition (_ % 3 == 0)
// (List(3, 6, 9),List(1, 2, 4, 5, 7, 8, 10))

filter() 関数は一致する要素のコレクションを返すのに対し、find() 関数は、以下のように最初に一致した要素だけを返します。

numbers find (_ % 3 == 0)
// Some(3)

ただし、find() の戻り値は一致した値そのものではなく、Option クラスにラップされた値です。Option の値は、Some または None のいずれかになります。Scala は、他の関数型言語と同じように、値がない場合に null が返されないようにするための慣例として Option を使用します。Some() インスタンスには、実際の戻り値がラップされます。つまり、numbers find (_ % 3 == 0) の場合には 3 がラップされます。存在しない要素を検索しようとすると、戻り値は None になります。

numbers find (_ < 0)
// None

Scala にはその他、述部関数に基づいてコレクションを処理し、値を保持または破棄する関数もいくつかあります。例えば、takeWhile() 関数は、コレクションの先頭から述部関数を満たす最初の要素までのすべての要素を返します。

List(1, 2, 3, -4, 5, 6, 7, 8, 9, 10) takeWhile (_ > 0)
// List(1, 2, 3)

一方、dropWhile() 関数は、述部関数を満たす最初の要素までのすべての要素をスキップします。

words dropWhile (_ startsWith "t")
// List(quick, brown, fox, jumped, over, the, lazy, dog)

Groovy

Groovy は関数型言語と見なされていませんが、この言語には多くの関数型パラダイムがあります。そのうちのいくつかには、スクリプト言語から派生した名前が付けられています。例えば、関数型言語で従来から filter() という名前が付けられている関数は、Groovy では findAll() メソッドに相当します。

(1..10).findAll {it % 3 == 0}
// [3, 6, 9]

Scala のフィルター関数と同様、Groovy のフィルター関数もすべての型で有効です。これには、以下のようにストリングも含まれます。

def words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"]
words.findAll {it.length() == 3}
// [The, fox, the, dog]

Groovy にも、split() という、partition() 関数のようなメソッドがあります。

(1..10).split {it % 3}
// [[1, 2, 4, 5, 7, 8, 10], [3, 6, 9]]

split() メソッドの戻り値は、Scala の partition() から返されるネストされたリストと同様に、ネストされた配列です。

Groovy の find() メソッドは、コレクションで最初に一致した要素を返します。

(1..10).find {it % 3 == 0}
// 3

Scala とは異なり、Groovy は Java の慣例に従い、find() で要素が見つからないときには null を返します。

(1..10).find {it < 0}
// null

また、Groovy にも、Scala での場合と同様の動作をする takeWhile() メソッドと dropWhile() メソッドがあります。

[1, 2, 3, -4, 5, 6, 7, 8, 9, 10].takeWhile {it > 0}
// [1, 2, 3]
words.dropWhile {it.startsWith("t")}
// [quick, brown, fox, jumped, over, the, lazy, dog]

Scala の例と同じく、dropWhile は特殊化されたフィルターとして機能し、リストの最初の部分だけをフィルタリングして、述部と一致する最初の要素までのすべての要素を破棄します。

def moreWords = ["the", "two", "ton"] + words
moreWords.dropWhile {it.startsWith("t")}
// [quick, brown, fox, jumped, over, the, lazy, dog]

Clojure

Clojure には、コレクションを操作するルーチンが驚くほど多くあります。その大部分は、Clojure の動的型付けのおかげで、かなり汎用的に適用できます。多くの開発者が Clojure に魅き付けられる理由は、そのコレクション・ライブラリーの豊富さと柔軟性にあります。Clojure は、以下の (filter ) 関数で示されているように、従来の関数型プログラミングでの名前を使用します。

(def numbers (range 1 11))
(filter (fn [x] (= 0 (rem x 3))) numbers)
; (3 6 9)

他の 2 つの言語と同じく、Clojure では単純な匿名関数を簡潔にする構文を使用できます。

(filter #(zero? (rem % 3)) numbers)
; (3 6 9)

他の 2 つの言語と同じく、Clojure には単純な匿名関数のための簡潔な構文があります。

(def words ["the" "quick" "brown" "fox" "jumped" "over" "the" "lazy" "dog"])
(filter #(= 3 (count %)) words)
; (the fox the dog)

これも他の 2 つの言語と同様ですが、Clojure の関数は、ストリングを含め、適用可能なあらゆる型に対して機能します。

Clojure の (filter ) の戻り値の型は、括弧で括られた Seq です。Seq は、Clojure においてコアとなる、シーケンシャル・コレクションの抽象概念です。


マップ

すべての Java.next 言語に共通する関数型の主な変換機能の 2 つ目はマップです。マップ関数は、高階関数とコレクションを引数に取り、渡された関数をコレクション内の各要素に適用してから、コレクションを返します。返されたコレクションのサイズは (フィルタリングとは異なり) 元のコレクションと同じですが、コレクション内の値が更新されています。

Scala

Scala の map() 関数は、コード・ブロックを引数に取り、変換した形でコレクションを返します。

List(1, 2, 3, 4, 5) map (_ + 1)
// List(2, 3, 4, 5, 6)

map() 関数は、適用可能なあらゆる型に対して機能しますが、必ずしもコレクションに含まれる要素を変換したコレクションを返すとは限りません。以下の例は、ストリングに含まれるすべての要素のサイズのリストを返します。

words map (_.length)
// List(3, 5, 5, 3, 6, 4, 3, 4, 3)

関数型プログラミング言語では、ネストされたリストが頻繁に出現するため、ネストの解除 (一般にフラット化と呼ばれます) をライブラリーでサポートするのが一般的となっています。以下は、ネストされたリストをフラット化する例です。

List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9)) flatMap (_.toList)
// List(1, 2, 3, 4, 5, 6, 7, 8, 9)

生成される List には要素だけが含まれ、余分なものは取り除かれます。flatMap 関数は、従来の形ではネストされているようには見えないデータ構造にも機能します。例えば、ストリングは、一連のネストされた文字とみなすことができます。

words flatMap (_.toList)
// List(t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, ...

Groovy

Groovy にも、collect() と呼ばれるいくつかのマップ系メソッドが含まれています。デフォルトのマップ系メソッドは、コレクション内の各要素に適用されるコード・ブロックを引数に取ります。

(1..5).collect {it += 1}
// [2, 3, 4, 5, 6]

他の 2 つの言語と同じように、Groovy では単純な匿名高階関数に対して省略形 it を使用できます。この予約語が、長いパラメーターの代わりを務めます。

collect() メソッドは、あらゆるコレクションに適用することができ、そのコレクションに合った述部 (例えば、ストリングのリストなど) を指定することができます。

def words = ["the", "quick", "brown", "fox", "jumped", "over", "the", "lazy", "dog"]
words.collect {it.length()}
// [3, 5, 5, 3, 6, 4, 3, 4, 3]

Groovy にも、flatMap() と同じように内部の構造をフラットにする flatten() というメソッドがあります。

[[1, 2, 3], [4, 5, 6], [7, 8, 9]].flatten()
// [1, 2, 3, 4, 5, 6, 7, 8, 9]

flatten() メソッドは、ストリングなどの自明ではないコレクションにも適用することができます。

(words.collect {it.toList()}).flatten()
// [t, h, e, q, u, i, c, k, b, r, o, w, n, f, o, x, j, ...

Clojure

Clojure には、(演算子を含む) 高階関数とコレクションを引数に取る (map ) 関数があります。

(map inc numbers)
; (2 3 4 5 6 7 8 9 10 11)

(map ) の最初のパラメーターには、引数を 1 つ取る任意の関数を指定することができます。例えば、名前付き関数、匿名関数、あるいは引数をインクリメントする inc などの既存の関数です。以下の例に、一般的な匿名関数の構文を示します。この匿名関数は、ストリングに含まれる単語の長さのコレクションを生成します。

(map #(count %) words)
; (3 5 5 3 6 4 3 4 3)

Clojure の (flatten ) 関数は、Groovy の flatten メソッドと同様です。

(flatten [[1 2 3] [4 5 6] [7 8 9]])
; (1 2 3 4 5 6 7 8 9)

畳み込み/集約

3 つの Java.next 言語に共通するこの 3 つ目の関数は、言語による名前の違いが最も顕著であり、言語ごとに微妙な違いがさまざまにあります。foldLeft と reduce は、リストの畳み込み操作を一般化した catamorphism と呼ばれるリスト操作の概念に、ある特定の変形を加えたものです。この場合、「fold left」は、以下の意味を持ちます。

  1. 二変数関数または二項演算子を使用して、リストの最初の要素を 2 番目の要素と結合することによって新しい最初の要素を生成します。
  2. リストのすべての要素に対して処理が完了するまでステップ 1 を繰り返します。これにより、最終的には 1 つの要素だけが残されます。

これは、数値のリストを合計するときとまったく同じことを行っていることに注目してください。つまり、ゼロから始めて、最初の要素をそこに加算し、その結果を取って次の要素に加算するという処理を、リストの最後まで続けます。

Scala

畳み込み操作が最も充実しているのは、Scala です。これは一部に、Scala は動的に型付けされる Groovy や Clojure にはない型付けのシナリオを容易にしているためです。合計を実行するには、一般的に集約が使用されます。

List.range(1, 10) reduceLeft((a, b) => a + b)
// 45

reduce() に含める関数は、一般に、2 つの引数を取って 1 つの結果を返すという方法でリストを処理する、関数または演算子です。Scala の構文糖を使用すれば、以下のように関数定義を短縮することができます。

List.range(1, 10).reduceLeft(0)(_ + _)
// 45

reduceLeft() 関数は、演算子の左側にある要素を最初の要素であると見なします。加算などの演算子の場合、オペランドの配置による影響はありません。ただし、除算などの演算では、順番が関係してきます。演算子を逆順で適用する場合は、reduceRight() を使用します。

List.range(1, 10) reduceRight(_ - _)
// 5

集約のような上位レベルの抽象化をどのような場合に適用できるかを理解することが、関数型プログラミングをマスターする鍵の 1 つです。以下の例では、reduceLeft() を使用して、コレクション内で最も長い単語を判別します。

words.reduceLeft((a, b) => if (a.length > b.length) a else b)
// jumped

集約操作と畳み込み操作の機能には、重複するところがありますが、この 2 つの操作には微妙な違いがあります。その点については、この記事では説明しませんが、明らかに異なる操作でも共通する使い方をする場合があります。例えば、Scala における reduceLeft[B >: A](op: (B, A) => B): B というシグニチャーでは、要求される唯一のパラメーターは要素を結合する関数であり、初期値は、コレクションの最初の値であることが要求されます。それとは対照的に、foldLeft[B](z: B)(op: (B, A) => B): B というシグニチャーでは、結果を得るには初期シード値が必要であり、このことからリストの要素の型とは異なる型を返すことができます。

以下に、foldLeft を使用してコレクションを合計する例を示します。

List.range(1, 10).foldLeft(0)(_ + _)
// 45

Scala は演算子の多重定義をサポートしているため、2 つの一般的な畳み込み操作 (foldLeftfoldRight) には、それぞれ対応する演算子 /::\ があります。従って、foldLeft を使用した合計は、以下のような簡潔なバージョンにすることができます。

(0 /: List.range(1, 10)) (_ + _)
// 45

同様に、リストの各メンバーから順番に減算処理を行っていくには (合計操作の逆。明らかに、この操作が必要になることはまれです)、foldRight() 関数または :\ 演算子のいずれかを使用することができます。

(List.range(1, 10) :\ 0) (_ - _)
// 5

Groovy

集約のカテゴリーでの Groovy のメソッドは、多重定義を使用して、Scala の reduce() および foldLeft() と同じ機能をサポートします。一方の関数バージョンは、初期値を引数に取ります。以下の例では、inject() メソッドを使用して、コレクションで合計を生成します。

(1..10).inject {a, b -> a + b}
// 55

もう一方のバージョンでは、初期値を以下のように引数に取ります。

(1..10).inject(0, {a, b -> a + b})
// 55

Scala や Clojure と比べ、Groovy の機能ライブラリーの規模は大幅に下回ります。Groovy は関数型プログラミングを強調するわけではなく、マルチパラダイム言語であることを考えると、これは当然のことでしょう。

Clojure

Clojure は主として関数型プログラミング言語であるため、(reduce ) をサポートしています。(reduce ) 関数は、Scala が処理する reduce()foldLeft() の両方をカバーするために、オプションの初期値を引数に取ります。(reduce ) 関数に意外なところはありません。この関数が引数に取るのは、2 つの引数とコレクションを要求する関数です。

(reduce + (range 1 11))
; 55

Clojure は、reduce のような機能に対して、reducers という名前のライブラリーで高度なサポートを提供します。これについては、今後の記事で説明します。


まとめ

(関数型プログラミングのような) 異なるパラダイムを学ぶ上で難しい部分は、新しい用語を覚えることです。コミュニティーによって異なる用語を使用しているとしたら、それはややこしい作業になります。けれども、いったん類似性を把握すれば、3 つの Java.next 言語のすべてが、重複する機能を意外な形の構文で提供していることがわかります。

次回の記事では、Java.next 言語でのメモ化を取り上げ、関数型の機能を組み合わせて使用することで、簡潔な機能になる仕組みについて説明します。

参考文献

学ぶために

  • Scala: Scala は JVM 上で実行される最近の関数型言語です。
  • Groovy は、Java 言語の動的バージョンとして Java の構文と機能が更新されたものです。
  • Clojure: Clojure は JVM 上で実行される最近の関数型 Lisp です。
  • reducers: Clojure のためのこの強力なライブラリーが集約操作のための自動パラレリズムを追加します。
  • 「関数型の考え方」: Neal Ford による developerWorks での連載で、関数型プログラミングについて詳しく調べてください。
  • 「言語設計者のノート」: この developerWorks の連載では、Java 言語アーキテクトである Brian Goetz 氏が、Java SE 7、Java SE 8 およびそれ以降へと Java 言語が進化する中で課題として提示された言語設計上の問題について探ります。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した豊富な記事を調べてください。

製品や技術を入手するために

議論するために

  • developerWorks コミュニティーに参加してください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

コメント

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=Java technology
ArticleID=968289
ArticleTitle=Java.next: シノニムのややこしさを克服する
publish-date=05152014