Java.next: 関数型のコーディング・スタイル

Groovy、Scala、Clojure で共有されている関数型の構成体とそれらを使用するメリット

すべての Java.next 言語には、開発者がより上位の抽象化レベルで考えることを可能にする、関数型プログラミングの構成体があります。しかし言語によって用語が異なるため、同様の構成体であることに、なかなか気付かないことがあります。今回の記事では、Java.next 言語のそれぞれで、共通の関数型プログラミングの構成体がどのような形で現れるのかを明らかにし、それらの機能の実装詳細における微妙な違いを指摘します。

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

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



2014年 1月 30日

この連載について

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

ガーベッジ・コレクションが主流となってからは、デバッグが困難な部類に属する問題はすべて解消され、開発者にとって複雑でエラーの原因となりやすいプロセスを、ランタイムが管理できるようになりました。関数型プログラミングの目的は、開発者が作成するアルゴリズムに、それと同じ成果をもたらすことです。つまり、ランタイムが高度な最適化を行って、開発者がより上位の抽象化レベルで作業できるようにすることを目的としています。

Java.next 言語が命令型言語と関数型言語のどちらにどの程度近いかは、Java.next 言語の範疇に属するすべての言語で同じになるわけではありません。いずれの Java.next 言語にしても、関数型の機能とイディオムがあるという点では共通しています。関数型プログラミングの手法は明確に定義されていますが、まったく同じ機能的概念であっても、言語によって使用する用語が異なっているために、その類似性に気付きにくい場合があります。今回の記事では、Scala、Groovy、Clojure での関数型のコーディング・スタイルを比較し、それぞれのメリットについて検討を行います。

命令型での処理

まずは一般的な問題と、その問題に対する命令型でのソリューションから説明します。例えば、英語の名前のリストに 1 文字だけの名前がいくつか含まれているとします。このリストから 1 文字だけの名前を除外して、大文字で始まる名前をコンマで区切った 1 つの文字列にして返さなければなりません。このアルゴリズムを実装する Java コードを、リスト 1 に記載します。

リスト 1. 命令型での処理
public class TheCompanyProcess {
    public String cleanNames(List<String> listOfNames) {
        StringBuilder result = new StringBuilder();
        for(int i = 0; i < listOfNames.size(); i++) {
            if (listOfNames.get(i).length() > 1) {
                result.append(capitalizeString(listOfNames.get(i))).append(",");
            }
        }
        return result.substring(0, result.length() - 1).toString();
    }

    public String capitalizeString(String s) {
        return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
    }
}

リスト全体を処理しなければならないことから、リスト 1 ではその最も簡単な方法として、命令型ループの中で問題に対処しています。具体的には、それぞれの名前に対して、その長さが 1 より大きいかどうかを調べ、(1 より大きい場合は) 先頭を大文字にした名前を末尾のコンマと併せて result 文字列に追加します。最終的な文字列の最後の名前にはコンマが不要であるため、最後の戻り値からはコンマを削除します。

命令型プログラミングの場合、開発者は下位レベルで処理を行うように促されます。リスト 1cleanNames() メソッドの中では 3 つの処理を行っています。その 3 つとは、名前のリストから 1 文字の名前を除外するための「リストのフィルタリング」、フィルタリング後のリストに含まれるすべての名前を大文字で始めるための「文字種の変換」、そして文字種の変換後のリスト全体を単一の文字列にするための「形式の変更」です。命令型言語では、この 3 つの処理すべてに対して同じ下位レベルのメカニズム (つまり、リストの繰り返し処理) を使用せざるを得ません。一方、関数型言語では、「リストのフィルタリング」、「文字種の変換」、「形式の変更」は共通の処理であると認識するため、命令型言語とは異なる観点で問題に取り組む手段が用意されています。


関数型での処理

