SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

japan.internet.com翻訳記事

Javaで作成する多次元配列操作ライブラリ

多次元配列を扱うプログラミングを単純化する方法

  • X ポスト
  • このエントリーをはてなブックマークに追加

本稿では、Javaで多次元配列のデータを扱うためのライブラリを実装する方法について説明します。このライブラリでは、APLというプログラミング言語と同じスタイルで、ごく少量のコードを書くだけでデータを変換したり、さまざまな軸に沿ってデータスライスを抽出したりできます。今回は数値データを扱いますが、さまざまな種類の表データを扱うように修正することもできます。

  • X ポスト
  • このエントリーをはてなブックマークに追加

はじめに

 パーソナルコンピューティングの世界に出現した最初のキラーアプリケーションは表計算ソフトでした。これは当然のことです。数値が詰まった表は、データを表示・分析するための最も簡単かつ有用な手法です。

 数値の配列は、数値プログラミングの基礎です。多次元配列を専門的に扱う言語(たとえば、APLや、その後継言語であるJおよびK)がいくつも作り出されたのは、驚くにあたりません。これらの言語の推進者は、配列言語を使うことで、すっきりと理解しやすいコードが書ける、と言っています。

 金融プログラミングを少しでも手がけた方なら、必ず数値の多次元配列を扱っているはずです。高次元の配列はデータ構造を図式化するのも難しく、一筋縄では扱えません。コード自体も読みにくいものになりがちです。三重のネストになった、優に3画面分にも及ぶforループなどを扱った覚えのある人もいるのではないでしょうか。

 しかし、多次元配列のメリットは否定できません。数値データ構造がたとえ扱いにくいほどの高次元配列だったとしても、すべての軸が均質であるというメリットは何物にも代えられません。つまり、配列を高次元空間で自在に回転させ、低次元のデータスライスを切り取って、それを操作し、表示できるのです。軸と軸の間に大きな違いはありませんから、どの軸の取り扱いにも同じルーチン群を使用できます。

 本稿では、多次元配列用ライブラリを設計して実装し、それを使用してプログラミングを単純化する方法について考えていきます。

Genericsについてのメモ

 本稿で説明するライブラリは、JDK 1.5のJava Generics機能を使用しています。むしろ、このライブラリのCubeクラスの実装は、JDK 1.5の新しいGenerics機能に多くを依存していると言ってもよいでしょう。Cubeクラスは、TCubeの各セルの型)でパラメータ化されています。この方法の利点は、Cubeの次元数を前もって決めておく必要がないことです。何次元の配列であっても、コンストラクタはそれを受け入れ、そこから再帰的にCubeを作成します。実は、これがまさに狙いどおりに運んだことに、私自身が驚きました。ちょっと魔法のようだとも思いました。このコンストラクタのコードは、きっと読者の参考になるでしょう。

 私がJavaのこの機能を使ったのは、今回が初めてです。コードのあちこちで型パラメータTを指定することになるだろうと予想していましたが、その必要がほとんどないことに驚きました。コンパイラがTの型をかなり正確に推測してくれるようです。

Cubeクラス

 今回のコードの中心は、Cubeというクラスです。Cubeは多次元配列です。単に「Cube」(立方体)と言うと3次元を連想するので混乱を招くかもしれませんが、安心してください。Cubeクラスは1次元以上、何次元でも取り扱うことができます。

 Cubeは多次元配列です。これは、Javaでは「ネストした配列」を意味します。つまり、「配列の配列」や「配列の配列の配列」といったものを表します。「Cube」という名前を選んだのは、ツリー(木)の概念が入り込むのを避けたかったからです。ツリーの場合は、個々のネスト部分で形状が異なることがありますが、Cubeはそうでなく、多次元の規則正しいグリッドを含んでいます。

 それでは、まずCubeの使い方を見てみましょう。その後で、具体的な実装方法を見ていきます。

Cubeの作成

 まずは簡単なデータで考えてみましょう。次に示すのは典型的な配列です。

Integer data[] = { 10, 20, 30 };

 これを1次元のCubeに置き換えるのは簡単です。

Cube cube = new Cube( data );

 図1は、このCubeの内部のデータがどう見えるかを示しています。

図1 1次元Cube(1次元Cube内のデータはこのように表現できる)
図1 1次元Cube(1次元Cube内のデータはこのように表現できる)

 私がスロット0に10、スロット1に20、スロット2に30を入れたのは偶然です。データを一様にして、それに何が起こるかを見やすくしたかっただけですが、現実にはさまざまなデータが入るはずです。

データの変更

 Cubeに対してまずできることは、1つ目の軸を中心にしてCubeを反転させることです。そのためには次のようにします。

Cube r = cube.reverse();

 これにより、図2のような新しいCubeが返されます。

