コレクションの利用 - Listクラス
サンプルプログラムでは、人物のリストを次のように作成しました。
// 人物のリスト val list = List( Male("Miles", 32), Female("Ella",18 ), Female("Sarah",25), Male("Wes",16) )
先ほど定義したMaleクラスとFemaleクラスを要素としてもつListを作成して、4つのPersonオブジェクトを変数listに代入しています。変数Listの型は省略されていますが、型推論で推論されます。
Scalaでは、組み込みのコレクション型としてArray(配列),List(LinkedList),Set(集合),Map(連想配列)などが用意されています。
ここではListを利用しています。
Javaの場合では、Listを利用する場合はjava.util.ArrayListなどのインスタンスを生成してから、addメソッドなどで要素を追加していきますが、ScalaではList(要素1,要素2,...)のようにListの作成を行います。同様のシンタックスシュガーはArray,Set,Mapなどにも用意されていますが、これはScalaの文法上で特別扱いされている訳ではなく、List「オブジェクト」やMap「オブジェクト」などがもつファクトリーメソッドなのです。これらはコンパニオンオブジェクトと呼ばれますが、解説は次回以降に行います。
通常、作成されるListはイミュータブルなListになります。つまり、一度作成されたListは要素の追加/削除/変更を行うことができません。しかし、Scalaのコレクションライブラリには変更可能なコレクションも用意されており、必要に応じて切り替えて利用することができるようになっています。
なぜ、デフォルトのListがイミュータブルになっているのでしょうか?
「ケースクラス」もイミュータブルでした。Scalaには、「イミュータブルなデータ構造を利用することで副作用を抑えた、堅牢なプログラムを書くことができる」という思想が反映されています。
変更可能な変数を利用すると、例えばnullなど意図しないデータが変数に代入されることで発生するバグを回避することができます。
もちろん、イミュータブルなオブジェクトを利用することで、パフォーマンス面で不利になる局面もありえます。ですので、Scalaではデフォルトではイミュータブルですが、変更可能なデータ構造も利用できる設計になっているのです。
Scalaにおけるコレクションの利用については、本連載の中で詳しく紹介する予定です。
関数の定義 - defによる宣言と関数リテラル・無名関数
次は、関数を見てみましょう。サンプルプログラムで、引数の年齢以上のものを抽出する関数filterAgeを作成しました。リスト9はfilterAge関数の定義です。
// 人物のリストから、引数の年齢以上のものを抽出する関数 def filterAge( n:Int, xs:List[Person] ) = xs.filter{ _.age >= n }
関数は、defキーワードで定義することになっていました。詳しく見てみましょう。
まず、HelloWorldで定義したmain関数と異なるのは「{}」がないことでしょう。Scalaでは、関数本体が一行の式で定義できる場合は、「{}」を省略できます。
また、関数の定義では、引数の宣言の後に、「:結果型」で関数の結果型を指定する必要がありましたが、filterAge関数では結果型が指定されていません。これは、変数と同様に関数の結果型も型推論が働くので省略してよいことになっています。対して、引数の型は省略することができません。
fiterAge関数は、省略せずに次のように書いても同じことです。
def filterAge( n:Int, xs:List[Person] ):List[Person] = { xs.filter{ _.age >= n } }
さて、「=」の右側の関数本体を詳しく見てみます。List[Person]型の引数xsに対して、fitler関数を呼び出していますね。filter関数は、ScalaのListクラスに用意されたAPIで、条件に一致する要素のみをListの中身から抽出したListを返す関数です。
問題は、このfilter関数に渡してる「{ _.age >= n }」の部分です。これは、いったい何を渡してるのでしょうか?
結論から言うと、filter関数には「Person型の変数を受け取って、ageフィールドがfilterAge関数の引数n以上だったらtrueを返す無名関数」を渡しているのです。
つまり、関数オブジェクトをその場で生成して、filter関数に渡しているということです。
このように、Scalaではその場で名前をつけずに関数オブジェクトを作り出す関数リテラルという記法があり、ここではそれを利用しているのです。具体的にはリスト11のような関数をその場で定義しているイメージです。
def checkAge( n:Int,p:Person ):Boolean = { p.age >= n }
「{ _.age >= n }」の「_」の部分には、List[Person]型の引数xsの個々の要素であるPerson型のオブジェクトが渡されます。「_」で渡されたPersonオブジェクトのageプロパティがn以上か、演算子「>=」を利用して判定してるのです(正確には、「>=」は演算子ではなくメソッドなのですが、分かりやすく説明するために演算子と言っています)。
関数の返り値は、最後に評価された式の値になるので、比較した結果のBoolean型が返り値になります。
関数リテラルの記法ですが、「{ _.age >= n }」という書き方は、実はかなり省略された書き方です。関数リテラルの書き方には何種類かあるのですが、この場合に省略せずに書くとすると、次のようになります。
{(p:Person) => p.age >= n }
関数リテラルでは「(引数1:型, 引数2:型, ・・・) => { 処理 }」という書き方をします。リスト12の例では、Person型の引数pをもらうので(p:Person)と書いて、「=>」のあとに関数本体を記述しています。
この引数の型の部分は、関数リテラルが定義できる場所でScalaが引数の型を省略できる場合は、書かなくてもよいことになっています。この場合では、 「p => { p.age >= n }」と書いてもpの引数はPerson型であることをScalaが推論してくれるので、問題ありません。
さらに、この引数p自体も省略してしまうことが可能です。「{ _.age >= n }」のように、「_」の部分を引数として受け取るように関数を書くことができます。この「_」はプレースホルダーと呼ばれています。
「_」は、関数リテラルの中で一度しか利用できないという制限がありますが、簡単な処理であれば「_」で済ませてしまった方が記述が単純になるというメリットがあります。
まとめると、Listクラスのfilter関数は、「Person型の変数を一つとり、Boolean型の結果を返す関数」を引数に取る関数です。filter関数に、「年齢を比較する関数」を渡して、比較結果がtrueのPersonオブジェクトだけを集めたListが、filterAge関数の返り値になるということです。
Listの中から条件にあった要素を抽出するような処理は、手続き型言語ではforループなどを利用して書くことが多いでしょうが、Scalaではこのようにコレクションがサポートする関数と無名関数を利用して、ループなしで書くことができます。このようなスタイルが、関数型言語の要素を取り入れているScalaの特徴の一つであると言えるでしょう。