多忙な Java 開発者のための Scala ガイド: コレクション型

Scala でタプル、配列、そしてリストを扱う

Scala にはオブジェクトがありますが、タプルや配列、リストなどの関数型もあります。Ted Neward による人気シリーズの今回は Scala の関数型の側面を探る作業を開始し、まず関数型言語に共通の型に対する Scala のサポートについて説明します。

Ted Neward, Principal, Neward & Associates

Ted Neward photoTed Neward は、Neward & Associates の代表として、Java や .NET、XML サービスなどのプラットフォームに関するコンサルティング、助言、指導、講演を行っています。彼はワシントン州シアトルの近郊に在住です。



2008年 6月 27日

Scala を学ぶ Java™ 開発者にとって、オブジェクトは自然で容易な入り口です。このシリーズの過去数回の記事では、Scala でのオブジェクト指向プログラミングが Java プログラミングとあまり変わらない、という面をいくつか紹介しました。また Scala が従来からのオブジェクト指向の概念について 21 世紀でも使えるように、どのようにして再度焦点を当て、欠けているものを見つけ出し、再構築しているかについても説明しました。しかしこうして Scala について説明している間、常に、ある重要な何かが陰で身を潜めて登場を待っていました。それは、Scala が関数型 (functional) の言語でもあるという側面です。(ここで言う関数型 (functional) には、使い物にならない (dysfunctional) 他の言語とは対照的、という意味も込められています。)

Scala の持つ関数型指向は十分調べる価値がありますが、その理由は、他に調べる対象がなくなってしまったから、というだけではありません。Scala における関数型プログラミングを学ぶことによって、設計のための新しい構成体や概念、また組み込みの構成体を利用できるようになり、一定のシナリオ (例えば並行性など) のプログラミングがずっと容易になるのです。

C# 2.0 の Nullable 型

他の言語では、「ヌル可能性 (ヌルになれること: nullability)」の問題を解決するためにさまざまな方法が試みられてきました。C++ は基本的にこの問題を可能な限り長期間にわたって無視してきましたが、最終的にヌルとゼロが異なる値であると結論づけました。Java 言語はまだこの問題を完全には解決していませんが、Java プログラマーは (プリミティブ型をラッパー・オブジェクトに自動変換する) Autoboxing に頼ることで、この問題を切り抜けています (ラッパー・オブジェクト自体は Java 1.1 まで導入されませんでした)。パターン化することを非常に好む一部の人たちは、すべての型が、対応する「ヌル・オブジェクト」 (つまりその型のインスタンス (実際にはサブタイプ)) を持つようにし、そのすべてのメソッドを何もしないように無効にするべきである、と提案していますが、これは実際には非常に面倒であることがわかりました。また C# の設計者達は C# 1.0 のリリース後、ヌル可能性に対して、まったく異なる方法を採用することを決定しました。

C# 2.0 では Nullable 型の概念が導入されています。この概念は要するに、任意の具体的な値型 (つまりプリミティブ型) をジェネリック・クラスあるいはテンプレート・クラスにラップすることによって、この型がヌルをサポートできるようにするための構文 (Nullable<T>) のサポートを追加するものです。この構文自体は型宣言の中で ? 修飾子を使うことで暗黙的に導入されます。したがって、int? はヌルの可能性がある整数を示すことになります。

表面的に見ると、これは妥当な決定のように思えますが、このようにすると突然、さまざまなものが複雑になってしまいます。int 型と int? 型は互換性があると考えるべきなのでしょうか。もし互換性があるとすると、どういう場合に intint? に格上げされ、あるいは逆に int?int に格下げされるのでしょう。int?int が加算されたら何が起こるのでしょう。その結果がヌルになることはあるのでしょうか。等々です。その後いくつかの大きな紆余曲折を経て、Nullable 型は 2.0 の一部としてリリースされました。しかし C# プログラマーはほとんど完全に Nullable 型を無視しました。

振り返ってみると、Option[T]Int の間を明確に区別するオプション型による関数型の方法は、他の方法と比べると、より単純なように思えます。Nullable 型に関わる直感に反したいくつかの格上げルールと比較した場合は、特にそう思えます (関数型の世界では、この概念についての検討が 20 年近く行われてっています)。Option[T] は慣れるまで少し時間がかかりますが、一般的には、より明確なコードを作成でき、期待どおりの結果をもたらしてくれるようです。

