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


Scalaコップ本の22章をやっていきますよー、22章ではリストに関するエトセトラですな。特にコップ本ではリスト使いまくりなので頑張ってやっていきますかねー。今回はリストの概要を駆け足で見ていきますよ(`・ω・´)

Listクラスの原則

Scalaでのリストは組み込みの言語要素ではなく、scalaパッケージで抽象クラスとして定義されているそうです。また、サブクラスとして::とNilの2つのクラスを持っているとのことデス(´・ω・`)

それではリストクラスを若干簡易的に実装したコードを見ていきますかねー、まずは抽象クラスの定義部分です

package scala
// 共変の型パラメータが定義されておりますね
abstract class List[+T]{

型パラメータが共変で定義されているので、List[Int]がList[Any]に代入できるんだとか…ここらへんはうろ覚えなので19章やり直さないとイカンな(´・ω・`)

とりあえず代入操作をやってみますよ

 // IntのListを用意しますよ
scala> val listInt = List(1,2,3)    
listInt: List[Int] = List(1, 2, 3)

// Int ListをAny Listに代入しますデス
scala> val listAny:List[Any] = listInt
listAny: List[Any] = List(1, 2, 3)

ついでにリストの演算は以下の3つの基本メソッドで定義されておりマス。ちなみに全部抽象メソッドらしく、各々ListのサブオブジェクトのNilとサブクラスの::で定義されるみたいですね(´・ω・`)

// 空判定
def isEmpty:Boolean
// 先頭要素の取得
def head:T
// 先頭要素以外で構成されたリストを取得
def tail:List[T]
Nilオブジェクト

ListのサブオブジェクトであるNilを見ていきますよ。Nilは空リストを定義するオブジェクトで、定義は次のようになるみたいです(´・ω・`)

// Nilはケースオブジェクトですね
case object Nil extends List[Nothing]{
  // isEmptyの具象メソッドデス
  override def isEmpty = true
  // 値が無いので例外を投げますよ
  def head:Nothing = throw new NoSuchElementException("head of empty list")
  def tail:List[Nothing ]= throw new NoSuchElementException("tail of empty list")
}

NilはList[Nothing]型を継承するのですが、ソレに加えてリストが共変なのでList型の全てのインスタンスに対して互換性があるそうです。Nothing型の互換性と共変という型パラメータの合わせ技ですな(`・ω・´)。なお、headとtailで例外を投げるのは値が無いよのサインとして現実的にできる唯一の手段とのことです。Java系言語だと例外は応答結果の一種として結構気軽に使われるイメージですが、どうなんだろ?(´・ω・`)

::クラス
クラスはconsって呼ばれるらしい、空では無いリストの表現みたいですな。なぜに::なんていう名前になってるかというと、中置き演算::のパターンマッチをサポートするためみたいデス。例えば x :: y を::(x, y)として扱うんですな(´・ω・`)ちなみにこいつもケースクラスみたいです

そんではとりあえず定義を書いてみますよ

// ファイナル宣言しますね
// 各パラメータはheadとtailの値になるのですな
final case class ::[T](hd:T, tl:List[T]) extends list[T] {
  def head = hd
  def tail = tl
  // 空ではないことを表現しますね
  override def isEmpty:Boolean = false
}

ちなみにhead, tailの各メソッドは単純にパラメータを返すだけなのでこんな感じで書く事も可能デス

// ケースクラスのパラメータはフィールドとして扱えるのですな
final case class ::[T](head:T, tail:List[T]) extends list[T] {
  override def isEmpty:Boolean = false
}

...と、ココマデ見てきて、::オブジェクトは生成時にheadの結果とtailの結果を予め保持してるようなイメージでおkかしら?

その他のメソッド

基本メソッド以外のListメソッドは上記3つの基本メソッドで書けるみたいですな(´・ω・`)たとえばこんな感じですかねー

 // 要素数はtailの要素数をベースに求めるみたいですね(´・ω・`)
 // ...でもこれだけみてると再帰的に処理される風だなぁ...どうなんだろ?
def length:int = if(isEmpty) 0 else 1 + tail.length

// drop処理もtailで定義されるリストをベースに処理しますね
// これも再帰処理すなー
def drop(n:int):List[T] = 
  if(isEmpty) Nil
  else if(n <= 0) this
  else tail.drop(n - 1)

// mapもheadの値に関数適用するって処理を
// 再帰的に行なっていく感じですな
def map[U](f:T => U):List[U] =
  if(isEmpty) Nil
  else f(head) :: tail.map(f)

全般的に再帰処理が多いみたいですね(´・ω・`)リストの処理は結構コスト高いデスネ…まあデータの構造上順繰りなめていく形式で使うからなぁ…と知ったような事を言ってみます(`・ω・´)

リストの構築

リスト構築用の::と:::メソッドについてですー。ここで::も:::も末尾がコロンなのでx :: yはy.::(x)のように解釈されますね。実際x :: List(y1, y2, y3)みたいな形式で使うのでリストのメソッドでありますね(`・ω・´)

ちなみに::メソッドはパラメータとして呼び出し元(上の例だとList(y1, y2, y3))のリスト要素と同じに擦る必要はないみたいです。これは::メソッドが次のような形式で下限境界設定をしてるからみたいですな

// リスト要素型のスーパー型で制限デス
def ::[U >: T](x:U):List[U] = new scala.::(x, this)

ちなみにサンプルオブジェクトを使うとこんな感じになりますです

abstract class Music
class Pops extends Music
class Jazz extends Music

//// 実際に代入操作をしてみますよ
// Pops型のリストです
scala> val pops = new Pops :: Nil
pops: List[Pops] = List(Pops@b1fea4)

// Jazz型の要素をつっこんでみたら親クラスのMusic型になりましたな
scala> val music = new Jazz :: pops
music: List[Music] = List(Jazz@16fcc4, Pops@b1fea4)

// 同じ型だけで構築する場合はそのままデスネ(´・ω・`)
scala> val jazz = new Jazz :: Nil  
jazz: List[Jazz] = List(Jazz@3f58bb)

scala> val jazz2 = new Jazz :: jazz
jazz2: List[Jazz] = List(Jazz@10fb9bd, Jazz@3f58bb)

型推論大活躍ですな(`・ω・´)とちょっと感心してしまいました。ちなみに下限境界をしているおかげで、通常は反変ポジションとして扱われるメソッドパラメータを共変として宣言できるという一石二鳥っぷりみたいです。

ついでに:::メソッドの定義は次のようになるみたいです

// こいつも下限境界を使ってますな
def :::[U >: T](prefix:List[U]):List[U] = 
  // 結合はheadとtailから要素を持ってきてるのデスね(´・ω・`)
  // でもコレって結局再帰的に計算されるような…
  if(prefix.isEmpty) this
  else prefix.head :: prefix.tail ::: this

最終行は(this.:::(prefix.tail)).::(prexix.head)みたいな感じになって再帰的に処理される塩梅っぽいですね。例えばList(x1, x2, x3) ::: List(y1, y2, y3)とかは最終的にx1 :: x2 :: x3 :: List(y1, y2, y3)に展開されるイメージですかね(´・ω・`)うーん、やっぱりコスト大きいような気がするなぁ、実際のところどうなんだろ?

いじょー

時間切れで以上です。次回はListBufferクラスをやりますネ(´・ω・`)