M.Hiroi's Home Page

お気楽 Scala プログラミング入門

入れ子クラスとイテレータ


Copyright (C) 2014-2024 Makoto Hiroi
All rights reserved.

はじめに

前回は多相的で immutable なデータ構造として「連結リスト」と「二分木」を作成しました。今回は「内部クラス (inner class)」と「イテレータ (iterator)」という機能を使って、mutable な双方向リストのプログラムを改良してみましょう。

●入れ子クラスとは?

Java では、クラスもしくはメソッドの中で定義されたクラスのことを「入れ子クラス」といいます。「ネストしたクラス」と呼ばれることもあります。Scala の場合、入れ子クラスは次に示す 3 通りのパターンがあります。

  1. 内部クラス (inner class)
  2. ローカルクラス
  3. 無名クラス (匿名クラス)

クラスやトレイトの中で定義されたクラスを「内部クラス」といいます。フィールド変数やメソッドと同様に、protected, private, 指定無し によるアクセス制御を行うことができます。なお、内部クラスという用語は入れ子クラスのことを指す場合もあるので注意してください。

ローカルクラスはブロックの中で定義されたクラスのことで、一般的にはメソッドの中で局所的に定義されたクラスのことをいいます。申し訳ありませんが、本稿ではローカルクラスの説明は割愛させていただきます。あしからずご了承ください。

無名クラスは名前の無いクラスのことで、抽象クラスもしくはトレイトから通常のクラス定義をせずに直接インスタンスを生成するための機能です。無名クラスはとても強力な機能で、Scala のイテレータは無名クラスを使うと簡単に実装することができます。

●内部クラス

内部クラスの定義は簡単です。次の例を見てください。

scala> class Foo {
     | class Bar { val x = 1 }
     | def foo: Int = (new Bar).x
     | }
// defined class Foo

scala> val a = new Foo
val a: Foo = Foo@5b47731f

scala> a.foo
val res0: Int = 1

scala> class Foo {
     | class Bar { private val x = 1 }
     | def foo: Int = (new Bar).x
     | }
-- [E173] Reference Error: ---------------------------------------------------------
3 |def foo: Int = (new Bar).x
  |               ^^^^^^^^^^^
  |           value x cannot be accessed as a member of Foo.this.Bar from class Foo.
  |           private value x can only be accessed from class Bar in class Foo.
1 error found

クラス Foo の中でクラス Bar を定義しています。この Bar が内部クラスになります。内部クラスは外側のクラス (Foo) の中で使用するのであれば、通常のクラスとほとんど同じですが、Java とは違って Foo から Bar の private なフィールド変数にアクセスすることはできません。

逆に、クラス Bar からクラス Foo の private なフィールド変数にはアクセスすることができます。次の例を見てください。

scala> class Foo {
     | private val x = 1
     | class Bar {
     | val y = 10
     | val z = x
     | }
     | }
// defined class Foo

scala> val a = new Foo
val a: Foo = Foo@60a7e509

scala> a.x
-- [E173] Reference Error: --------------------------------------------------
1 |a.x
  |^^^
  |value x cannot be accessed as a member of (a : Foo) from object rs$line$6.
  |  private value x can only be accessed from class Foo.
1 error found

scala> val b = new a.Bar
val b: a.Bar = Foo$Bar@443a53df

scala> b.y
val res1: Int = 10

scala> b.z
val res2: Int = 1

Foo のフィールド変数 x は private ですが、内部クラス Bar では x の値を参照することができます。Bar は Foo の中で定義されているので、Foo の private な変数やメソッドにアクセスできるのは当然のことといえます。

Foo の外側から Bar のインスタンスを生成する場合、まず Foo のインスタンスを生成し、それを使って Bar のインスタンスを生成します。上の例では、Foo のインスタンスを生成して、変数 a にセットします。次に、new a.Bar でインスタンス a の内部クラス Bar を生成します。

Scala の場合、new (new Foo).Bar で内部クラス Bar のインスタンスを生成することはできません。Foo のインスタンスは変数にセットして、その変数を使って Bar のインスタンスを生成します。このとき、Bar のインスタンスの型は変数名を使って a.Bar と表されます。

Foo のインスタンスを変数 c にセットして、Bar のインスタンスを生成すると、次のようになります。

scala> val c = new Foo
val c: Foo = Foo@503556cb

scala> val d = new c.Bar
val d: c.Bar = Foo$Bar@563ada5