今回は、いよいよ Scala による関数型プログラミングに本格的に突入し、大部分の関数型言語に共通の 4 つの型、つまりリスト型、タプル型とセット型、そしてオプション型について調べます。また、実際のところ他の関数型言語にはほとんど見られない、Scala の配列についても学びます。 これらの型はどれも、コードを作成する際に新しい考え方を導入するために役立ちます。これらの型を従来のオブジェクト指向の機能と組み合わせることによって、驚くほど簡潔な結果を得ることができます。

オプション型を用いる

「何もない」が実際には「何もなくはない」のはどういう場合でしょう。ヌルではなく 0 であるのはどういう場合でしょう。

ソフトウェアの世界では、私達の大部分が非常によく理解している概念でありながら、「何もない」を表現しようとすると驚くほど困難なことがわかります。例えば C++ コミュニティーの中で盛んに行われた NULL と 0 に関してのあらゆる議論や、SQL コミュニティーの中で行われた NULL 列の値に関する盛んな激論を調べてみてください。大部分のプログラマーは NULL (あるいはお好みによってはヌル) を使うことで「何もない」ことについて考えますが、これによって Java プログラミングではいくつか具体的な問題が起こります。

例えば、あるプログラマーの給料をメモリー内またはディスク上のデータベースから検索するように設計された単純な操作を考えてみてください。この API は呼び出し側がプログラマーの名前を含む String を渡せるように設計されており、そしてこの API が返すのは・・・何なのでしょう?モデリングの観点から見ると、この API は、そのプログラマーの年収を示す Int を返す必要があります。しかしここには厄介な問題があります。そのプログラマーがデータベースの中にない場合には何を返すのでしょう。 (そのプログラマーは雇用されたことがないかもしれず、あるいは解雇されたのかもしれず、あるいは名前の入力が誤っているのかもしれません。) もし戻り型が Int であるとすると、(データベースの中でユーザーを見つけることができなかったことを示すために通常使われる「フラグ」である) ヌルを返すことはできません。(例外をスローする必要があると考える人もいるかもしれませんが、データベースの中で値が見つからないという事態はほとんどの場合、実際には例外的な条件ではありません。そのため、この場合に例外をスローするのは不適切です。)

Java コードでは結局、そのメソッドが java.lang.Integer を返すように変更することになります (java.lang.Integer によって、このメソッドがヌルを返す可能性があることを呼び出し側に強制的に認識させます)。当然ながら、このシナリオを完全に文書化してくれるようにプログラマーに頼ることになり、また注意深く作成されたドキュメンテーションをプログラマーが読んでくれることを期待することになります。そのとおり。これはちょうど、課された締め切りは不可能だという私達プログラマーの反対意見を上司が十分に聞き入れ、そうした反対意見を会社の幹部や顧客にフィードバックしてくれることを期待するのと同じようなものです。

Scala は関数型による一般的な方法を提供することによって、この難題に対応します。その方法であるオプション型、つまり Option[T] は、ある面、簡単に説明できるようなものではありません。Option[T]Some[T] と None というサブクラスのみを持つジェネリック・クラスであり、「値がない」可能性を伝えるために使われますが、この概念をサポートするために言語の型システムが大きな変更を余儀なくされることはありません。実際、Option[T] 型を使うことで (次のセクションでは Option[T] 型の使い方を説明します)、さまざまなものがずっと明確になります。

Option[T] を扱う際に重要なことは、Option[T] は本来、「何もない」という値の可能性を表現するために None という別の値を使う、強く型付けされたサイズ「1」のコレクションである、と理解することです。従って、データが見つからなかったことを示すためにメソッドがヌルを返すのではなく、Option[T] を返すようにそのメソッドを宣言するのです (T は戻り型として返される、元々の型です)。そして何もデータが見つからないシナリオでは単純に None を返します。そのため次のようになります。

リスト 1. フットボールの準備をする
  @Test def simpleOptionTest =
  {
    val footballTeamsAFCEast =
      Map("New England" -> "Patriots",
          "New York" -> "Jets",
          "Buffalo" -> "Bills",
          "Miami" -> "Dolphins",
          "Los Angeles" -> null)
    
    assertEquals(footballTeamsAFCEast.get("Miami"), Some("Dolphins"))
    assertEquals(footballTeamsAFCEast.get("Miami").get(), "Dolphins")
    assertEquals(footballTeamsAFCEast.get("Los Angeles"), Some(null))
    assertEquals(footballTeamsAFCEast.get("Sacramento"), None)
  }