図2 反転したCube(図1のデータを反転させると、このCubeが得られる)
図2 反転したCube(図1のデータを反転させると、このCubeが得られる)

 ここで「新しいCubeが返される」と言ったことに注意してください。このクラスは関数のスタイルで書かれているので、オブジェクトが自分自身を変化させるのではなく、「オブジェクトのメソッドが新しいオブジェクトを返す」ということになります。やや奇妙に聞こえるかもしれませんが、配列プログラミングでは非常に有用な方法です。他のプログラミング言語(特に、APLなどの配列言語)から借用したアイデアですが、Javaでも有効です。

マップ操作

 マップとは、ある関数をリスト中のすべての要素に適用する操作を言います。マップの結果として得られるものは、毎回の関数コールから得られた出力のリストです。たとえば、Cubeオブジェクトに対してdoubler(2倍化)という関数をマップした場合は、図3に示すようなCubeが得られます。

Cube d = cube.map( doubler );
図3 2倍化したCube(図1のCubeの値を2倍化した結果を示している)
図3 2倍化したCube(図1のCubeの値を2倍化した結果を示している)

 ところで、このdoublerとは何でしょうか。関数のような形をしていますが、Java内で受け渡しできないので、正確には関数ではありません。むしろ、1つの関数を含む単純なオブジェクトと言えます。

Fun doubler = new Fun<Integer>() {
  public Integer apply( Integer n ) {
    return n * 2;
  }
};

 doublerクラスは、apply()というメソッドを1つだけ含むFunインターフェイスを実装しています。apply()メソッドは、何らかのオブジェクトを受け取り、それを変更して、変更済みバージョンを返します。Funは値変換プロセスであり、この例では、数値を受け取り、それを2倍にして返します。このFunmap()メソッドに渡すと、Cube全体に変換が適用され、すべてのセルの値が2倍になります。

フォールド操作

 フォールド(折り畳み)も、多次元配列に対して行える便利な操作です。マップがN次元配列を受け取ってN次元配列を返したのに対し、フォールドはN次元配列を受け取って(N-1)次元配列を返します。つまり、ある次元に沿ってCubeを折り畳みます。

 例を示しましょう。最初の[10 20 30]という配列に戻ります。次のようにして、この配列にフォールドを適用することができます。

Integer data[] = { 10, 20, 30 }
Cube cube = new Cube( data );
int sum = cube.fold( plus, 0 );

 フォールドの結果(10+20+30=60)が、整数型の変数sumに格納されます。つまり、配列中のすべての要素が合計されたわけです。フォールドでは加算以外の処理を行うこともできますが、それはさておき、この例ではどのようにして合計が計算されたのでしょうか。

 先ほどの例ではfold( plus, 0 )というメソッドを呼び出しました。これは次のコードを意味しています。

int sum = 0;            // 0
sum = plus( sum, 30 );  // 30 =  0+30
sum = plus( sum, 20 );  // 50 = 20+30
sum = plus( sum, 10 );  // 60 = 10+50

 つまり、このコードはplusを繰り返し呼び出して配列を圧縮した、ということです。もっと関数らしい形で表現すると、次のようになります。

int sum = plus( 10, plus( 20, plus( 30, 0 ) ) );

 あるいは、+演算子を使えば、次のようになります。

int sum = 10 + (20 + (30 + 0));

 フォールドの長所は、いろいろな関数を使い分け、いろいろな結果を得られることです。たとえば、和でなく積が欲しいときは、plusと0の代わりにtimesと1を使います。

Integer data[] = { 10, 20, 30 }
Cube cube = new Cube( data );
Integer product = cube.fold( times, 1 );

 このコードからは、10 * (20 * (30 * 1))の結果、すなわち6000が返されます。もちろん、plustimesはあらかじめ定義しておく必要があります。doublerと同じ要領で定義できますが、引数が1つではなく、2つになる点が異なります。

Fun2 plus = new Fun2<Integer>() {
  public Integer apply( Integer a, Integer b ) {
    return a + b;
  }
};

Fun2 times = new Fun2<Integer>() {
  public Integer apply( Integer a, Integer b ) {
    return a * b;
  }
};

 少々長ったらしい定義であることは否定できませんが、再使用可能であるのが最大のメリットです。こうしたマップやフォールドを行うプロセスをいくつも定義し、ライブラリを作っておけば、コード中で必要なものを名前で参照できます。

左フォールドと右フォールド

 フォールドには、上記の方法しかないわけではありません。実は、左フォールドと右フォールドという2通りの方法があります。

 先ほど説明したのは右フォールドでした(foldrとも呼ばれます)。これが既定なので、上記のfold()は、実はfoldr()を意味しています。右フォールドでは、個々の要素を右から左へ結合していきます。たとえば、次の右フォールドは、

Integer data[] = { 10, 20, 30 }
Cube cube = new Cube( data );
int sum = cube.foldr( plus, 0 );

 次の操作と同じです。

