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


Scalaコップ本の17章に入っていきますよー、ようやく折り返しですよー

17章はコレクションに関するアレやコレ、配列やリストやマップや集合についてやっていきますかねー

ライブラリーの概要

Scalaのコレクションライブラリーは数多くのクラスやトレイトが含まれるらしいんですが、概要として以下のトレイトだけを抑えておこーぜ!とのことです。

メインのトレイとがIterableで、そのサブトレイトとしてシーケンス(Seq)、集合、マップがある形ですね。シーケンス、集合、マップはそれぞれimmutableとmutableがあるっすね。シーケンスは配列やリストのような順序性があるもので、集合は乱暴に言って重複なしのコレクションで、マップはキーと値の対応関係のあるコレクションですな。

イテレータの生成

メインとなるIterableはelementsというメソッドでIteratorを作り出せるコレクションオブジェクトを表す名前だそうです…ということなのでIteratorを作ってみますよ

// とりあえずリストを生成で
scala> val l = List(1,2,3)        
l: List[Int] = List(1, 2, 3)
// elementsメソッドでイテレータを生成しますよ
scala> val li = l.elements
li: Iterator[Int] = non-empty iterator
// hasNextメソッドで次の値の存在チェック
scala> li.hasNext
res17: Boolean = true
// nextメソッドで次の値を取得します
scala> li.next   
res18: Int = 1
// 次の値
scala> li.next
res19: Int = 2
// 次の値
scala> li.next
res20: Int = 3
// 存在チェックです、値が無いのでfalseが返ります
scala> li.hasNext
res21: Boolean = false
// 次の値がない状態でアクセスするとエラー出まくりです
scala> li.next   
java.util.NoSuchElementException: next on empty Iterator
	at scala.List$$anon$2.next(List.scala:603)
	at .<init>(<console>:7)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMeth...

上記のIteratorを使ってたくさんの具象メソッドが提供されているみたいですね。前回までにやったflatMapとかfilterおかexistsとかfindとかとか…とりあえず反復処理の根源みたいな存在ですかねー

ちなみにiteratorトレイトのスーパートレイとはAnyRefとのことです

IterableとIterator

IterableとIteratorの役割分担は次のようになるようです。

  • Iterable: 反復処理できる型(コレクション型)
  • Iterator: 反復処理実行のためのメカニズム

Iterableから生成されたIteratorは反復処理終了後に使えなくなってしまうので、再度反復処理を擦る必要がある場合はIterableからIteratorを再生成擦る必要があるよ(´・ω・`)とのことです。

Iteratorのメソッド

さっきもちょろっと使ったのですがIteratorが持っているメソッドはnextをhasNextの2つだけですねー。ソレゾレ次のようなものです。

  • hasNext:Boolean 次の要素が残っているかどうかをBooleanで返す
  • next: A 次の要素を返す(型はコレクション次第)

上の二つは抽象メソッドとして定義されているので、実際は各コレクションで実装されているものを使うような塩梅ですかねー。ちなみにiterable、Iteratorは無限に要素を持つコレクションでも使える人のことです…πとからしいですが、どう使うんだろう(´・ω・`)

シーケンス

ほんでは順序性のあるコレクションのシーケンスをざざっと見ていきますよー

リスト

シーケンスと言えば前の章でやったリストですなー。リストの主な特徴としてはイミュータブルな連結リスト構造で、先頭項目への追加削除が高速だけども任意の添字アクセスはデータ構造の都合上あまり高速ではないってのがそれですな。

欠点ぽく見える上記構造ですが、多くのアルゴリズムに適合する構造であったりで、またScala的にはパターンマッチに適している構造みたいです。そのうえイミュータブルなので値の保証もあるしねー、ということです。

とりあえず詳しくは16章とか22章なんですが、簡単なサンプルだけのっけておきますねー

// リストを定義しますよー
scala> val l = List(1,2,3)
l: List[Int] = List(1, 2, 3)
// リストの先頭要素にアクセスしますね
scala> l.head
res26: Int = 1
// リストの先頭要素以外を取り出しますよー
scala> l.tail
res27: List[Int] = List(2, 3)
配列

リストが余り得意でない添字アクセスを効率的に行うことが出来るのが配列デス。とりあえず配列サンプルをのっけてみます。

// 配列定義してみました
scala> val a = Array(1,2,3,4,5)
a: Array[Int] = Array(1, 2, 3, 4, 5)

// 4番目の要素にアクセスしてみました
// 添字アクセスは()です
scala> a(3)
res28: Int = 4
// 他の言語風にアクセスすると怒られます(´・ω・`)
scala> a[3]                     
<console>:1: error: identifier expected but integer literal found.
       a[3]
         ^

ちなみに空の配列を作るときはこんな感じです

// 要素が整数で要素数が5つの空配列を生成します
scala> val a = new Array[Int](5)
a: Array[Int] = Array(0, 0, 0, 0, 0)
リストバッファー