Scala の Map に対する get の戻り値が、渡されたキーに対応する実際の値ではないことに注意してください。この戻り値は Option[T] インスタンス (対象としている値をラップする Some() か、あるいは None) であり、これによってその渡されたキーがマップの中に見つからなかった場合にそのことが明確にわかります。これが特に重要となるのは、指定されたキーがマップの中に存在し、しかもキーに対応する値としてヌルを持つことが受け入れられる場合です (例えばリスト 1 の Los Angeles キーの場合など)。

Option[T] を扱う際、プログラマーはほとんどの場合パターン・マッチングを使います。パターン・マッチングは関数型の性質を非常によく表した概念であり、両方の型や値を効果的に「切り替える」ことができ、しかも言うまでもなく、定義の時点で変数に値をバインドしたり、Some()None とを切り替えたり、また使えなくなった get() メソッドを呼び出すことなく Some から値を抽出することもできます。リスト 2 は Scala のパターン・マッチングの実際を示しています。

リスト 2. パターン・マッチングの例
  @Test def optionWithPM =
  {
    val footballTeamsAFCEast =
      Map("New England" -> "Patriots",
          "New York" -> "Jets",
          "Buffalo" -> "Bills",
          "Miami" -> "Dolphins")
          
    def show(value : Option[String]) =
    {
      value match
      {
        case Some(x) => x
        case None => "No team found"
      }
    }
    
    assertEquals(show(footballTeamsAFCEast.get("Miami")), "Dolphins")
  }

タプルとセット

C++ では構造体と呼びました。Java プログラミングではデータ転送オブジェクトまたはパラメーター・オブジェクトと呼びました。それを Scala ではタプルと呼びます。要するにタプルは、単にいくつかの異なるデータ型を 1 つのインスタンスの中に集めたクラスであり、そのためにほとんど、あるいはまったくカプセル化や抽象化を必要としません。実際、抽象化が行われない方が便利なことが多いのです。

Scala の中でタプル型を作成する作業は信じられないほど簡単です。それがタプルの魅力の 1 つですが、例えばタプル型の内部の要素が最初から公開される場合には、要素を表すような名前を付ける意味がありません。リスト 3 を考えてみてください。

リスト 3. tuples.scala
// JUnit test suite
//
class TupleTest
{
  import org.junit._, Assert._
  import java.util.Date
 
  @Test def simpleTuples() =
  {
    val tedsStartingDateWithScala = Date.parse("3/7/2006")

    val tuple = ("Ted", "Scala", tedsStartingDateWithScala)
    
    assertEquals(tuple._1, "Ted")
    assertEquals(tuple._2, "Scala")
    assertEquals(tuple._3, tedsStartingDateWithScala)
  }
}

タプルの作成は、値を括弧でくくるだけ、という単純なものです (メソッドを呼び出す際に渡す値を括弧でくくるのとほとんど同じです)。値の抽出は「_n」メソッドを呼び出せばよいだけです (n は対象とするタプル要素の位置を表す引数であり、_1 は1 番目、_2 は 2 番目、等々です)。そうすると Java でのいわゆる java.util.Map は、基本的に 2 つの部分から成るタプルのコレクションということになります。

タプルによって、複数の値を 1 つのエンティティーとして非常に簡単に扱えるようになります。これはつまり、Java プログラミングでは非常に困難であった、複数の戻り値がタプルによって可能になるということです。例えば、あるメソッドが String の中にある文字の数をカウントし、その String の中で最も頻繁に現れる文字を返すとします。もしプログラマーが、最も頻繁に現れる文字と、その文字が現れる回数の両方を知りたいとすると、設計が面倒になります。その文字とその文字のカウントの両方を含む明示的なクラスを作成するか、あるいはオブジェクトの中にフィールドとして値を保持し、要求された場合にはこれらのフィールドの値を返す必要があります。いずれの方法の場合も、Scala のコードと比較すると非常に長いコード・セットとなります。Scala では、その文字とその文字のカウントを含むタプルを単純に返し、さらには各値に容易にアクセスできる「_1」や「_2」もタプルの中に含めることで、複数の戻り値を容易に返すことができるのです。

次のセクションで説明するように、Scala のプログラマーはよく、Option とタプルを (Array[T] やリストなどの) コレクションの中に保存します。こうすることによって、比較的簡単な構成体を使って驚くほどの柔軟性と強力さを実現することができます。


配列の強力さ