関数型のプログラミング言語では、問題を命令型言語とは異なるカテゴリーとして扱います。「リストのフィルタリング」、「文字種の変換」、「形式の変更」の論理カテゴリーは、「関数」となります。これらの関数は下位レベルの変換を実装しており、その振る舞いは、開発者が機能を引数として渡す形で記述することによってカスタマイズします。リスト 1 の問題は、以下の擬似コードに概念化することができます。

listOfEmps -> filter(x.length > 1) -> transform(x.capitalize) -> 
   convert(x, y -> x + "," + y)

関数型言語を使用する場合、実装の詳細を気にすることなく、この概念ソリューションをモデル化することができます。

Scala の場合

リスト 2 は、リスト 1 での処理の例を Scala で実装したものです。このコードは、必要な実装詳細が加えられているだけで、上記の擬似コードとほとんど同じです。

リスト 2. Scala での処理
val employees = List("neal", "s", "stu", "j", "rich", "bob")
val result = employees
  .filter(_.length() > 1)
  .map(_.capitalize)
  .reduce(_ + "," + _)

名前のリストを指定すると、最初にそのリストをフィルタリングして、長さが 1 以下の名前を除外します。この処理の出力は、map() 関数に取り込まれ、指定されたコード・ブロックがコレクションに含まれる各要素に対して実行されて、文字種が変換されたコレクションが返されます。そして最後に、map() から出力されたコレクションが reduce() 関数に渡されて、コード・ブロックに指定されたルールに従って各要素が結合されます。この例の場合、要素の各ペアを結合する方法として使用しているのは、コンマの挿入による連結です。この 3 つの関数呼び出しのいずれにしても、引数の名前は重要ではないため、Scala の便利なショートカット機能により、「_」を使用して引数名を省略することができます。reduce() 関数は、最初の 2 つの要素を連結して 1 つの要素を生成し、その要素が次の連結での最初の要素となります。このように reduce() はリストを先へと進みながら、必要なコンマ区切り文字列を作成していきます。

Scala の実装を最初に記載したのは、Scala の構文は比較的親しみやすいことと、Scala では「リストのフィルタリング」、「文字種の変換」、「形式の変更」のそれぞれに対し、業界の慣習と一致した名前である filter、map、reduce を使用していることが理由です。

Groovy の場合

Groovy にも同じ機能がありますが、これらの機能には Ruby などのスクリプト言語と一致した名前が付けられています。リスト 1 の処理の例は、Groovy バージョンではリスト 3 のようになります。

リスト 3. Groovy での処理
class TheCompanyProcess {
  public static String cleanUpNames(List listOfNames) {
    listOfNames
        .findAll {it.length() > 1}
        .collect {it.capitalize()}
        .join(',')
  }
}

リスト 3 は、構造に関してはリスト 2 の Scala の例と似ていますが、メソッド名は異なっています。Groovy の findAll コレクション・メソッドは、指定されたコード・ブロックを要素に適用し、このコード・ブロックが true に評価された要素を保持します。Scala と同じく Groovy にも暗黙引数のメカニズムがあり、単一引数のコード・ブロックには定義済みの暗黙引数 it を使用しています。collect メソッド (map の Groovy バージョン) は、コレクションに含まれる各要素に対し、指定されたコード・ブロックを実行します。Groovy には、指定された区切り文字を使って文字列のコレクションを 1 つの文字列に連結する関数 (join()) があります。これがまさに、この例に必要なものです。

Clojure の場合

Clojure は、reducemap、および filter という関数名を使用する関数型言語です (リスト 4 を参照)。

