scalaの、配列に対する統一的な扱いについて

C言語だと、配列ってなくてはならない存在で、入門書でも比較的最初の方に出てきますよね?それはいいとして、言語の誕生や影響うけた順として、
C => C++ => Java
みたいなことがよく言われるじゃないですか?そして、C++でもJavaでも、C言語*1の配列の構文を、よくも悪くも受け継いでますよね?

なんか、出だしからまとまってなくて何が言いたいか意味不明ですが、scalaはその配列の構文受け継いでないよって話。
なんで受け継いでないか?それが具体的にどういうことか?を説明していきます。
これ以降基本的にJavaの例をだして、Javaと比較して説明します。
配列の特殊な構文を挙げてみると、以下のような4点でしょうか?
1.初期化が簡単にできる。
2.型名の書き方が特殊

int[] intArray = {10,20,30,40,50}; // 中括弧つかう・・・
// "int[]" っていうのが型の名前・・・

3.ある要素にアクセスするときに、 変数名[添字] っていう形

System.out.println(intArray[2]);//30って表示される

4.ある要素を更新する場合 変数名[添字] = 代入する値

System.out.println(intArray[4]);//50って表示される
intArray[4] = 100;
System.out.println(intArray[4]);//100って表示される

っていう点でしょうか?
で、ここでやっとscalaがでてくるわけですが、scalaは今挙げた4つのシンタックスすべて受け継いでません。
scalaは、java(やそれに近い文法の言語)を知っている人などために、わざと色々それらの言語に見た目を似せているところがありますが、あえてこれについては変えています。それだけの理由があるからです(`・ω・´)キリッ

scalaは関数型とオブジェクト指向のハイブリッドってことですが、scalaでは、配列自体*2が、ひとつのオブジェクトである、と同時に、関数でもあるという概念です。
こんなこといっても、最初は
はぁ?( ゚д゚ )なにいってんの?
っていう感じだとおもいますが、慣れればすごく統一的で使いやすいです。

最初にあげた、4つのシンタックスについて、scalaでの対応するものを解説していきます
1.初期化が簡単

val intArray = Array(10,20,30,40,50)

こんな感じ↑で、Arrayって先頭につけて、中括弧は使いません。なぜこんな文法にしたかというと、他のclassと、(見た目上は)統一的に扱えるからです。
以下のように、SetでもListでも、初期化の文法が全く同じです。*3

val set1  = Set(10,20,30,40,50)
val list1 = List(10,20,30,40,50)

javaの場合は、配列は1行で済むのに、一方それ以外のclass、例えばArrayListの場合

    ArrayList<Integer> list = new ArrayList<Integer>();
    list.add(10);
    list.add(20);
    list.add(30);
    list.add(40);
    list.add(50);

っていうことになりますよね?*4


2.型名の書き方が特殊
配列って、以下のように性質*5はすべて同じじゃないですか?*6

  • n番目の値取得
  • n番目に違う値代入
  • 長さ取得

ちがうのは、値の型がちがうということだけです。なので、scalaの場合は配列は、型引数を1つとる、Arrayという名前のclassという扱いです。*7 *8
それが理解出来れば、型の書き方も自然と理解できるというか、もう説明する必要がありません。
例えば、
「あるintの配列受け取って、すべての要素を2倍した新しい配列返す関数」
は以下のように書けます*9

def double(array:Array[Int]):Array[Int] = {
  array.map{_ * 2}
}

上記のように、intの配列ならば、型の部分の書き方は、 Array[Int] となります



3.ある要素にアクセスするときに、変数名[添字] っていう形
scalaの配列は、ひとつのオブジェクトである、と同時に、関数でもあると書きました。どういうことがというと、
添字を使って配列のある要素へアクセス
と考えるのではなく、配列そのものを
intの値を1つとり、その対応する要素を返す関数
と考えるわけです。

関数だから、scalaでは、[ ]を使ってアクセスするのではなく( )を使うわけです。

C++や、C# *10 では[ ]オペレーターのoverloadという形で、配列以外のcollectionを配列っぽくアクセスできるようにしていますが、scalaの場合はちょっと考え方がちがいます。*11 *12


最後に

4.ある要素を更新する場合 [添字] = 代入する値
っていうとこですが、以下のようになります↓

val hoge = Array(10,20,30,40)
hoge(0) = 100
print(hoge(0)) // 100って表示される

これが、配列のための特殊な構文でなく、どのclassでも全く同じというのがポイントです。 *13
言い換えれば、(実装はべつにして見た目上は) scalaでは配列はArrayという1つのclassにすぎない からです(`・ω・´)キリッ *14
たとえばArrayBufferならば、*15

val hoge = ArrayBuffer(10,20,30,40)
hoge(0) = 100
print(hoge(0)) // 100って表示される

という感じです *16

細かい違いのように思うかもしれませんが、この、配列を統一的に扱えるという点は、個人的にはかなり好きなところです。scalaかわいいよscala・・・(・д・`)

その他、scalaでは、配列がわざと共変でないなど、細かい違いがあるわけですが、それについては、また気が向いたら書いてみます(´・ω・`)

*1:あるいはC言語自体の配列の構文が、どの言語からきたものとかは詳しく知りませんが

*2:というかそれを含めたcollection系のものすべて

*3: 基本的にこういう場合、コンパニオンオブジェクトのapplyが呼ばれるっていう設計になっています。しかし、Arrayの場合は、そうとは限らず、逆コンパイルしたら、 Array$.MODULE$.apply(10, Predef$.MODULE$.wrapIntArray(new int[] { 20, 30, 40, 50 }) );っていう事になってました。が、普段は、意識する必要ない、というか意識してはいけないものだとおもいます。

*4:無名クラス使えば1行でかけるとか、google collection使えばいいとか、反論はあるとおもいます。が、結局入門書なんかでは、それらを説明するのが面倒で、こういう形で紹介されちゃってるとこが多く、初心者の場合こういう形で書いてしまうことが多いってことが言いたい。そして配列とArrayListで、構文が違う

*5: オブジェクト指向的にいえば、配列というオブジェクトに対して送信できるメッセージの種類

*6: さっきも言いましたが、長さ取得があるし、ここで言ってる配列はjavaの配列です

*7: 実装上は色々違うんですが、少なくともほとんどの場合そう思っていれば、問題ない

*8: javaの配列は、参照型ではあるがclassとはちょっと違っているという、特殊な存在

*9:ジェネリックじゃねぇとか、こんなのわざわざ関数にする必要ねぇ、とかあると思いますが、初心者向けの説明のためなので気にしないください。わざとです。

*10: あとRubyも似たような感じかな?(Rubyはあれもメソッドだった気がするけど)その他の言語でも、似たような[ ]を使った文法あった気がする

*11: scalaでは、 [ ] は、ジェネリックな型名を書く場合につかう

*12: オブジェクト自体を関数っぽく使うためには、applyやupdateっていうメソッド定義します。もしくは、Function系のclass継承する

*13: 配列は別にして、基本的に、updateというメソッドの呼び出しになる

*14: 大事なことなので2回・・・どころか何度でもいいますよ(´・ω・`)てかこれがこのblogのエントリでのいいたいことですから

*15:javaでいうArrayListみたいなもの

*16: デフォルトで名前空間入ってないから、 import collection.mutable.ArrayBuffer っていうの必要だけど