まず、古くから慣れ親しんでいて、今では新たに Array[T] の管理の下にある、配列を再検証してみましょう。Java コードでの配列と同様、Scala の Array[T] は順序付きの要素シーケンスであり、配列内の位置を示す数値でインデックスが付けられており、この数値はその配列の合計サイズを超えてはなりません (リスト 4)。

リスト 4. array.scala
object ArrayExample1
{
  def main(args : Array[String]) : Unit =
  {
    for (i <- 0 to args.length-1)
    {
      System.out.println(args(i))
    }
  }
}

Scala での配列は Java コードでの配列と等価です (Scala での配列は最終的にはコンパイルされて Java コードでの配列になります) が、Scala での配列の定義方法は明らかに異なっています。まず、Scala での配列は実質的にジェネリック・クラスであり、最初から配列として Scala に存在しているわけではありません (少なくとも、Scala ライブラリーに含まれているクラスと同程度以上のものではありません)。例えば Scala では、配列は Array[T] のインスタンスとして正式に定義されます。Array[T] はいくつかの興味深いメソッドが定義されているクラスであり、そうしたメソッドの中には、よくある length メソッドなども含まれています (length メソッドはもちろん配列の長さを返します)。従って Scala では、これまでと同じように Array を使うことができます。例えば Int を使って 0 から args.length - 1 まで繰り返し処理を行い、その配列の i 番目の要素を取得する、といったことができます。(どの要素を返すかを指定するのに大括弧ではなく小括弧を使いますが、この小括弧も実は例の奇妙な名前を持つメソッドの 1 つです。)

しかし今述べた内容は面白い (fun) 話でもなく、役に立つ (fun(ctional)) わけでもありません。(つまらないダジャレを失礼しました。先へ行きましょう。)

ヒント

Array[T] の完全な階層については、Scaladocs を参照してください。この階層構造は非常によくできており、多くの点で java.util Collections クラスを連想させます。

配列の拡張

Array は驚くほどリッチな親の階層構造を継承しているため、Array には大量のメソッドがあります。つまり ArrayArray0 を継承し、Array0ArrayLike[A] を継承し、ArrayLike[A]Mutable[A] を継承し、Mutable[A]RandomAccessSeq[A] を継承し、RandomAccessSeq[A]Seq[A] を継承し、等々です。当然ながら、このように親がたくさんあることは Array に対して多種多様な操作を行えることを意味しており、そのため Scala では Java プログラミングの場合よりも容易に配列を操作できるようになっています。

例えば、(リスト 4 のような) 配列全体にわたっての繰り返しは (当然ながら) ずっと容易に行うことができ、(Iterable という trait から継承する) foreach メソッドを使うことによって、(当然とは言えないまでも) より関数型の特徴を利用した方法で行うことができます。

リスト 5. ArrayExample2
object 
{
  def main(args : Array[String]) : Unit =
  {
    args.foreach( (arg) => System.out.println(arg) )
  }
}

あまり簡単になったように見えないかもしれませんが、特定のセマンティクス (この場合は配列全体にわたっての繰り返しを行う状況) の下で実行するために別のクラスに (匿名あるいは匿名ではない) 関数を渡せる機能は、関数型プログラミングの一般的なテーマです。また、高階関数のこうした使い方は、決して繰り返し操作に限定されているわけではありません。実際、ふさわしくない候補を取り除くために配列の内容に対して何らかのフィルタリング・プロセスを行い、その結果を何らかの形で処理することは珍しいことではありません。例えば Scala では、フィルタリング用に filter メソッドを使用し、その結果のリストを取り上げ、各要素を map ともう 1 つの関数 (この場合の型は (T) => U (この T と U も共にジェネリック型)) を使うか、あるいは再度 foreach を使うかして処理することが容易なのです。リスト 6 では後者の方法を試しています。(filter(T) : Boolean という方法を使っていることに注意してください。これはつまり、filter はその配列が持つ任意の型のパラメーターを 1 つ引数に取り、Boolean を返すということです。)

リスト 6. すべての Scala プログラマーを検索する
class ArrayTest
{
  import org.junit._, Assert._
  