Listクラスは先頭へのアクセスは高速な反面、末尾へのアクセスが遅かったので末尾へのアクセスが頻繁になる場合はreverseメソッドで要素順を反転するYO!って内容を16章でやりました。

でもListBufferを使えばそんな悩みともオサラバらしいです。ミュータブルなオブジェクトのListBufferは要素の追加を効率化してくれる(先頭でも末尾でも一定時間で処理してくれるみたいです)素敵構造みたいなので、リストを構築するためのツールっていう位置づけで良いかしら?

ちなみに次のような感じで使うみたいです

// scala.collection.mutableパッケージからListBufferを読み込みます
scala> import scala.collection.mutable.ListBuffer
import scala.collection.mutable.ListBuffer

// ListBufferオブジェクトを生成します
scala> val buf = new ListBuffer[Int]             
buf: scala.collection.mutable.ListBuffer[Int] = ListBuffer()

// += 演算子で末尾に追加します
scala> buf += 1
// もう一個末尾に追加します
scala> buf += 2
// 追加結果はこんな感じです
scala> buf
res32: scala.collection.mutable.ListBuffer[Int] = ListBuffer(1, 2)

// +:演算子で先頭に追加します
scala> 3 +: buf
res33: scala.collection.mutable.Buffer[Int] = ListBuffer(3, 1, 2)

// toListメソッドでListに変換しますよ
scala> buf.toList
res34: List[Int] = List(3, 1, 2)

ListBufferを使うことでスタックオーバーフローの危険を避けることができるそうです。また必要な再帰アルゴリズムが末尾再帰でない場合はfor式、whileループ、ListBufferの組み合わせを使うのがコップ本的オススメみたいです。詳しくは22章でー

ちなみにスタックオーバーフローはこんな感じみたいですねー ⇒ wikipedia:スタックオーバーフロー

コールスタックに格納できる情報量には上限がある。コールスタックに蓄積するデータ量が多すぎるとスタックは「オーバーフロー」し、プログラム側で対策をとっていなければ通常はクラッシュしてしまう。

・・・(中略)・・・

スタックオーバーフローの一番の原因は再帰による無限ループである。ただし、末尾最適化を実装した言語では末尾再帰をループへ展開することができ、末尾再帰ではスタックオーバーフローは起こらない。末尾再帰はループ処理に最適化されるので、再帰することそれ自体でスタックを消費することが無いからである。

配列バッファー

配列と同様の操作が可能だけども、先頭と末尾の要素を追加削除できるものとしてArrayBufferがあるみたいです。ただしArrayBufferは配列よりも若干速度が遅く、追加ds駆除操作は平均では一定時間になるものの、バッファーの内容新しい配列で保持する必要上、サイズに比例する時間が必要になる場合があるみたいです。

