LL脳がscalaの勉強を始めたよ その75


Scalaコップ本の20章の続きをやっていきますよー、今回は抽象型についてやっていきますねー

抽象型

サブクラスで具体的な型定義を行うためのプレースホルダーとして抽象型ってのがあるみたいですね(´・ω・`)たとえばtype Tみたいな抽象型宣言で定義するみたいです。

抽象型のサンプルをば

とりあえず抽象型の使い方サンプルを見ていきますかねー、サンプルとして動物の食性モデリングを取り上げてみますよー

// 食べ物クラスです
class Food
// 動物抽象クラス定義です
abstract class Animal {
  def eat(food: Food)
}

上記クラスを特化させて草を食べる牛クラスを勢いで作成してみますよー、ただしこの定義ではコンパイルが通りませんな(´・ω・`)

// 食べ物を継承した草クラス
class Grass extends Food
// 動物を特化させた牛クラスですな
class Cow extends Animal {
  override def eat(food:Grass){}
}

//  コンパイルしてみますがエラー大発生ですな
<console>:7: error: class Cow needs to be abstract, since method eat in class Animal of type (Food)Unit is not defined
       class Cow extends Animal {
             ^
<console>:8: error: method eat overrides nothing
         override def eat(food:Grass){}
                      ^

どうやらAnimalクラスのeatメソッドのパラメータ型はGrassなのにCowクラスのeatメソッドのパラメータ型がFoodで異なっているからオーバライドできませんYO!って怒られてるみたいです。(´ε`;)ウーン…パラメータ型が異なるとオーバーライドできないのか…

型システムの厳密性

上記のような厳密な型システムは「そこまでするひつようがあるの?」と批判されてたみたいですが、コップ本的には次のような危険なコードをスルーしてしまう可能性があるので"必要です"と主張しております。

// 食べ物と動物の定義です
class Food
abstract class Animal {
  def eat(food:Food)
}

// 実際はダメな草と牛の定義です
class Grass extends Food
class Cow extends Animal {
  override def eat(food:Grass){}}
}

// 食べ物クラスのサブクラスとして魚を用意しますね
class Fish extends Food
// 上記コンパイルが通ってしまうとCowにFishを与えてしまうことに…
// ...とサブ型を問答無用に許可すると意図しない型でもスルーすることになる?
val bessy:Animal = new Cow
bessy.eat(new Fish)

サブクラスでメソッドのパラメータを特化できるという条件だと上記のようなコードが通ってしまうそうですが…何故なんだかはいまいち分からんす(´・ω・`)eatメソッドのパラメータにFish型を渡したらそもそも怒られるような気が…それともそもそも上記のような条件がつくとそこすらスルーされてしまうのかしら?

…と、なやんでいても進まないのでとりあえず適切なサンプルを書いてみますかね

class Food
abstract class Animal {
  // 上限境界を使って抽象型を定義しますよ
  // SuitableFoodはFoodのサブ型であればOKという形式です
  type SuitableFood <: Food
  // eatメソッドのパラメータ型は抽象型を利用します
  def eat(food:SuitableFood)
}

class Grass extends Food
class Cow extends Animal {
  // 抽象型SuitableFoodを具象実装します
  type SuitableFood = Grass
  // 具象実装した型をパラメータとして利用しますねー
  override def eat(food: Grass){}
}

とりあえず作成したCowにえさを食べさせてみますかねー

// CowにGrassを食べさせます
scala> val bessy:Cow = new Cow   
bessy: Cow = Cow@1cb4cae

scala> bessy.eat(new Grass)   

それではちょっぴり抽象度を上げたCowに魚を食べさせてみます

// 魚クラスを定義しますよ
scala> class Fish extends Food
defined class Fish
// 動物として牛を定義してやります
scala> val bessy:Animal = new Cow
bessy: Animal = Cow@5902ca

// 魚を食べさせますよ
scala> bessy.eat(new Fish)
// 型的にアウト!とおこられまった
<console>:11: error: type mismatch;
 found   : Fish
 required: bessy.SuitableFood
       bessy.eat(new Fish)
                 ^
// ただし抽象度を上げたせいで草を食べさせるのもアウトっぽいです
scala> bessy.eat(new Grass)
<console>:10: error: type mismatch;
 found   : Grass
 required: bessy.SuitableFood
       bessy.eat(new Grass)
                 ^

うーん、抽象度を上げてAnimal型にしてしまうとGrassをeatに渡すのもダメになってしまうのですね…牛でやりたきゃCow型で定義しろってことかしら?ちょっと混乱してきたぞ(´・ω・`)とりあえず先に進んでみますかねー

パス依存型

上記エラーメッセージを眺めてみると、eatメソッドが要求しているのはbessy.SuitableFoodという「bessyが参照しているオブジェクトのメンバーであるSuitableFood型」になっているんですが、このような型をパス依存型と呼ぶみたいです(´・ω・`)ここでいうパスはオブジェクト参照のことですね

このパス依存型は一般的にパスが異なれば違う型を参照することになるらしです。例えば次のようなサンプルで試してみますかねー。

// サンプルとして犬とドッグフードを定義してやります
class DogFood extends Food
class Dog extends Animal {
  type SuitableFood = DogFood
  override def eat(food:DogFood){}
}

//// んじゃ、試してみますよ

// 牛を呼び出します
scala> val bessy = new Cow
bessy: Cow = Cow@8e3115
// 犬を呼び出します
scala> val lassie = new Dog
lassie: Dog = Dog@13dc4d5

// 犬にSuitableFood(DogFood)を食べさせてみます
scala> lassie.eat(new lassie.SuitableFood)

// 牛にSuitableFood(DogFood)を食べさせてみると怒られます
scala> lassie.eat(new bessy.SuitableFood) 
// 草じゃなくてDogFoodよこせYOって怒られました
<console>:13: error: type mismatch;
 found   : Grass
 required: DogFood
       lassie.eat(new bessy.SuitableFood)
                  ^

おお、パスに沿ってlassie.SuitableFoodがきちんとDogFoodとして解釈されてますねー、せっかくなので別の犬も呼び出して試してみます

// 別のお犬様bootsieを呼び出します
scala> val bootsie = new Dog             
bootsie: Dog = Dog@2e9c76
// bootsieはlassieの餌(DogFood)を食べられました
scala> bootsie.eat(new lassie.SuitableFood)

scala> 

うん、結局のところ各インスタンスが参照しているオブジェクトに沿ってきちんとふるまいますよ!ってことでいいのかしら(´・ω・`)

javaの内部クラス型との比較

ちなみにコップ本によればパス依存型はjavaの内部クラス型に似ているけども、Javaの内部クラス型は外部クラスに名前を与えるのに対してパス依存型は外部オブジェクトに名前を与えるそうです。

ScalaでもJavaの内部クラス型の表現も出来るんですが、実際に書き方が違うみたいなのでそこを確認してみますかねー、例えば次のような2つのクラスOuterとInnerについて考えてみます(´・ω・`)

// 入れ子の2つのクラスですね
class Outer{
  class Inner
}

上記のようなクラスがあった場合、javaでは内部クラスをOuter.Innerで参照するらしいんですが、ScalaではOuter#Innerを使って参照するみたいです。"."はパス依存型でのオブジェクト参照のためにとっておいてるとのこと(´・ω・`)

パス依存型的にオブジェクト参照で書くとこんな感じですな

// インスタンス生成です
val o1 = new Outer
val o2 = new Outer

// オブジェクト参照しますよ
o1.Inner
o2.Inner

上記のようなo1.Innerとo2.Innerは異なる2つのパス依存型で、o1.Innerは特定の外部オブジェクトをさしていて、かつo2.Innerも別の外部オブジェクトのInnerクラスを指しているみたいです。それに対してOuter#Innerはobject型の任意のオブジェクトが持つInnerクラスを表すので、o1.Innerとo2.InnerはOuter#Innerのサブ型で互換性があるらしいです(´・ω・`)ほぼ写経ですが、そんなあんばいみたいです…

ちなみに、ScalaではJavaと同様に内部クラスインスタンスが自分を包含している外部クラスへの参照を持っているみたいなので、内部クラスは例えば外部クラスのメンバーにアクセス出来るとのことです。なので、何らかの形で外部クラスインスタンスを指定しないと内部クラスのインスタンスが生成できないんだとか…

内部クラスインスタンスの生成方法

内部クラスインスタンスを生成するには、たとえば外部クラスの本体内で内部クラスインスタンスを生成する方法があるみたいです。この方法だと現在の外部クラスインスタンス(this参照できるもの)が使用されるみたいです。

また、違う方法をとしてはパス依存型を利用する方法があるみたいです。例えば前述のo1.Innerは特定の外部オブジェクトの名前になっているので、次のようにしてこいつのインスタンスを生成してやります。

// パス依存型から内部インスタンスを生成します
scala> new o1.Inner                        
res12: o1.Inner = Outer$Inner@1a30706

こんな感じでパス依存型から内部インスタンスが生成できたりするのですが、java的内部クラス型のOuter#Inner型ではOuterの特定のインスタンスを指定していないので内部インスタンスが作れないみたいです

// 無理っておこられましたな(´・ω・`)
scala> new Outer#Inner
<console>:6: error: Outer is not a legal prefix for a constructor
       new Outer#Inner
                 ^

列挙

Scalaのパス依存型の応用として列挙サポートに関するものがあるみたいです。ScalaではJavaC#などで標準でサポートされている新しい型定義の組み込み構文要素としての列挙をもっていないので、そのかわりにscala.Enumerationという標準ライブラリのクラス拡張してオブジェクトを定義するそうです(´・ω・`)

列挙してみますよ

んでは実際に試してみますよー

// Colorの列挙を試してみます
object Color extends Enumeration {
 val Red = Value
 val Green = Value
 val Blue = Value
}

ちなみにscalaでは複数のvalやvarの右辺が等しい場合には短縮した書き方ができるみたいなので、上記を次のよう省略することが出来るみたいです。

// 省略してみますかねー
object Color extends Enumeration {
  val Red, Green, Blue = Value
}

上記で定義したColorオブジェクトでは次のように3つの値を提供するのですが、Colorという枕詞を省略したければimportしてやりますね

// Colorメンバーです
scala> Color.Red
res14: Color.Value = Color(0)

scala> Color.Green
res15: Color.Value = Color(1)

scala> Color.Blue 
res16: Color.Value = Color(2)

// インポートして省略形メンバーです
scala> import Color._
import Color._

scala> Red
res17: Color.Value = Color(0)

scala> Green
res18: Color.Value = Color(1)

scala> Blue
res19: Color.Value = Color(2)

ちなみに上記のRed, Green, Blueの各値はEnumerationで定義された内部クラスValue
の同名パラメータなしメソッドValueによって返されるので、各値の型はColor.Value型になるみたいです。Color.Value型はパス依存型でColorがパス、Valueが依存型になるらいいです(´・ω・`)これらは他の全ての型と全く異なる新しい型という特徴をもつらしいですが、この部分はチョー重要とのことですc⌒っ゚д゚)っφ メモメモ...

なので、同じEnumerationを拡張した次の各値の型Direction.Valueと前述のColor.Valueは異なる型となるみたいです(`・ω・´)

object Direction extends Enumeration {
  val North, East, South, West = Value
}
Enumerationいろいろ

Enumerationクラスでは列挙に関する他の様々な機能を持っているみたいです。例えば多重定義されたValueメソッドを使うと、次のように名前と列挙値を結びつけられるみたいです。

object Direction extends Enumeration {
  // 名前と列挙値を紐付けます
  val North = Value("North")
  val East = Value("East")
  val South = Value("South")
  val West = Value("West")
}

また列挙値の全ての値あはmap, flatMap, filter付きfor, foreachで反復処理が出来るみたいです。

// foreachで処理しますよ(´・ω・`)
scala> Direction.foreach(d => print(d + " "))
North East South West 

// forで処理っすね(`・ω・´)
scala> for(d <- Direction) print(d + " ")
North East South West 
scala> 

さらに列挙の値には0から始まるidがふられるのですが、idメソッドを使うことで列挙値にふられたidを取得できます(`・ω・´)逆にidから値の取得も出来るみたいです

// idを取得してみますよー
scala> Direction.North.id
res28: Int = 0

scala> Direction.South.id
res29: Int = 2

// idから値を指定してみますよ
scala> Direction(1)
res30: Direction.Value = East

scala> Direction(3)
res31: Direction.Value = West

簡単に列挙について触れましたー、詳しくはscala.EnumerationのScaladocを見よ!だそうですー

いじょー

とりあえず、なんとかかんとか抽象型をやってみましたよー、型関係はやっぱり苦手ですな(´・ω・`)次回は抽象型をつかってケーススタディー的なコードを書いてみますよー