  @Test def testFilter =
  {
    val programmers = Array(
        new Person("Ted", "Neward", 37, 50000,
          Array("C++", "Java", "Scala", "Groovy", "C#", "F#", "Ruby")),
        new Person("Amanda", "Laucher", 27, 45000,
          Array("C#", "F#", "Java", "Scala")),
        new Person("Luke", "Hoban", 32, 45000,
          Array("C#", "Visual Basic", "F#")),
		new Person("Scott", "Davis", 40, 50000,
		  Array("Java", "Groovy"))
      )

    // Find all the Scala programmers ...
    val scalaProgs =
      programmers.filter((p) => p.skills.contains("Scala") )
    
    // Should only be 2
    assertEquals(2, scalaProgs.length)
    
    // ... now perform an operation on each programmer in the resulting
    // array of Scala programmers (give them a raise, of course!)
    //
    scalaProgs.foreach((p) => p.salary += 5000)
    
    // Should each be increased by 5000 ...
    assertEquals(programmers(0).salary, 50000 + 5000)
    assertEquals(programmers(1).salary, 45000 + 5000)
    
    // ... except for our programmers who don't know Scala
    assertEquals(programmers(2).salary, 45000)
	assertEquals(programmers(3).salary, 50000)
  }
}

map 関数は、新しい Array を作成することになっていた場所で元の配列の内容はそのままで使われますが、実際のところ、関数型言語のプログラマーの大部分はこの方法を好みます。

リスト 7. filter と map
  @Test def testFilterAndMap =
  {
    val programmers = Array(
        new Person("Ted", "Neward", 37, 50000,
          Array("C++", "Java", "Scala", "C#", "F#", "Ruby")),
        new Person("Amanda", "Laucher", 27, 45000,
          Array("C#", "F#", "Java", "Scala")),
        new Person("Luke", "Hoban", 32, 45000,
          Array("C#", "Visual Basic", "F#"))
		new Person("Scott", "Davis", 40, 50000,
		  Array("Java", "Groovy"))
      )

    // Find all the Scala programmers ...
    val scalaProgs =
      programmers.filter((p) => p.skills.contains("Scala") )
    
    // Should only be 2
    assertEquals(2, scalaProgs.length)
    
    // ... now perform an operation on each programmer in the resulting
    // array of Scala programmers (give them a raise, of course!)
    //
    def raiseTheScalaProgrammer(p : Person) =
    {
      new Person(p.firstName, p.lastName, p.age,
        p.salary + 5000, p.skills)
    }
    val raisedScalaProgs = 
      scalaProgs.map(raiseTheScalaProgrammer)
    
    assertEquals(2, raisedScalaProgs.length)
    assertEquals(50000 + 5000, raisedScalaProgs(0).salary)
    assertEquals(45000 + 5000, raisedScalaProgs(1).salary)
  }

リスト 7 では Personsalary メンバーに「val」を付けることで不変にしていることに注目してください。これまでは、さまざまなプログラマーの給料の値を変更するために「var」を付ける必要がありました。

Scala の Array は、ここにリストアップしたり実例を示したりできないほど多くのメソッドを持っています。配列を扱う際の一般的な助言として、従来の for ... パターンを使うよりも Array に提供されているメソッドを利用する方法を積極的に探し、その方法を使って配列をひととおり調べ、必要なものを見つけたり、必要な動作を行ったりしてください。そのために最も容易な方法は通常、必要な動作を行う関数 (場合によっては例えばリスト 7 の testFilterAndMap の例のようにネストした関数) を作成し、その関数を (要求される結果に応じて) mapfilterforeach、あるいは他の Array のメソッドの 1 つに渡す方法です。


リストを関数型で活用する

リストは長年にわたって関数型プログラミングのコア機能となっており、オブジェクトの世界での配列が長年にわたってそうであるように、「最初からあるもの」と思われています。リストは関数型のソフトウェアを作成するための基本であり、従って皆さんは (新進の Scala プログラマーとして) リストとその動作について理解できている必要があります。たとえ新しい設計にリストが使われることがないとしても、Scala のコードでは、ライブラリー全体にわたって広範にリストが使われているため、リストについて学ぶことは必須 (imperative) なのです。

(必須 (imperative) は、関数型に対する命令型 (imperative) をかけてみました・・・。失礼しました。もう 2 度とやりません。)

リストのコアとなる定義は Scala ライブラリーの List[T] という標準クラスであるという意味で、Scala でのリストは配列と似ています。そして List[T]Array[T] と同様、直下の基底クラスとしての Seq[T] を始め、いくつかの基底クラスや trait から継承します。

リストは基本的に要素のコレクションであり、これらの要素はリストの先頭または末尾から抽出することができます。このリストというものは、Lisp に由来しています。雑学マニアなら覚えているかもしれませんが、Lisp という名前は主に「LISt Processing (リスト処理)」を中心とする言語という意味なのです。Lisp では、リストの先頭を取得する場合には car 操作を行い、リストの末尾を取得する場合には cdr 操作を行います。(これらの操作の名前が付いた歴史的な理由を最初に教えてくれた人にはボーナス・ポイントを差し上げます。)