これだけ聞くとなんのメリットもなさそうなんですが、配列のサイズを指定せずに構築出来るのがメリットっぽいですな(´・ω・`)

とりあえず触ってみますかね

// パッケージをインポートします
scala> import scala.collection.mutable.ArrayBuffer
import scala.collection.mutable.ArrayBuffer

// ArrayBufferオブジェクトを生成します
scala> val buf = new ArrayBuffer[Int]()           
buf: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer()
// 末尾に追加します
scala> buf += 1                                   
// さらに末尾に追加しますよ
scala> buf += 2
// 配列の内容を見てみますねー
scala> buf                                        
res57: scala.collection.mutable.ArrayBuffer[Int] = ArrayBuffer(1, 2)

// 先頭に要素を追加しますね
scala> 3 +: buf
res58: scala.collection.mutable.Buffer[Int] = ArrayBuffer(3, 1, 2)

//// 配列操作もできますよ(若干効率悪いけども)
// 要素数を取得します
scala> buf.length  
res59: Int = 3
// 要素にアクセスしますよ
scala> buf(2)    
res60: Int = 2

// 配列に変換するときはtoArrayっすね
scala> buf.toArray
res61: Array[Int] = Array(8, 1, 2)
待ち行列(Queue)

Listの構造なんかだと先入れ後出しの処理が向いてたりするみたいなんですが、先入れ先出しのシーケンスが必要だったらQueueクラスを使えばいいYO!とのことです。Queueにもミュータブルとイミュータブルの2種類あるらしいデスが…とりあえず触ってみますかねー

// パッケージをインポートします
// 今回はイミュータブル版を使ってみますよ
scala> import scala.collection.immutable.Queue
import scala.collection.immutable.Queue

// 空のQueueを生成しますよ
scala> val empty = new Queue[Int]             
empty: scala.collection.immutable.Queue[Int] = Queue()

// enqueueメソッドで空Queueに要素を追加します
scala> val has1 = empty.enqueue(1)            
has1: scala.collection.immutable.Queue[Int] = Queue(1)
// 複数の要素を追加しますねー
// 複数の要素を追加する場合はコレクション(ここではList)を指定しますです
scala> val has123 = has1.enqueue(List(2,3))   
has123: scala.collection.immutable.Queue[Int] = Queue(1, 2, 3)

// dequeueメソッドでQueueから要素を取り出し(削除)しマス
// (削除した要素、残りの要素) 形式のタプルで返ってきますねー
scala> val (element, has23) = has123.dequeue  
element: Int = 1
has23: scala.collection.immutable.Queue[Int] = Queue(2, 3)

上記はイミュータブル版なのでenqueueやdequeueメソッドを使って新しいQueueを生成しているんですが、ミュータブル版だと要素の追加は演算子で処理できますね

// イミュータブル版をインポートしますよー
scala> import scala.collection.mutable.Queue  
import scala.collection.mutable.Queue
// 空のQueueを定義します
scala> val queue = new Queue[Int]
queue: scala.collection.mutable.Queue[Int] = Queue()

// Queueに要素を追加する場合は+=演算子を使いマス
scala> queue += 1
// 複数の要素をコレクション形式で追加する場合は++=演算子を使います
scala> queue ++= List(2,3)
// Queueの内容です
scala> queue
res64: scala.collection.mutable.Queue[Int] = Queue(1, 2, 3)

// Queueの先頭要素(先入れしたやつ)をdequeueメソッドで削除します
// 戻りは削除した要素が返されます
scala> queue.dequeue
res65: Int = 1
// 削除されたQueueの中身です
scala> queue
res66: scala.collection.mutable.Queue[Int] = Queue(2, 3)
スタック

後入れ先出しだったらStackを使うべき!だそうです。こいつもミュータブルとイミュータブルがあるみたいですが…とりあえずミュータブル版のサンプルをやってみますねー

// ミュータブル版のパッケージをインポートデス
scala> import scala.collection.mutable.Stack
import scala.collection.mutable.Stack
// 空のスタックを生成します
scala> val stack = new Stack[Int]
stack: scala.collection.mutable.Stack[Int] = Stack()

// スタックに値を追加します
scala> stack.push(1)
// スタックの中身はこんな感じです
scala> stack
res71: scala.collection.mutable.Stack[Int] = Stack(1)

// スタックに更に値を追加します
scala> stack.push(2)
// 追加結果です
scala> stack        
res73: scala.collection.mutable.Stack[Int] = Stack(1, 2)

// 最後入れ(次のpopの候補)の要素を表示します
scala> stack.top
res74: Int = 2
// 要素は変更されません
scala> stack    
res75: scala.collection.mutable.Stack[Int] = Stack(1, 2)

// 最後入れの要素を取り出しますよ
scala> stack.pop
// 取り出した値が戻ってきます
res76: Int = 2
// 取り出した要素は削除されます
scala> stack    
res77: scala.collection.mutable.Stack[Int] = Stack(1)

ついでなんでイミュータブル版もやってみますかね

// イミュータブル版のパッケージをインポートします
scala> import scala.collection.immutable.Stack
import scala.collection.immutable.Stack
// 空のスタックを生成します
scala> val empty = new Stack[Int]
empty: scala.collection.immutable.Stack[Int] = Stack()
// 要素を追加しますね
scala> val has1 = empty.push(1)
has1: scala.collection.immutable.Stack[Int] = Stack(1)
// さらに要素を追加しますね
scala> val has12 = has1.push(2)
has12: scala.collection.immutable.Stack[Int] = Stack(1, 2)
// 最後入れ要素を確認します
scala> has12.top               
res79: Int = 2
// 最後入れ要素を取り出しますよ
// mutable版と違って取り出した要素は取得できないっすね(´・ω・`)
scala> val has1pop2 = has12.pop   
has1pop2: scala.collection.immutable.Stack[Int] = Stack(1)

うん、QueueとStackは使いこなせるようになりたいな(`・ω・´)

文字列(RichString)

Seq[Char]という文字列シーケンスとしてRichStringってのがあるみたいです。ScalaではStringからRichStringへの暗黙の型変換を提供しているので、全ての文字列はSeq[Char]として使えるとのこと…

とりあえずサンプルやってみますかねー

// 大文字が含まれているかどうかをチェックしますよ
scala> "Hello".exists(_.isUpperCase)
res84: Boolean = true

// 大文字がなければfalseデスネー
scala> "hello".exists(_.isUpperCase)
res85: Boolean = false

上記の処理の流れとしては、"Hello"というStringが暗黙の型変換によってSeq[Char]に変換され、existsメソッド経由で文字一つ一つに対してisUpperCaseメソッドを適用されるような感じですかねー

いじょー

きりがいいのでココマデー、次回は集合とマップについてやっていきます。今回も量が多いけどなるべく早く終われるように頑張ります(´・ω・`)