scala> var b1: a.Bar = b
var b1: a.Bar = Foo$Bar@443a53df

scala> var b1: a.Bar = d
-- [E007] Type Mismatch Error: --------------------------------
1 |var b1: a.Bar = d
  |                ^
  |                Found:    (d : c.Bar)
  |                Required: a.Bar
  |
  | longer explanation available when compiling with `-explain`
1 error found

同じ内部クラスから作成されたインスタンスですが、Scala では型が異なることに注意してください。これを「パス依存型 (path-dependent types)」と呼びます。したがって、型を b.Bar と宣言した変数 b1 に変数 b のインスタンスを代入することはできますが、変数 d のインスタンスを代入することはできません。

パス依存型ではなく同じ型として扱いたい場合は、"外側のクラス名#内部クラス名" にキャストしてください。

簡単な例を示します。

scala> class Foo { class Bar }
// defined class Foo

scala> val a = new Foo
val a: Foo = Foo@1ddc8fc

scala> val b = new Foo
val b: Foo = Foo@4b1b2255

scala> val c = new a.Bar
val c: a.Bar = Foo$Bar@2262bc86

scala> val d = new b.Bar
val d: b.Bar = Foo$Bar@1181526e

scala> var e: Foo#Bar = c
var e: Foo#Bar = Foo$Bar@2262bc86

scala> var e: Foo#Bar = d
var e: Foo#Bar = Foo$Bar@1181526e

変数 e の型を Foo#Bar と宣言すれば、a.Bar のインスタンス c でも b.Bar のインスタンス d でも代入することができます。

●無名クラス

Scala は Java と同様に、クラスが定義されていなくてもインスタンスを生成することができます。生成されたインスタンスが属するクラスを「無名クラス」とか「匿名クラス」といいます。無名クラスの構文を示します。

new { ... }

{ } の中はクラスと同様にフィールド変数やメソッドを定義することができます。

簡単な例を示しましょう。

scala> new { }
val res0: Object = anon$1@2de3b052

無名クラスはブロック { } の中でクラスを定義し、その場でインスタンスを生成します。ただし、クラス名が無いのでコンストラクタを定義することはできません。Scala 2 の場合、無名クラスの型は AnyRef{型1; 型2; ... } でしたが、Scala 3 では Object{型1; 型2; ... } になります。{ } の型は定義されているフィールド変数やメソッドの型になります。

scala> val a1 = new {val x = 1; def getX: Int = x}
val a1: Object = anon$1@3350ab4

scala> val a2: Object{ val x:Int; def getX: Int} = new {val x = 1; def getX: Int = x}
val a2: Object{val x: Int; def getX: Int} = anon$1@1d9af731

scala> val b: Object{ val x:Int; def getX: Int} = a1
-- [E007] Type Mismatch Error: ----------------------------------------------------------
1 |val b: Object{ val x:Int; def getX: Int} = a1
  |                                           ^^
  |                                           Found:    (a1 : Object)
  |                                           Required: Object{val x: Int; def getX: Int}
  |
  | longer explanation available when compiling with `-explain`
1 error found

scala> val b: Object{ val x:Int; def getX: Int} = a2
val b: Object{val x: Int; def getX: Int} = anon$1@1d9af731

Scala 2 の場合、変数 a1 は AnyRef{ ... } と型推論されますが、Scala 3 では Object と推論され、型の情報が失われます。変数 a2 のように型を指定してください。

scala> b.x
-- [E007] Type Mismatch Error: -----------------------------------
1 |b.x
  |^
  |Found:    (b : Object{val x: Int; def getX: Int})
  |Required: Selectable | Dynamic
  |
  |The following import might fix the problem:
  |
  |  import scala.reflect.Selectable.reflectiveSelectable
  |
  |
  | longer explanation available when compiling with `-explain`
1 error found

scala> b.getX
-- [E007] Type Mismatch Error: -----------------------------------
1 |b.getX
  |^
  |Found:    (b : Object{val x: Int; def getX: Int})
  |Required: Selectable | Dynamic
  |
  |The following import might fix the problem:
  |
  |  import scala.reflect.Selectable.reflectiveSelectable
  |
  |
  | longer explanation available when compiling with `-explain`
1 error found

フィールド変数のアクセスやメソッドの呼び出しは、Scala 2 だと warning が表示されたのですが、Scala 3 ではエラーになります。scala.language.reflectiveCalls を import すると動作します。