実際のところ、リストの処理は多くの点で配列の処理よりも簡単です。その理由としては、関数型言語は歴史的にリスト処理を非常によくサポートしているためでもあり (Scala もこの点を継承しています)、またリストは合成や分解が容易なためでもあります。例えば、ある関数がリストの内容の一部を分離して取り出したい、という場合がよくあります。そのために関数は、リストの最初の要素 (先頭) を切り取り、その要素に対して処理を行い、そしてリストの残り部分を再帰的に関数自身に渡します。これによって、処理コードの中で何らかの共有状態が発生する可能性が大幅に低下し、また (処理が単純ではない場合には) コードが複数のスレッドに分割され、各ステップが 1 つの要素のみを処理すればよくなる可能性が高くなります。

リストの統合や分割は非常に単純です (リスト 8)。

リスト 8. リストの処理
class ListTest
{
  import org.junit._, Assert._
  
  @Test def simpleList =
  {
    val myFirstList = List("Ted", "Amanda", "Luke")
    
    assertEquals(myFirstList.isEmpty, false)
    assertEquals(myFirstList.head, "Ted")
    assertEquals(myFirstList.tail, List("Amanda", "Luke")
    assertEquals(myFirstList.last, "Luke")
  }
}

リストの作成が配列の作成とほとんど同じであることに注目してください。どちらの動作も通常のオブジェクトの作成に関しては似ていますが、リストを作成する場合には「new」が必要ない点が異なります。(これは「case class」の機能ですが、これについては今後の記事で説明します)。特に、tail メソッドによる呼び出しの結果に注意してください。この結果はリストの最後の要素ではなく (リストの最後は last で指定されます)、最初の要素を除いた残りのリストなのです。

もちろん、リストの強力さの一端はリスト要素の再帰処理にあります。再帰処理というのは単にリストが空になるまでリストの先頭を抽出し、そしてその結果を累積していくことを意味します。

リスト 9. 再帰処理
  @Test def recurseList =
  {
    val myVIPList = List("Ted", "Amanda", "Luke", "Don", "Martin")
    
    def count(VIPs : List[String]) : Int =
    {
      if (VIPs.isEmpty)
        0
      else
        count(VIPs.tail) + 1
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

もしここで count の戻り型を省略すると、Scala のコンパイラーやインタープリターに怒られることになります。なぜなら、これは末尾からの再帰呼び出し (再帰処理を多用する操作で作成されるスタック・フレームの数を減らすための最適化) であるため、戻り型の指定が必要なためです。確かに、リストの中にある項目の数を得るためには単純に List の length メンバーを使った方が簡単ですが、ここでのポイントはリスト処理がいかに強力であるか、その概略を示すことです。リスト 9 のメソッド全体は完全にスレッド・セーフです。それが確かであるという理由は、この処理の間に使われる中間的な状態はすべてスタック上のパラメーターの中に保持されるため、その定義から言って、複数スレッドからアクセスされることがないからです。関数型の手法の素晴らしい点は、関数型のプログラムを作成した上で共有状態を作り出す、ということが実際には非常に困難である点です。

リストの API

リストには他にもいくつか興味深い性質があります。例えば、:: メソッドを使ってリストを作成することができます。(そうです。:: はメソッドです。これも奇妙な名前を持つメソッドの 1 つにすぎず、特別注目するようなものはないので、先を読み進んでください。) そのため、「List」を作成するための構文を使ってリストを作成する代わりに、「cons」(二重コロンのメソッドはこう呼ばれます) を使ってリストを合成することができます。つまり次のようなことができます。

リスト 10. :: (二重コロン) は C++ では何に相当するでしょう?
  @Test def recurseConsedList =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    def count(VIPs : List[String]) : Int =
    {
      if (VIPs.isEmpty)
        0
      else
        count(VIPs.tail) + 1
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

ただし :: メソッドを使う場合には注意してください。このメソッドにはいくつか奇妙なルールが導入されています。この構文は関数型言語では非常に一般的なため、Scala の作成者達はこの構文をサポートすることにしたのですが、この構文が適切かつ一般的な動作をするためには、1 つ奇妙なルールに従う必要があります。つまり「奇妙な名前を持つメソッド」のうちコロンで終わるものは、すべて右結合です。右結合というのは式全体を一番右から評価し始めるという意味であり、この場合の右端は Nil、そして便利なことに NilList です。これはつまり、:: は (ここで使われている) String のメンバー・メソッドではなく、グローバルな :: メソッドであると判断することができます。ということはつまり、何からでもリストを作り出せるということです。:: を使う際には、最も右にある要素はリストである必要があり、そうでないとエラー・メッセージを受け取ることになります。

右結合とは何か

:: によって何が起きるかを理解するためには、演算子 (例えば「cons」演算子など) は単に奇妙な名前を持つメソッドにすぎない、ということを念頭に置く必要があります。通常の左結合の構文が指定された場合、メソッド名 (右側にあるトークン) を呼び出す対象となるオブジェクトは左側にあるトークンです。つまり通常は、1+2 という式はコンパイラーにとっては 1.+(2) と見なされます。

しかしリストの場合には、このようには動作しません。もしこのように動作したとすると、システムの中のすべてのクラスはシステムの中のすべての型に対して :: メソッドを持たなければならなくなってしまい、これは恐ろしいほどコンサーン (関心事) の分離に違反します。

Scala はこれを、コロンで終わる奇妙な名前を持つメソッド (例えば :::::、さらには私が勝手に作った foo: に至るまで) はすべて右結合で扱われるとすることで解決しています。例えば a :: b :: c :: Nil は Nil.::(c.::(b.::(a))) と解釈されますが、これはまさに私が望んでいるとおりのことです。List によってすべてが起動され、:: への各呼び出しはオブジェクト・パラメーターを引数に取り、List を返すことでチェーンが継続されます。

他の命名規則に対しても右結合の性質を指定できるとよいのですが、この記事の執筆時点でこのルールは Scala 言語の中にハードコーディングされています。今のところ、右結合の動作を実行させられる文字はコロンのみです。

Scala でのリストの使い方のうち最も強力な使い方の 1 つは、パターン・マッチングと組み合わせる方法です。この方法では、リストによって型と値の両方に対して突き合わせを行えるだけではなく、突き合わせと同時にさまざまな結合も行えるためです。例えばパターン・マッチングを使用して、少なくとも 1 つの要素を持つリストと、空のリストとの違いを認識することで、リスト 10 のリストに関するコードを単純化することができます。

リスト 11. リストの中でのパターン・マッチング
  @Test def recurseWithPM =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    def count(VIPs : List[String]) : Int =
    {
      VIPs match
      {
        case h :: t => count(t) + 1
        case Nil => 0
      }
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
  }

最初の case 式で、リストの先頭 (head) が抽出されて変数 h にバインドされますが、リストの残り部分 (末尾 (tail)) は t にバインドされます。この場合、h に対しては何も行われません (実際のところ、先頭は使われないということを示すために h をワイルドカード _ で置き換え、使われない変数用のプレースホルダーであることを示した方が適切かもしれません)。しかし t は先ほどの例とまったく同じように、再帰的に何度も count に渡されます。これも思い出して欲しいのですが、Scala ではすべての式が暗黙的に値を返します。この場合でのパターン・マッチングの式の結果は、count + 1 への再帰的呼び出しか、あるいはリストの最後に達した時には 0 です。

どちらもコードの行数は同じ程度であることを考慮すると、パターン・マッチングを使うことによる価値はどこにあるのでしょう。正直なところ、このように単純な例では、その価値を見出すことは困難です。しかし、この例より少しでも複雑な場合 (例えばこの例を拡張することによって特定の値を取得するなど) を改めて検討してみるとよいでしょう。

リスト 12. Amanda とのパターン・マッチング
  @Test def recurseWithPMAndSayHi =
  {
    val myVIPList = "Ted" :: "Amanda" :: "Luke" :: "Don" :: "Martin" :: Nil
    
    var foundAmanda = false
    def count(VIPs : List[String]) : Int =
    {
      VIPs match
      {
        case "Amanda" :: t =>
          System.out.println("Hey, Amanda!"); foundAmanda = true; count(t) + 1
        case h :: t =>
          count(t) + 1
        case Nil =>
          0
      }
    }
    
    assertEquals(count(myVIPList), myVIPList.length)
    assertTrue(foundAmanda)
  }

非常に複雑な例、特に正規表現や XML ノードなどでの複雑な例では、パターン・マッチングによる方法の方が有利であることは、すぐにわかるはずです。またパターン・マッチングはリストに限定されるわけではなく、パターン・マッチングを拡張して先ほどの配列の例に適用することもできます。実際、上記の recurseWithPMAndSayHi テストを配列に適用した例を次に示します。

リスト 13. 配列に対するパターン・マッチング
  @Test def recurseWithPMAndSayHi =
  {
    val myVIPList = Array("Ted", "Amanda", "Luke", "Don", "Martin")

    var foundAmanda = false
    
    myVIPList.foreach((s) =>
      s match
      {
        case "Amanda" =>
          System.out.println("Hey, Amanda!")
          foundAmanda = true
        case _ =>
          ; // Do nothing
      }
    )

    assertTrue(foundAmanda)
  }

演習を希望する人は、リスト 13 を再帰型にしたものを作成してみてください。つまり recurseWithPMAndSayHi スコープの中で宣言される、可変 var を使わずにカウントも行うようにするのです。ヒントとして、複数のパターン・マッチング・ブロックが必要になるかもしれません。(この記事のコードのダウンロードの中には 1 つの解が含まれていますが、それを見る前に皆さん自身で試してみてください。)


まとめ

このシリーズについて

このシリーズでは、Ted Neward が皆さんと共に Scala プログラミング言語を深く掘り下げます。developerWorks の、この新しいシリーズでは、Scala が最近もてはやされている理由を調べ、Scala の言語機能の実際の動作を調べます。Scala のコードと Java のコードの比較が重要な場合には両者のコードを並べて示しますが、(これから学ぶように) Scala の機能のうちの多くは、Java には直接対応するものがありません。そして Scala の魅力の多くがあるのはそこなのです。結局のところ、Java で可能ならば、手間をかけて Scala を学ぶ必要はないのです。

Scala にはコレクション型が豊富に用意されていますが、これは関数型という Scala の歴史、そして Scala の持つ機能セットによる直接の結果です。つまり、緩く結合した一連の値をタプルによって容易にまとめることができ、「何も値がない」ことと「何らかの値がある」ことを Option[T] によって直接的な方法で簡単に示すことができ、配列に関しては従来の Java スタイルのセマンティクスを利用できる上にさらに機能強化が図られ、そしてリストは関数型言語の基本的なコレクションである、等々です。

ただし、これらの機能を使う際には、特にタプルを使う際には注意してください。タプルを使いながら、直接タプルを使うことを優先して従来の基本的なオブジェクト・モデリングを忘れる、という事態に陥りやすいのです。ある特定のタプル (例えば既知の名前や年齢、給料、プログラミング言語のリストなど) がコード・ベースの中に頻繁に現れる場合には、そのタプルを正式なクラス型とオブジェクトとしてモデリングしてしまうことです。

Scala の素晴らしい点は、関数型であると同時にオブジェクト指向であることです。そのため、たとえ Scala の関数型を学ぶ一方で、クラスの設計には従来と同じようにできるのです。


ダウンロード

内容ファイル名サイズ
Sample Scala code for this articlej-scala06278.zip200KB

参考文献

学ぶために

  • 多忙な Java 開発者のための Scala ガイド: オブジェクト指向のための関数型プログラミング」(Ted Neward 著、developerWorks、2008年1月) はこのシリーズの第 1 回として、何よりも Scala の概要と、並行性に対して Scala が持つ関数型の手法を解説しています。
  • Functional programming in the Java language」(Abhijit Belapurkar 著、developerWorks、2004年7月) は、Java 開発者の視点から関数型プログラミングの利点と使い方を説明しています。
  • COBOL のように死んだ言語」(Ted Neward 著、developerWorks、2008年5月) では、Java プラットフォームを捨ててもっと優れたものに移行する時が来ているのかどうか、Ted Neward が Java の終焉を支持する人と反対する人との間の議論を考察しています。
  • ポッドキャスト「Ted Neward on why we need Scala」(JavaWorld、2008年6月) を聞いてください。Ted Neward が Andrew Glover と、Java エコシステムにおける関数型プログラミングと Scala の位置付けについて話をしています。
  • Scala by Example」(Martin Odersky 著、2008年5月) はコードを中心に Scala を簡潔に紹介しています (原文は PDF)。
  • Programming in Scala』(Martin Odersky、Lex Spoon、Bill Venners の共著、2007年12月 Artima 刊) は 1 冊の本の長さで Scala を紹介した最初の資料です。
  • Scaladocs は Scala ライブラリーの API の仕様です。
  • developerWorks の Java technology ゾーンには、Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

議論するために

コメント

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=323661
ArticleTitle=多忙な Java 開発者のための Scala ガイド: コレクション型
publish-date=06272008