リスト 4. Clojure での処理の例
(defn process [list-of-emps]
  (reduce str (interpose "," 

      (map clojure.string/capitalize 
          (filter #(< 1 (count %)) list-of-emps)))))

Clojure の thread-first マクロ

Clojure の thread-last マクロは、コレクションを容易に扱えるようにしますが、それと同様の thread-first マクロは、Java API を容易に扱えるようにします。例えば、person.getInformation().
getAddress().getPostalCode()
というありふれた Java コードのステートメントには、デメテルの法則に違反する Java の傾向が現れています。このようなタイプのステートメントは、Clojure で Java API を使用する開発者にとっては頭痛の種となります。というのも、Clojure では (getPostalCode (getAddress (getInformation person))) のように内側から外側の方向でステートメントを作成しなければならないためです。thread-first マクロを使用すれば、この構文の扱いにくさが解消され、どれだけ深くネストされた呼び出しでも (-> person getInformation getAddress getPostalCode) のように Java API と同じ順番で記述することができます。

Clojure のコードを読むのに慣れていなければ、リスト 4 に記載されているコードの構造を理解するのは難しいでしょう。Clojure などの Lisp は、「内側から外側」の方向に働きます。従って、上記のコードは、最後に記述されている引数の値 list-of-emps から読んでいく必要があります。Clojure の (filter ) 関数が取る引数は、フィルタリングに使用する関数 (この例では、匿名関数) とフィルタリング対象のコレクションの 2 つです。1 番目の引数には、(fn [x] (< 1 (count x))) といった正式な関数定義を記述することもできますが、Clojure では匿名関数をより簡潔に記述することができます。これまでに記載した例と同じく、フィルタリング処理の出力は、要素が少なくなったコレクションです。(map ) 関数は、1 番目の引数として文字種を変換するための関数を取り、2 番目の引数としてコレクション (この例では、(filter ) 処理の戻り値) を取ります。Clojure の (map ) 関数が 1 番目の引数として取る関数は、通常は開発者が指定しますが、引数を 1 つだけ取る関数であれば、どれでも有効です。組み込み capitalize 関数は、この要件に適合します。(map ) 処理の最終的な結果は、(reduce ) に対するコレクションの引数になります。(reduce ) の 1 番目の引数は、(interpose ) 関数の戻り値に適用される結合関数 (str ) です。(interpose ) は、1 番目の引数をコレクションの各要素の間 (最後の要素の後は除く) に挿入する関数です。

経験豊富な開発者であっても、リスト 4(process ) 関数のようにあまりにも深く機能がネストされてくると手こずってしまうものです。幸い、Clojure に含まれるマクロを使用すれば、より読みやすい順序に構造を「戻す」ことができます。リスト 5 の機能は、リスト 4 のバージョンでの機能とまったく同じです。

リスト 5. Clojure の thread-last マクロを使用する
(defn process2 [list-of-emps]
  (->> list-of-emps
       (filter #(< 1 (count %)))
       (map clojure.string/capitalize)
       (interpose ",")
       (reduce str)))

Clojure の thread-last マクロは、コレクションに対して各種の変換を適用するという一般的な操作を引数に取り、Lisp での通常の順序を逆にして、左から右への方向で自然に読めるようにします。リスト 5 では、(list-of-emps) コレクションが最初に来ています。ブロック内の以降に続く各フォームは、そのフォームの前のフォームに適用されます。Lisp の長所の 1 つは、構文の柔軟性にあります。コードが読みにくくなった場合には、いつでも読みやすくなるように構文を変えることができます。


関数型プログラミングのメリット

Beating the Averages」というタイトルの有名なエッセイで、Paul Graham 氏は「Blub のパラドックス」について定義しています。彼は Blub という名前の架空の言語を考え出し、Blub と他の言語との強力さの比較について次のように想像を巡らしています。

架空の Blub プログラマーが、Blub より強力でない言語に目を向けているときには、彼にはその言語が強力でないという自覚があります。というのも、Blub より強力でない言語には、このプログラマーが使い慣れている機能が欠けているため、この言語が強力さに劣ることは明らかだからです。けれども、この架空の Blub プログラマーが Blub よりも強力な言語に目を向ける場合、彼はその言語が Blub より強力であることに気付きません。彼の目には、ただ奇妙な言語として映るのみです。おそらく彼はその言語について、強力さは Blub と同じくらいだが、他の難解なものが追加されていると考えるでしょう。Blub の世界で考えている彼には、Blub で十分なのです。

多くの Java 開発者たちにとって、リスト 2 のコードは異質で奇妙なものに映るため、メリットがあるものと見なすのは困難です。けれども、タスクを実行する方法を事細かに指定しすぎるのを止めれば、ますます賢くなってきている言語とランタイムを解放して、強力な改善を実現することができます。例えば、JVM の出現により、メモリーの管理は開発者の懸念事項ではなくなり、R&D 分野全体が最先端のガーベッジ・コレクションの作成に取り組めるようになりました。命令型によるコーディングでは、反復ループの動作の詳細にはまりこみ、最適化 (並列処理など) は困難になります。それよりも上位レベルで (filter、map、reduce などの) 処理について検討することが、概念を実装から切り離し、変更 (並列処理など) の作業を、複雑で詳細な作業から単純な API の変更へと変えることになります。

ここで少し時間を取って、リスト 1 のコードをマルチスレッド化する方法について考えてみます。このコードは、for ループで行われている処理の詳細が密接に関わっていることから、扱いにくい並行処理のコードにも対処する必要があります。そこで、リスト 6 に記載する並列化された Scala バージョンを検討してみましょう。

リスト 6. 処理の並列化
val parallelResult = employees
  .par
  .filter(f => f.length() > 1)
  .map(f => f.capitalize)
  .reduce(_ + "," + _)

リスト 2リスト 6 の唯一の違いは、一連のコマンドに .par メソッドが追加されていることです。.par メソッドはコレクションの並列バージョンを返し、以降の処理はこの並列バージョンに対して実行されます。コレクションに対する処理は高次の概念として指定しているため、基礎となるランタイムはさらに他の処理を自由に行うことができます。

命令型のオブジェクト指向言語を使用する開発者は、コードの再利用をクラス・レベルで考えがちです。それは、これらの言語ではクラスをビルディング・ブロックとして使用するように奨励していることが原因となっています。関数型プログラミング言語では、関数レベルで再利用する傾向があります。関数型言語は、高度な一般的機構 (filter()map()reduce() など) を作成し、関数を引数として提供するという形でのカスタマイズを可能にします。関数型言語では、データ構造体をリストやマップなどの標準コレクションに変換するのが一般的です。こうすることで、強力な組み込み関数によってコレクションを操作できるためです。例えば、Java の世界には何十もの XML 処理フレームワークがあり、そのそれぞれが独自の観点から XML 構造をカプセル化して、固有のメソッドによって提供しています。その一方、Clojure などの言語では、XML は標準のマップ・ベースのデータ構造に変換されます。このように変換された XML には、その言語にすでに組み込まれている強力な変換処理、集約処理、フィルタリング処理を適用することができます。


まとめ

最近のすべてのプログラミング言語には、関数型プログラミングの構成体が組み込まれているか、あるいは追加されていることから、関数型プログラミングは開発者にとって将来欠かせないものとなっています。Java.next 言語は例外なく、強力な関数型の機能を実装していますが、その名前や動作はすべての言語で同じであるとは限りません。この記事では、Scala、Groovy、Clojure での新しいコーディング・スタイルを説明し、そのメリットを明らかにしました。

次回の記事では、言語による filter、map、reduce 実装の違いについて、さらに深く掘り下げます。

参考文献

学ぶために

  • Scala: Scala は JVM 上で実行される最近の関数型言語です。
  • Groovy は、Java 言語の動的バージョンとして Java の構文と機能が更新されたものです。
  • Clojure: Clojure は JVM 上で実行される最近の関数型 Lisp です。
  • デメテルの法則: ソフトウェア開発用の設計ガイドラインについて学んでください。このガイドラインでは、プロパティー・アクセス用に長いリストを作成することを非推奨としています。
  • Beating the Averages」(Paul Graham 著、2003年4月): Graham 氏が、初めて自作のオンライン e-コマース・サイト ViaWeb を構築したときの経験を読んでください。
  • 関数型の考え方」: 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=960648
ArticleTitle=Java.next: 関数型のコーディング・スタイル
publish-date=01302014