前回は多相的で immutable なデータ構造として「連結リスト」と「二分木」を作成しました。今回は「内部クラス (inner class)」と「イテレータ (iterator)」という機能を使って、mutable な双方向リストのプログラムを改良してみましょう。
Java では、クラスもしくはメソッドの中で定義されたクラスのことを「入れ子クラス」といいます。「ネストしたクラス」と呼ばれることもあります。Scala の場合、入れ子クラスは次に示す 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
正常に動作していますね。
// // 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() }