読者です 読者をやめる 読者になる 読者になる

seratch's weblog in Japanese

About Scala, Java and Ruby programming in Japaense. If you need English information, go to http://blog.seratch.net/

オブジェクト指向言語としての Scala 入門

Scalaのバージョン

この記事が対象とする Scala のバージョンは「2.9.2.final」です。

Anyを頂点とするクラス階層

Scala では scala.Any が全ての型の親です。

scala.Any の下には scala.AnyVal と scala.AnyRef がいます。

java.lang.Object を親に持つ全ての Java オブジェクトは scala.AnyRef 型として扱うことができます。

AnyVal は Byte,Int,Boolean などの親で AnyRef はそれ以外の型の親になります。

全てがオブジェクト

Scala では Java でいう primitive 型(基本型)は存在せず、すべてがオブジェクトです。

たとえば、四則演算や比較などの演算子的な処理も以下のように Int などの数値型のメソッドとして実装されています。

1.+(2) // 3
1+2 // 3
1 + 2 // 3

クラスをつくる

中身のないクラスはこれだけで OK です。

class MyClass

中身がない場合は{}を省略できます。アクセス修飾子を指定しない場合、スコープは public です。

クラス名に続いて基本コンストラクタ(primary constructor)を定義します。

この状態では基本コンストラクタのみがインスタンス化の手段になります。

class MyClass(name: String)
val myClass = new MyClass("aaa") 
// 文末のセミコロンは optional で基本的にはつけません

コンストラクタを複数書きたい場合は、補助コンストラクタ(auxiliary constructor)と呼ばれる別のコンストラクタを定義します。

補助コンストラクタは基本コンストラクタを呼び出す必要があります。

class MyClass(firstName: String, lastName: String) {
  def this(firstName: String) = {
    this(firstName, "")
  }
}
val m1 = new MyClass("aaa")
val m2 = new MyClass("aaa","bbb") 

インスタンス化のタイミングで本体が実行されます*1

class MyClass(name: String) {
  println(name)
}
val m = new MyClass("aaa") 
// "aaa"が出力される

アクセサ自動生成

Scala では var で定義した変数は再代入可能、val で定義した値は再代入不可となります。

class MyClass(var name: String)
val m = new MyClass("aaa")

m = new MyClass("bbb")
//
// error: reassignment to val
//       m = new MyClass("bbb")
//         ^

コンストラクタ引数に var で定義した変数には getter/setter が自動生成されます。

getter は変数名です。() はつけられません(これについては後述)。

m.name // "aaa"

setterはメソッドの定義としては「(変数名)_=(T)」ですが「(変数名) = T」でもアクセスでき、普通はそのようにアクセスします。

m.name_=("bbb")
m.name = "bbb"

val で定義した値は再代入不可なので getter だけが生成されます。

class MyClass(val name:String)
val m = new MyClass("aaa")
m.name // "aaa"

メソッドをつくる

メソッドは def キーワードを使って定義します。アクセス修飾子を指定しない場合、クラスと同様に public になります。

class MyClass(val name:String) {
  def upperName() : String = name.toUpperCase
}
new MyClass("abc").upperName // "ABC"
new MyClass("abc").upperName() // "ABC"

このメソッドのように引数がないものは定義の時に () を省略する事もできますが、 () を省略した場合、呼び出し時に () をつけるとコンパイルエラーになります(自動生成される getter もこうなっているので () をつけられません)。

Scala では副作用のないものは () をつけられる場合でも () をつけずに呼び出す方が良い慣習とされています。

class MyClass(val name:String) {
  def upperName : String = name.toUpperCase
}
new MyClass("name").upperName
new MyClass("name").upperName() // NG

引数を受け取る場合は以下のようになります。

def toMillis(hours: Int) : Int = 1000 * 60 * 60 * hours
new MyClass().toMillis(3) // 10800000

連続パラメータ(可変長引数)

Java でいう可変長引数を連続パラメータ(repeated parameter)といいます。

型の末尾に「*」をつけたもので Seq[T] 型として受け取れます。

def p(args: String*) = args.foreach(print)
p("aaa","bbb","ccc") // "aaabbbccc"

あらかじめ Array[T] 型のパラメータをそのまま渡したい場合には以下のように「t: _*」と指定します。

これをシーケンス引数(sequence argument)と呼びます。

val args = Array("aaa","bbb","ccc")
p(args: _*)  // "aaabbbccc"

連続パラメータが末尾になっていれば他の引数と組み合わせる事もできます。

def repeatp(times: Int, args: String*) = {
  args foreach { arg => for(i <- 1 to times ) print(arg) }
}
repeatp(3,"a","b","c") // "aaabbbccc"

入れ子のメソッド

メソッドの内部にメソッドを定義することもできます。

以下のサンプルでは、再帰処理で一時的に使用するメソッド「_getLength(Int, List[T])」を入れ子のメソッドとして定義しています。

入れ子のメソッドはローカル定義の変数などと同様のスコープとなり、外部からは呼び出せません。

def getLength[T](list: List[T]): Int = {
  def _getLength(accumulator: Int, list: List[T]): Int = {
    list match {
      case head :: Nil => accumulator + 1
      case head :: tail => _getLength(accumulator + 1, tail)
      case _ => throw new NoSuchElementException
    }
  }
  _getLength(0, list)
}
val len = getLength(List(1,2,3,4,5)) // 5

抽象クラス(abstract class)

基本的に Java と同様です。

abstract class Base {

  // サブクラスで実装が必要なメソッド
  def doSomething(): Unit

  // 抽象でないメソッドのオーバーライドでは
  // Javaの@Overrideと違って記述は必須です。
  override def toString: String = "orz..." 

}

class HelloWorld extends Base {

  // 抽象メソッドを実装するときのoverride指定は任意です
  override def doSomething(): Unit = println("doSomething!")

}
new HelloWorld().doSomething // "doSomething!"

Singletonオブジェクト(singleton object)

Scala には static キーワードがなく、クラスをつくっても static フィールドや static メソッドは定義できません。

Scala で static な事をやりたい場合は以下のように Singleton オブジェクトを使います。

object HelloWorld { 
  def display() = println("Hello World")
}
HelloWorld.display() // "Hello World"

コンパニオンオブジェクト(companion object)

クラスと同名の Singleton オブジェクトを作る事がよくあります。

これをコンパニオンオブジェクトといいます。

class HelloWorld(val message: String) {
  def display() = println(message)
}
object HelloWorld {
  def create(): HelloWorld = new HelloWorld("Hello World!")
}
val hw = HelloWorld.create
hw.display // "Hello World!"

apply メソッドを実装すると new が不要なコンストラクタにみえるファクトリメソッドになります。もちろん apply として呼んでも構いません。

object HelloWorld {
  def apply(message:String): HelloWorld = new HelloWorld(message)
}
val hw = HelloWorld("Hello World")
val hw2 = HelloWorld.apply("Hello WORLD")
hw.display // "Hello World"

List や Map の定義で new を書かなくてもいいのはこれによるものです。

val nums = List(1,2,3,4,5)
val members = Map("Taro" -> 1, "Jiro" -> 2)

パッケージ(package)

※package を使ったコード例は REPL 上では動作しません。scalac でコンパイルして実行するか IntelliJ IDEA などの IDE 上で試して下さい。

以下のクラスは com.example.SnippetA となります。

package com.example
class SnippetA

以下のように入れ子で宣言する事もできます。

package com {
  package example {
    class SnippetB
  }
}

ブロックを省略することもできます。

package com
package example
class SnippetC

package オブジェクト(package object)

package オブジェクトという仕組みが Scala 2.8 から導入されました。

そのパッケージで見えるように変数・メソッド・型を定義することができます。

なお、package オブジェクトの場合、慣習的にソースコードのファイル名は「com/example/package.scala」のように「package.scala」とするようです。

package com
package object example {
  val name = "com.example package object"
  def p(str: String) = println(str)
}
package example {
  object Main extends Application {
    p(name)
  }
}
package example2 {
  import com.example._
  object Main extends Application {
    p(name)
  }
}  

scala.collection.immutable.List が何も import しなくても使えるのは scala パッケージの package オブジェクトで List[+A] として type 宣言されているためです。

インポート(import)

import は Java と比較すると、ワイルドカード指定が「*」ではなく「_」であることと、以下のように相対的にインポートできる点が異なります。

今いるスコープ(package)から相対的な指定が可能です。

package com.example {
  import util.UsefulUtil // = import com.example.util.UsefulUtil
}

既に import されている package は省略できます。

import scala.collection.mutable.ListBuffer

// 絶対パスを意味する
import _root_.scala.collection.mutable.ListBuffer 

// scala._はデフォルトでimport済なのでそこからの相対パス
import collection.mutable.ListBuffer 

また同一パッケージの複数の型を import する場合に以下のような書き方もできます。

import collection.mutable.{ListBuffer, HashMap}

アクセス修飾子

Java ではアクセス修飾子を何もつけない場合、デフォルトで package private になりますが Scala では public になります。

package private は「private[(パッケージ名)]」と指定します。

private で同一クラス内のみのアクセスのみ、 private[this] で同一インスタンス内でのアクセスのみにすることができます。

sealed宣言

また Scala では一つのファイルの中で複数のクラスを内部クラスではなく宣言できます。

例えば example.scala というファイルに以下のように複数のクラスや Singleton オブジェクトを定義する事ができます。

ただし、同じソースファイルで定義したとしても、コンパイルされて生成されたバイトコードでは別々になります。

同一ソースファイル内でしか継承させたくない場合は、下の例で Base クラスがやっているように sealed と宣言します。

package com.example {
  object Main extends Application {
    val util = new com.example.sample.Foo
    util.publicMethod          // "PUBLIC"
    // util.packageLocalMethod // NG
    // util.privateMethod      // NG
  }
  sealed abstract class Base {
    protected def name(): String = "Foo"
  }
  package sample {
    class Foo extends Base {
      def publicMethod = println("PUBLIC")
      private[sample] def packageLocalMethod() = println("PACKAGE LOCAL")
      private def privateMethod() = println("PRIVATE")
    }
  }
}

ミックスイン(mixin)

Ruby でいう module が Scala では trait です。

Java でいうと interface が実装も持てるようになったものです。

trait Printer {
  def display(str: String = "Hello World") = {
    println(str) // "Hello World"は引数のデフォルト値
  }
}
class HelloWorld extends Base with Printer
val hw = new HelloWorld
hw.display // "Hello World"

extends している抽象クラスがない場合は一つ目は with ではなく extends を使います。

class HelloWorld extends Printer
val hw = new HelloWorld
hw.display // "Hello World"

自分型(self type)

self type を指定すると内部はその型が mixin された状態となります。

self type の変数名には慣習的に this や self が用いられますが、任意の変数名でも動作します。

trait Printer {
  def display(str: String = "Hello World") = {
    println(str) // "Hello World"は引数のデフォルト値
  }
}
class HelloWorld { this: Printer =>
  def sayHello() = display("Hello World!")
}

インスタンス化する際にコンパイラによって Printer の mixin が強制されます。

val hw = new HelloWorld with Printer
hw.sayHello // "Hello World!"

Printer が一つ目の mixin になるのでもう一つ追加するときは extends ではなく with キーワードを使います。

class HelloWorld { this: Printer with Writer =>
  def sayHello() = display("Hello World!")
}

型パラメータ(type parameter)

型パラメータの指定には変位と型境界の二つがあります。

class GrandPa
class Pa extends GrandPa
class Child extends Pa

型パラメータの変位

変位には以下の3種類があります。

非変(nonvariant)

指定された型しか代入できません。以下の例だとGrandPaもChildも代入不可です。

class Foo[Pa]
val f: Foo[Pa] = new Foo[GrandPa] // NG
val f: Foo[Pa] = new Foo[Pa]  
val f: Foo[Pa] = new Foo[Child]   // NG
+T:共変(covariant)

その型もしくはその子クラス型を指定できます。GrandPaだけが代入不可です。

class Foo[+T]
val f: Foo[Pa] = new Foo[GrandPa] // NG
val f: Foo[Pa] = new Foo[Pa]  
val f: Foo[Pa] = new Foo[Child] 
-T:反変(contravariant)

その型もしくはその親クラス型を指定できます。Childだけが代入不可です。

class Foo[-T]
val f: Foo[Pa] = new Foo[GrandPa] 
val f: Foo[Pa] = new Foo[Pa] 
val f: Foo[Pa] = new Foo[Child]   // NG

型パラメータの型境界

上限境界(upper bound)

「(型名)

class Foo[T <: Pa]
val f = new Foo[GrandPa] // NG
val f: Foo[Pa] = new Foo[Pa]
val f: Foo[Child] = new Foo[Child]
下限境界(lower bound)

「(型名) >: (境界となる型)」の形で指定します。反変と似ていますが、型境界では指定した時点で型が確定されます。

class Foo[T >: Pa]
val f: Foo[GrandPa] = new Foo[GrandPa] 
val f: Foo[Pa] = new Foo[Pa]
val f = new Foo[Child] // NG

また、クラスに対して指定する型パラメータだけではなく、メソッドの引数型にも型境界を指定することができます。

class Female(val name: String)
class Girl(override val name: String) extends Female(name)
class Male(val name: String)
class Boy(override val name: String) extends Male(name)

class Lady(val name: String)
object Lady {
  def apply[F <: Female](f: F): Lady = new Lady(f.name)
}

val lady = Lady(new Female("Micheal"))
val lady = Lady(new Girl("Micheal"))
val notLady = Lady(new Male("Micheal")) // NG
val notLady = Lady(new Boy("Micheal")) // NG

抽象型(abstract type)

typeキーワードを使って抽象な型のメンバーを持つことができます。

abstract class LengthPrinter {
  type Target
  def printLength(target: Target): Unit
}

class ArrayLengthPrinter[T] extends LengthPrinter {
  type Target = Array[T]
  override def printLength(target: Target): Unit = println(target.length)
}
new ArrayLengthPrinter[Int].printLength(Array(1,2,3,4,5)) // 5

class StringLengthPrinter extends LengthPrinter {
  type Target = String
  override def printLength(target: Target): Unit = println(target.size)
}
new StringLengthPrinter().printLength("abcde") // 5

抽象型は継承による型のパラメータ化だけでなく、型のalias(別名)定義としても使います。

実際にscala/package.scalaの中でcollection.immutable.ListやRangeのaliasを定義しています。

以下はRangeの定義箇所の抜粋です。type宣言によってこのパッケージではRangeのclassがimportなしで使えるようになっています。

type Range = scala.collection.immutable.Range // これによりclassがimportなしで使える
val Range = scala.collection.immutable.Range // これによりobjectがimportなしで使える

これを簡単な例で実装してみました。

package com {
  package object example {
    type Util = com.others.Util
    val Util = com.others.Util
  }
  package example {
    object Main extends Application {
      new Util().p("aaa") // "type Util = com.others.Util"により使えるようになった
      Util.p("aaa") // "val Util = com.others.Util"により使えるようになった
    }
  }
  package others {
    class Util {
      def p(str: String) = println(str)
    }
    object Util {
      def p(str: String) = println(str)
    }
  }
}

構造型(structural type)

構造型はダックタイピングに対する型安全なアプローチです。

特定のフィールドやメソッドを含む事を指定します。

class PrintExecuter {
  // print(String)というメソッドを持っている型なら何でもよい
  type Printable = { def print(str: String) }
  var printer: Printable = null
  def setPrinter(newPrinter: Printable) = printer = newPrinter
  def print(str: String) = printer.print(str) 
}
val executer = new PrintExecuter
class Printer {
  def print(str: String) = println(str)
}
executer.setPrinter(new Printer)
executer.print("test") // "test"

シンボル

同じ名前(文字シーケンス)のシンボルは同一オブジェクトとなります。

先頭にシングルクォートをつけて記述します。

'Id
scala.Symbol("Id")
'Id eq scala.Symbol("Id") // true

列挙(Enum

Scalaの列挙は、JavaC#のようにenum構文は存在しないので、scala.Enumerationを継承したSingletonオブジェクトを定義します。

object PartOfSpeech extends Enumeration {
  val Noun = Value("Noun")
  val Verb = Value("Verb")
  val Adjective = Value("Adjective")
}
PartOfSpeech.Noun // PartOfSpeech.Value
PartOfSpeech.Noun.toString // "Noun"
PartOfSpeech.values.foreach { println } // Noun Verb Adjective

*1:クラスだけでなく後述の抽象クラスやトレイトについても同様です