scala> import scala.reflect.Selectable.reflectiveSelectable

scala> b.x
val res1: Int = 1

scala> b.getX
val res2: Int = 1

●抽象クラスとトレイトからインスタンスを生成する

無名クラスを使うと、抽象クラスまたはトレイトからインスタンスを直接生成することができます。構文を次に示します。

new 抽象クラス名 [with トレイト] { ... }
new トレイト名 [with トレイト ] { ... } 

無名クラスでも "with トレイト" でトレイトをいくつでも Mix-in することができます。簡単な例を示しましょう。

scala> abstract class Foo { def getX: Int }
// defined class Foo

scala> val a = new Foo { def getX: Int = 10 }
val a: Foo = anon$1@204bd52d

scala> a.getX
val res3: Int = 10

scala> val b = new Foo { val x = 10; def getX: Int = x }
val b: Foo = anon$1@79a1f0a1

scala> b.getX
val res4: Int = 10

scala> b.x
-- [E008] Not Found Error: -------------------------------
1 |b.x
  |^^^
  |value x is not a member of Foo
1 error found

Foo は抽象クラスで、仮想メソッド getX があります。Foo のインスタンスを無名クラスで生成する場合、{ } の中で仮想メソッドの実体を定義します。これで Foo のインスタンスを生成して、メソッド getX を呼び出すことができます。

また、無名クラスの中でフィールド変数 x を定義して、getX でその値を返すこともできます。Scala 2 の場合、無名クラスの型は Foo{val x: Int} になりますが、Scala 3 では型は Foo のままなので、フィールド変数 x に直接アクセスすることはできません。

scala> trait Bar { def getY: Int }
// defined trait Bar

scala>  val c = new Bar { def getY: Int = 20 }
val c: Bar = anon$1@749aa36f

scala> c.getY
val res5: Int = 20

scala> val d = new Foo with Bar { def getX:Int = 1; def getY:Int = 2 }
val d: Foo & Bar = anon$1@1f27b6fc

scala> d.getX
val res6: Int = 1

scala> d.getY
val res7: Int = 2

scala> val e = new Foo with Bar { val x = 1; val y = 2; def getX:Int = x; def getY:Int = y }
val e: Foo & Bar = anon$1@5970734c

scala> e.x
-- [E008] Not Found Error: -------------
1 |e.x
  |^^^
  |value x is not a member of Foo & Bar
1 error found

scala> e.getX
val res8: Int = 1

scala> e.y
-- [E008] Not Found Error: ------------
1 |e.y
  |^^^
  |value y is not a member of Foo & Bar
1 error found

scala> e.getY
val res9: Int = 2

Bar はトレイトで、仮想メソッド getY があります。Bar のインスタンスを生成する場合も、{ } の中で getY の実体を定義します。Bar はトレイトなので、Foo に Mix-in することができます。この場合、getX と getY の実体を定義しないといけません。

生成されるインスタンスの型は Foo & Bar となり、Foo と Bar の共通の型 (Foo かつ Bar) を表します。これを Intersection Types といい、Scala 3 で導入された機能です。フィールド変数 x, y を定義している場合、Scala 2 では Foo with Bar{val x: Int, val y: Int} という型になりますが、Scala 3 では Foo & Bar のままです。フィールド変数 x, y に直接アクセスすることはできません。

●イテレータ

「イテレータ (iterator)」はコレクションの要素を順番にアクセスするための機能です。日本語では「反復子」と呼ばれることもあります。Scala のイテレータはトレイト (Iterator) として定義されていて、各々のコレクションには Iterator に変換するメソッド iterator が用意されています。なお、for 式を使用したい場合はメソッド foreach を実装してください。イテレータだけを実装しても for 式は動作しません。ご注意くださいませ。

Iterator は次に示す抽象メソッドを持つトレイトです。

リスト : Iterator トレイト

trait Iterator[+A] {
    def hasNext: Boolean
    def next(): A
} 

Iterator のメソッド hasNext はコレクションに次の要素があるとき真を返し、無ければ false を返します。メソッド next はコレクションから次の要素を取り出して返します。

それでは簡単な例として、奇数列を生成するイテレータを作ってみましょう。次の例を見てください。

scala> val iter = new Iterator[Int] {
     | private var i = -1
     | def hasNext: Boolean = true
     | def next(): Int = {i += 2; i}
     | }