int sum = 10 + (20 + (30 + 0)); // 60

 一方、左フォールド(foldlとも呼ばれます)では、数値を左から右へ合計していきます。

Integer data[] = { 10, 20, 30 }
Cube cube = new Cube( data );
int sum = cube.foldl( plus, 0 );

 これは、次のようにするのと同じことです。

int sum = ((0 + 10) + 20) + 30; // 60

 もちろん、加算なら、左フォールドでも右フォールドでも結果は同じになります。加算は結合的演算であり、(a + b) + ca + (b + c)と同じだからです。しかし、除算は結合的演算ではありません。さて、次のフォールドの結果はどうなるでしょうか。

Float data[] = { 10F, 20F, 30F, 40F };
Cube cube = new Cube( data );
float sum_right = (Float)cube.foldr( dividedby, 1F );
float sum_left = (Float)cube.foldl( dividedby, 1F );

 このコードでは、sum_right(右フォールドの結果)が0.375、sum_left(左フォールドの結果)が2.6666667となります。

次元の追加

 これまで扱ってきたのは1次元配列でした。簡単ですが、あまりおもしろくありません。そこで、もっとデータを追加してみることにします。2次元Cubeの作り方は1次元Cubeの作り方と同じですが、2次元分のデータが必要です。

Integer data[][] = { { 1010, 1020, 1030 },
                     { 2010, 2020, 2030 }, 
                     { 3010, 3020, 3030 } };
Cube cube = new Cube( data );

 この2次元Cube内のデータは、図4のようになります。

図4 2次元Cube(この図は、2次元Cube内のデータを表現している)
図4 2次元Cube(この図は、2次元Cube内のデータを表現している)

 マップ操作は、2次元配列でも1次元配列と同じように実行できます。図4の2次元Cubeにマップ操作としてdoublerを適用すると、図5のような結果が得られます。

図5 2倍化した2次元Cube(1次元Cubeの場合と同じ要領で、2次元Cubeにも2倍化が適用される)
図5 2倍化した2次元Cube(1次元Cubeの場合と同じ要領で、2次元Cubeにも2倍化が適用される)

 フォールド操作も、2次元配列に対して支障なく実行できます。フォールドでは次元数が1つ減ることを思い出してください。1次元配列(10, 20, 30)を折り畳むと1個の数値(60という和)になったように、2次元配列を折り畳むと、結果は1次元配列になります。

Cube f = cube.fold( 'plus', 0 );

 このフォールドでは、図6に示すとおり1列の値が得られます。

図6 折り畳まれた2次元Cube(フォールドは多次元Cubeにも適用できる)
図6 折り畳まれた2次元Cube(フォールドは多次元Cubeにも適用できる)

軸の操作

 高次元の配列では、軸を動かすと便利なことがあります。たとえば、上の2次元配列の軸を交換したくなったとします。つまり、図4の配列を図7の配列のようにしたいとします。できるでしょうか。

図7 軸の交換(図4の2次元Cubeの軸を交換するとこうなる)
図7 軸の交換(図4の2次元Cubeの軸を交換するとこうなる)

 答えは簡単です。次のようにして、2つの軸を入れ替えるだけです。

int permutation[] = { 1, 0 };
Cube pcube = cube.permuteAxes( permutation );

 { 1, 0 }という配列は、出力配列の組み立て方法を制御しています。この配列変更では、入力の軸#1を出力の軸#0にし、入力の軸#0を出力の軸#1にします。つまり、軸の交換を行っています。

 マップとフォールドは強力な操作です。その力の源泉は、計算の実行方法を記述せず、データをたどっていく順序だけを記述するという点にあります。計算方法自体は、FunまたはFun2として引き渡されます。

 データをたどる順序と計算方法とを分離することが、関数的プログラミングの基本です。これは、アスペクト指向プログラミングで言う「関心事の分離」という考え方にも通じます。本稿で述べたマップとフォールドは初歩中の初歩です。あとはご自分で新しい関数を作成し、データ構造を縦横にマップし、フォールドしてください。

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
japan.internet.com翻訳記事連載記事一覧

もっと読む

この記事の著者

japan.internet.com(ジャパンインターネットコム)

japan.internet.com は、1999年9月にオープンした、日本初のネットビジネス専門ニュースサイト。月間2億以上のページビューを誇る米国 Jupitermedia Corporation (Nasdaq: JUPM) のニュースサイト internet.comEarthWeb.com からの最新記事を日本語に翻訳して掲載するとともに、日本独自のネットビジネス関連記事やレポートを配信。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

Greg Travis(Greg Travis)

ニューヨーク在住のJavaプログラマ兼テクノロジーライター。ハイエンドPCゲーム業界で3年間を過ごした後に、EarthWebに参加し、当時最新鋭のJavaプログラミング言語を用いた新規テクノロジーを各種開発。1997年以降は、さまざまなWebテクノロジーについてのコンサルタントを務める。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/149 2005/11/08 17:40

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング