Scalaに入門してみた その二

早くも転職活動に対して息が切れてきました。というか事前の話で聞いた想定と完全に食い違ってしまっています。書類選考の通過率が大体5割、応募出したのが20社くらいなので、当初の想定以上に面接に振り回されています。最初は20%くらいって言ってたのに・・・。
そんな感じでしたが、一番手応えがあった企業から内定通知が決ましたヒャッホゥイ!
・・・しかし、初出社日が1/4て。その日は実家にいるんですが。しかも毎年のことなので、1/3には新幹線 & 高速バスがありません。さすがにこれくらいは交渉聞いてくれると思うので交渉はしてみますが。というかしないと強行軍で帰ってくることになってしまうので、それは避けたい。

さて、そんな近況は置いておいて、前回に引き続きScalaに入門します。今回はJavaっぽいけど大分違うクラス周りです。まぁ、ライブラリとかもっと大事な部分は置き去りなので、あまり意味が無いといえば無いですが、そもそも始めた時期が時期で周回遅れ感が半端無いので気にしない。

クラス定義
// 基本的なクラス定義は、
// class クラス名(基本コンストラクタの引数) {
//   基本コンストラクタを含めた定義
// }

// Stringを受け取る基本コンストラクタを持つclass
class Engineer(_language:String) {
  println("language is " + _language)
  val language = _language

  def coding() = println("I use " + language)
}

// 補助コンストラクタを使う。補助コンストラクタはオーバーロード可能。
class Engineer(_language:String) {
  println("language is " + _language)
  val language = _language

  def this() = this("Scala")
  def this(_lang:String, version:Float) = this(_lang + version.toString)

  def coding() = println("I use " + this.language)
}

// 基本コンストラクタに事前条件を指定してみたもの
class YoungPerson(_age:Int) {
  require(_age < 50)
  val age = _age

  def this() = this(30)

  def say() = println("my ages is " + this.age.toString)
}

// 基本コンストラクタをprivateにしたもの。基本コンストラクタに引数がいらない場合は()も省略できる。
class CantCreateClass private {
  def say() = println("hai!")
}

基本的には、javaの定義から、どうせデフォルトコンストラクタが必ず定義されるなら、内部にそのまま書いてしまえ!という感じですね。それ以外のコンストラクタは補助コンストラクタに書く、といった感じですね。
基本コンストラクタは、つまるところclass { .. }の { .. }全体なので、valとかvarとかも上から呼ばれた段階で定義されます。
補助コンストラクタは、最終的にthis(..)を呼び出して、基本コンストラクタを呼び出さないとなりません。また、補助コンストラクタより前に定義されている変数とか定数は使えません。
つまり、補助コンストラクタ→基本コンストラクタと呼び出しているので、補助コンストラクタはそもそも基本コンストラクタより前の段階になるっていうことですね。

また、各メンバはデフォルトでpublicなので、必要なところだけprivateにしていく感じです。ただし、valやvarについては、内部で自動的にアクセスメソッドが定義され、それが呼び出しや代入で自動的に呼び出されているということです。ということはその動作をオーバーロードできるんでしょか?

シングルトンオブジェクト
// 最も基本的なシングルトンオブジェクト
object Singleton {
  val hoge = 0
  def hello() = println("world!")
}

Javaで、ユーティリティなどの関数だけをまとめたようなクラスを作る際、よくコンストラクタをprivateにして、static変数からのみそのクラスのインスタンスにアクセスさせる、というやりかたでシングルトンを作っていましたが、Scalaではobjectキーワードを使うことで、class定義と同じ形で明示的にシングルトンオブジェクトを作れるようになっています。
内部まで見ないとわからないJava形式とは、違い、宣言時点ですぐにわかるのでよろしいと思います。

コンパニオンオブジェクト
object Main {
  class Companion private (num:Int) { }

  object Companion {
    def apply(num:Int) = {
      new Companion(num)
    }
  }

  def main(args: Array[String]) = {
    val ins = Companion(100)
    println(ins)
  }
}

あるclassについて、「同一」スコープ内で、かつ同名のobjectが指定されているとき、そのobjectはコンパニオンオブジェクトになります。C++のfriendクラスみたいなもんでしょうか。実用上、Factoryクラスのようにして利用することが多いそうです。
また、objectで定義しているapply関数は、補助コンストラクタで指定したthis同様実際にはキーワードであり、オブジェクトをfunctorのようにして呼び出すことができます。

classに対するパターンマッチ
class Apple
object Apple {
  def unapply(a:Any) : Boolean = {
    if (a.isInstanceOf[Apple]) true else false
  }
}

class Orange(val name:String)
object Orange {
  def apply(name:String): Orange = new Orange(name)
  def unapply(a: Orange):Option[String] = Some(a.name)
}

def matchTest(a:Any) = {
  a match {
    case Apple => println("Apple")
    case Orange("hoge") => println("Orange.name = hoge")
    case _ => println("other")
  }
}

matchTest(Apple) -> "Apple"
matchTest(Orange) -> "other"
matchTest(Orange("huga")) -> "other"
matchTest(Orange("hoge")) -> "Orange.name = hoge"

// もっと簡単な方法。基本はこれでよいみたい。引数が無い場合でも()を付けないといらないwarningが出ます(2.9.2)
case class Apple(name:String)
case class Orange(name:String)
def matchTest(a:Any) = {
  a match {
    case Apple(n) => println(n)
    case Orange("hoge") => println("Orange.name = hoge")
    case _ => println("other")
  }
}

classに対するパターンマッチは、OCamlHaskellほど簡単にはいきません。unapplyという抽出子というものを指定する必要があります。このメソッドは、パターンマッチでその型がcaseに出てきたときに呼び出されるもので、Appleの例よろしく、パターンマッチの対象となった変数が引数として渡され、その型がそもそも一致しているか、コンストラクタの形が一致しているか、などが見られるようです。
パターンマッチ時に、コンストラクタの引数部分を変数にしてやれば、その引数の内容を使うことができます。
が、当然ながら必ずコンパニオンオブジェクトを作成しなければならないなどめんどいので、case classというものを使えるようになっています。case classは、パターンマッチで利用されるような基本的な機能を自動的に定義してくれるような機能で、データを持たせたいだけのクラスとかならば、これでなんとかなりそうです。

パッケージ
// hoge.hugaパッケージにclass Hugaを定義
package hoge.huga {
  class Huga(val name:String)
}
// hogeパッケージにclass Hogeを、hoge.hugaにclass Hugaを定義
package hoge {
  class Hoge(val name:String)
  package huga {
    class Huga(name:String)
  }

  // 変数とかメソッドとかも定義できる。これらは同じパッケージからアクセスできる
  val foo = 100
  def bar() = foo * 2
}

// 色々なインポート方法
import hoge.huga.Huga  // 基本的な絶対指定
import hoge.huga._     // hoge.hugaパッケージを一括インポート
import hoge.huga       // パッケージオブジェクトをインポート
import java.util.{List.Map} // 一部のみインポート
import java.sql.{Date => SData,_}  // DateをSDateとしてインポートし、それ以外をそのままインポート
import _root_.scala.collection.Map   // 明示的な絶対指定
import java.util                     // 相対的なインポート
import util.Date

Scalaのパッケージは、Javaであった

  • ファイル名とクラス名が一致していないとならない
  • パッケージ階層とディレクトリ階層が一致していないとならない

という制限が取っ払われています。また、それ以外にもかなり柔軟なインポートができるようになっています。OCamlのmodule A = LongLongModuleName みたいなのは無いみたいですが、それについては別名インポートでなんとかしやがれ、といった感じでしょうか。
あと、個人的は冗長であったとしても、階層とファイル名がパッケージの階層と一致しているのは、ツール的な都合としてはよかったんでねーべか、とも思ったりなんだり。でも大抵カラッポのディレクトリが量産される原因でもあったから、別にいらないと言えば確かに不要ですね。

継承・抽象クラス・アクセス制限
abstract class X(name:String) {
  println("X name is " + name)
  def hoge():Unit
  def huga(n:Int) = printf("%d", n)
}

// スーパークラスのコンストラクタを呼ぶ
class Y(name:String) extends X(name) {
  def hoge() = println("My name is Y")
  override def huga(n:Int) = printf("value is %d", n)

  // このクラスとオブジェクト以外からはアクセス不可
  private def foo() = println("foo")
  // このクラスのインスタンス以外からはアクセス不可
  private[this] def bar() = println("bar")
  // このクラスとサブクラス以外からはアクセス不可
  protected def foobar() = println("foobar")
}

sealed abstract class T
case class T1 extends T
case class T2 extends T
case class T3 extends T

継承や抽象クラスについては、ほとんどJavaと同じようなもんですが、オーバーライドするときにはoverrideキーワードを付けないと駄目です。
私はJavaのアクセス制限がいまだによーわかってませんが、Scalaではより複雑になって帰ってきました。private,protectedだけならば簡単なんですが、それ以外にも制限をかける方法が色々あるようです。・・・privateだけでいいんじゃね?とか一瞬思った私はダメですかそうですよね。

また、個人的にユニークだと思ったのはsealedキーワードです。sealedが付けられたクラスはシールドクラスとなり、「同一ファイル内」のクラス以外はそのクラスについてサブクラスを作成出来ない、という制限を加えることができます。パターンマッチの網羅性を可能な範囲に抑えることができるとのことです。

まとめ(その二)

今回はクラス周りの機能を個人的にわかればいいやの範囲でまとめました。Javaより柔軟性があるのは確かですが、その代わりにJavaのシンプル極まりないclassよりは複雑になってしまっているような気がするのは仕方が無いことでしょうか。
こうやってみると、やっぱりScalaオブジェクト指向言語なのだなぁ、と頷かされます。まぁ、最初は必要な範囲で使えばいいってことですね、何でも。

次で一通りまとめ終わるかと思います。まとめてからScalaで何かするっていうのは何も考えてません :-p