このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

多次元配列と入れ子のリスト

J言語の話をしても誰も興味を持ちそうにないので、J言語の構文の話はしません。データ構造だけに注目して、それを普通の言葉と記法で説明します。「普通の言葉と記法」としては、Lisp風の表現を使います。それが普通か? ええ、J言語に比べたら普通です!

内容:

  1. 入れ子のリスト
  2. 0, 1, 2次元配列
  3. 多次元配列とシェイプ
  4. J言語では…

入れ子のリスト

J言語が扱うデータ構造は多次元配列です。多次元配列を入れ子リストの特殊なものと考えます。基本となるデータをアトムと呼び、今回は数値アトムだけを考えます。(1 2 3) は3つの数値アトムからなるリストです。空リスト () はもちろんリストです。Lispでは、空リストを nil という名前のアトムともみなしますが、ここでは数値アトムしか考えないので、() はアトムではありません

リストは入れ子にできるので、(1 (2 3))、((1 2) (3 4))、(1 (2 (3))) などもリストです。リストの長さを常識通りに考えます。

  • (1 2 3) の長さは3
  • () の長さは0
  • (1 (2 3)) の長さは2
  • ((1 2) (3 4)) の長さは2
  • (1 (2 (3))) の長さは2

入れ子の深さ(最大レベル)も、まー常識的です。

  • (1 2 3) の深さは1
  • () の深さは1
  • (1 (2 3)) の深さは2
  • ((1 2) (3 4)) の深さは2
  • (1 (2 (3))) の深さは3

任意のリストは、数値アトムと空リストから構成できるので、構成に関して帰納的に長さ(length)と深さ(depth)を定義できます。

  • length( () ) = 0
  • length( cons(x, y) ) = length(y) + 1
  • depth( n ) = 0 (nは数値アトム)
  • depth( () ) = 1 (0としても実質的な差はない)
  • depth( cons(x, y) ) = max(depth(x) + 1, depth(y))

0, 1, 2次元配列

n次元の配列を、n = 0, 1, 2 に対して定義します。

  • 0次元の配列とは、アトム(数値だけ)である。
  • 1次元の配列とは、アトムを要素とするリストである。
  • 2次元の配列とは、1次元の配列を要素とするリストで、それら要素の長さがすべて同じものである。

この定義から、((1 2) (3 4)) は2次元の配列となります。(1 (2 3)) は2次元の配列ではありません。最初の要素である1は、1次元の配列ではないからです。((1 2) (3 4 5)) も2次元の配列ではありません。要素であるリスト (1 2) と (2 4 5) の長さが違うからです。

3次元以上の配列を定義するには、多次元配列のシェイプ(shape)という量を定義するのが便利です。入れ子のリストのなかでシェイプを持つものが多次元配列となります。

多次元配列とシェイプ

今まで定義した 0, 1, 2次元の配列に対して、そのシェイプを定義します。シェイプは、整数値からなるリストです。

  • 0次元の配列のシェイプは、空リスト () である。
  • 1次元の配列のシェイプは、配列であるリストの長さをnとして (n) である。
  • 2次元の配列のシェイプは、配列であるリストの長さをn、要素であるリストの長さをmとして (n m) である。

いくつかの具体例を出します。

  • 0次元の配列(数値アトム)10のシェイプは ()。
  • 1次元の配列 (10) のシェイプは (1)。
  • 1次元の配列 (2 1 3) のシェイプは (3)。
  • 2次元の配列 ((1 2) (3 4)) のシェイプは (2 2)。
  • 2次元の配列 ((1 2) (3 4) (5 6)) のシェイプは (3 2)。
  • 2次元の配列 ((1 2 3) (4 5 6)) のシェイプは (2 3)。
  • 2次元の配列 ((10)) のシェイプは (1 1)。

3以上のnに対するn次元の配列を、そのシェイプと共に定義します。

  • n次元の配列とは、その要素がすべて同じシェイプを持つ(n - 1)次元配列であるリストである。要素のシェイプをsとして、当該のn次元配列xのシェイプは cons(length(x), s) となる。

例えば、(((1 2) (3 4)) ((5 6) (7 8)) ((9 10) (11 12))) は3次元配列で、そのシェイプはcons(3, (2 2)) = (3 2 2) となります。

シェイプの長さは配列の次元数に等しく、またこれは、入れ子のリストとしての深さとも等しくなります。多次元配列に含まれるアトムの個数は、シェイプに現れる整数値をすべて掛け算した値となります。例えば、シェイプが (3 2 2) である3次元配列には、3×2×2 = 12 個のアトムが含まれます。

J言語では…

ここまでの話は特に珍しいことはないと思います。さて、J言語ですが…、そんなもん知りたくもない人はここで読むのを打ち切っていいのですが、J言語の多次元配列は、上記の入れ子のリストとは微妙に異なります。その差はほんとに僅かではありますが。

多次元配列を入れ子のリストと考えた場合、シェイプが (0) である配列は存在します。空リスト () のシェイプが (0) です。シェイプが (2 0) である2次元配列も存在します; (() ()) がそうです。ところが、シェイプが (0 2) である2次元配列は作れません。定義に従えば、長さが2である1次元配列を0個含むリストです。これは空リスト () となってしまうので、そのシェイプは (0) です。

J言語の多次元配列では、シェイプが (0 2) の配列も存在します。シェイプが (3 0 2) の3次元配列、シェイプが (0 0) の2次元配列とかも存在します。シェイプに0が現れると、その配列に含まれるアトムの個数は0になるのですが、それでも、別々なシェイプの配列が存在すると考えます。

J言語では、入れ子のリストの特別なものとして多次元配列を考えるのではなくて、まずはシェイプありきで、シェイプごとにデータの集合が存在します。シェイプ (3 0 2) のデータも、シェイプ (0 0) のデータもそれぞれ1個存在します。線形代数の行列でも、0行2列、2行0列の行列があったほうが合理的です。同様に、J言語の考え方も辻褄はあっているんです。