val iter: Iterator[Int] = <iterator>

scala> iter.hasNext
val res0: Boolean = true

scala> iter.next()
val res1: Int = 1

scala> iter.next()
val res2: Int = 3

scala> iter.next()
val res3: Int = 5

scala> iter.next()
val res4: Int = 7

scala> iter.next()
val res5: Int = 9

Int を返すので、型は Iterator[Int] になります。private なフィールド変数 i を -1 に初期化します。hasNext は無条件に true を返し、next は i に 2 を加算して、その値を返します。あとは、iter.next を呼び出すたびに、1, 3, 5, ... と奇数列が生成されていきます。とても簡単ですね。

●双方向リストの改良

それでは入れ子クラスを用いて双方向リストのプログラムを改良してみましょう。最初にクラス Cell と ItemCell を修正します。Cell と ItemCell は双方向リストを構成する部品なので、他のクラスから利用されることはありません。このような場合、Cell と ItemCell をクラス DList[A] の中で定義することができます。プログラムは次のようになります。

リスト : 双方向リスト

class DList[A] {
  // セル (内部クラス)
  private class Cell(var prev: Cell = null, var next: Cell = null)
  private class ItemCell(var item: A) extends Cell

  private val head: Cell = new Cell()
  head.prev = head
  head.next = head
  //
  // メソッドの定義 (省略)
  // 
}

Cell と ItemCell は private なクラスとして宣言します。内部クラスから DList の型パラメータ A を参照できるので、多相クラスにする必要はありません。あとは、メソッドの型で Cell[A] と ItemCell[A] を Cell と ItemCell に書き換えるだけです。

次はイテレータを返すメソッド iterator を作ります。

リスト : 双方向リストのイテレータ

  def iterator(): Iterator[A] =
    new Iterator[A] {
      private var cp = head.next
      def hasNext: Boolean = cp != head
      def next(): A = 
        if (cp != head) {
          cp match {
            case that: ItemCell => {cp = cp.next; that.item}
          }
        } else throw new java.util.NoSuchElementException
    }

new Iterator[A] で Iterator のインスタンスを生成して返します。フィールド変数 cp は先頭のセル head.next で初期化します。無名クラスは内部クラスと同様に、外側のクラスのインスタンス変数にアクセスすることができます。メソッド hasNext は cp が head でなければ真を返します。メソッド next はセル cp の要素 item を返します。そして、cp を次のセルへ移動します。cp が head と等しいときはすべての要素を出力したので、エラーを送出します。

●実行例

それでは実行してみましょう。

scala> :load dlist.scala
// defined class DList
// defined class Stack
// defined class Queue

scala> val a = new DList[Int]
val a: DList[Int] = DList()

scala> for (i <- 1 to 4) a.insert(-1, i)

scala> a
val res1: DList[Int] = DList(1, 2, 3, 4)

scala> val iter = a.iterator()
val iter: Iterator[Int] = <iterator>

scala> iter.next()
val res2: Int = 1

scala> iter.next()
val res3: Int = 2

scala> iter.next()
val res4: Int = 3

scala> iter.next()
val res5: Int = 4

scala> iter.hasNext
val res6: Boolean = false

正常に動作していますね。


●プログラムリスト3

//
// dlist.scala : doubly-linked list
//
//               Copyright (C) 2014-2024 Makoto Hiroi
//

//
// mutable な双方向リスト
//
class DList[A] {
  // Cell (内部クラス)
  private class Cell(var prev: Cell = null, var next: Cell = null)
  private class ItemCell(var item: A) extends Cell

  private val head: Cell = new Cell()
  head.prev = head
  head.next = head

  private def _nth(n: Int): Cell = {
    val m = if (n >= 0) n else n.abs - 1
    var cp = if (n >= 0) head.next else head.prev
    var i = 0
    while (cp != head) {
      if (i == m) return cp
      cp = if (n >= 0) cp.next else cp.prev
      i += 1
    }
    cp
  }

  def apply(n: Int): A =
    _nth(n) match {
      case that: ItemCell => that.item
      case _ => throw new Exception("DList.apply: out of range")
    }

  def update(n: Int, x: A): Unit =
    _nth(n) match {
      case that: ItemCell => that.item = x
      case _ => throw new Exception("DList.update: out of range")
    }

  def delete(n: Int): A =
    _nth(n) match {
      case that: ItemCell => {
        that.prev.next = that.next
        that.next.prev = that.prev
        that.item
      }
      case _ => throw new Exception("DList.delete: out of range")
    }

  private def insertCell(p: Cell, q: Cell, x: A): Unit = {
    val cp = new ItemCell(x)
    cp.prev = p
    cp.next = q
    p.next = cp
    q.prev = cp
  }

  def insert(n: Int, x: A): Unit =
    if (n == 0) insertCell(head, head.next, x)
    else if (n == -1) insertCell(head.prev, head, x)
    else _nth(if (n >= 0) n - 1 else n + 1) match {
           case that: ItemCell =>
             if (n >= 0) insertCell(that, that.next, x)
             else insertCell(that.prev, that, x)
           case _ => throw new Exception("DList.insert: out of range")
         }

  def length(): Int = {
    var n = 0
    var cp = head.next
    while (cp != head) {
      n += 1
      cp = cp.next
    }
    n
  }

  def mapInto(f: A => A): Unit = {
    var cp = head.next
    while (cp != head) {
      cp match {
        case that: ItemCell => that.item = f(that.item)
      }
      cp = cp.next
    }
  }

  def positionIf(f: A => Boolean): Int = {
    var cp = head.next
    var i = 0
    while (cp != head) {
      cp match {
        case that: ItemCell => if (f(that.item)) return i
      }
      i += 1
      cp = cp.next
    }
    -1
  }

  def deleteIf(f: A => Boolean): Int = {
    var cp = head.next
    var c = 0
    while (cp != head) {
      cp match {
        case that: ItemCell =>
          if (f(that.item)) {
            that.prev.next = that.next
            that.next.prev = that.prev
            c += 1
          }
      }
      cp = cp.next
    }
    c
  }

  def foldl[B](a: B, f: (B, A) => B): B = {
    var cp = head.next
    var acc = a
    while (cp != head) {
      cp match {
        case that: ItemCell => acc = f(acc, that.item)
      }
      cp = cp.next
    }
    acc
  }

  def foldr[B](a: B, f: (A, B) => B): B = {
    var cp = head.prev
    var acc = a
    while (cp != head) {
      cp match {
        case that: ItemCell => acc = f(that.item, acc)
      }
      cp = cp.prev
    }
    acc
  }

  def foreach(f: A => Unit): Unit = {
    var cp = head.next
    while (cp != head) {
      cp match {
        case that: ItemCell => f(that.item)
      }
      cp = cp.next
    }
  }

  def isEmpty(): Boolean = head == head.next
  def clear(): Unit = {
    head.prev = head
    head.next = head
  }

  def iterator(): Iterator[A] =
    new Iterator[A] {
      private var cp = head.next
      def hasNext: Boolean = cp != head
      def next(): A =
        if (cp != head) {
          cp match {
            case that: ItemCell => {cp = cp.next; that.item}
          }
        } else throw new java.util.NoSuchElementException
    }

  override def toString: String = {
    var s = "DList("
    var cp = head.next
    while (cp != head) {
      cp match {
        case that: ItemCell =>
          if (cp.next == head) s += that.item else s += s"${that.item}, "
        case _ => ()
      }
      cp = cp.next
    }
    s += ")"
    s
  }
}

//
// mutable なスタック
//
class Stack[A] {
  private val top = new DList[A]

  def push(x: A): Unit = top.insert(0, x)

  def pop(): A =
    if (top.isEmpty()) throw new Exception("Stack is Empty")
    else top.delete(0)

  def front(): A =
    if (top.isEmpty()) throw new Exception("Stack is Empty")
    else top(0)

  def length(): Int = top.length()

  def isEmpty(): Boolean = top.isEmpty()

  def clear(): Unit = top.clear()
}

//
// mutable なキュー
//
class Queue[A] {
  private val q = new DList[A]

  def enqueue(x: A): Unit = q.insert(-1, x)

  def dequeue(): A = {
    if (q.isEmpty()) throw new Exception("Queue is Empty")
    q.delete(0)
  }

  def peek(): A = {
    if (q.isEmpty()) throw new Exception("Queue is Empty")
    q(0)
  }

  def isEmpty(): Boolean = q.isEmpty()

  def length(): Int = q.length()

  def clear(): Unit = q.clear()
}

初版 2014 年 9 月 6 日
改訂 2024 年 12 月 20 日