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


Scalaコップ本の23章の続きをやっていきますよ。今回はfor式によるクエリーからやっていきますデス(´・ω・`)

for式によるクエリー

for式の記法はデータベース的に利用することが可能みたいですな(´・ω・`)なのでそんなクエリー的なfor式の使い方をいくつか見ていきますよ

まずはデータベースとして利用する本棚を用意します

// 本オブジェクトをケースクラスで定義しますね
case class Book(title:String, authors:String*)
  
// 本を束ねて本棚を構築しますデス
val books:List[Book] = {
  List(
    Book("吾輩は猫である", "夏目漱石"),
    Book("山月記", "中島敦"),
    Book("臨終まで", "梶井久"),
    Book("海", "梶井基次郎"),
    Book("ある女の生涯", "島崎藤村"),
    Book("夜明け前", "島崎藤村"),
  )
} 

上記のように構築した本棚データベースに対してfor式でのクエリーを発行してやりますよ(`・ω・´)

まずは作者名での検索です、とりあえず先頭一致検索をしてみますよ

scala> for(b <- books; a<-b.authors; if a startsWith "梶井") yield b.title
res14: List[String] = List(臨終まで, 海)

次に作品名での検索をします。こちらは一致検索デスね

scala> for(b <- books; if(b.title indexOf "山") >= 0) yield b.title
res15: List[String] = List(山月記)

最後にちょっと複雑なクエリーとして2冊以上の著書を持っている作家を検索してみます

scala> for(b1 <- books; b2 <- books; if b1 != b2; 
     |   // 2つの作家ループを比較して各1回以上出てくる作家を取得します
     |   a1 <- b1.authors; a2 <- b2.authors; if a1 == a2)
     |   yield a1
res17: List[String] = List(島崎藤村, 島崎藤村)

ただ、上記の結果だと作家名が重複して出てしまうので重複を削除する関数を次のように定義してラップしてやりますかね

// 重複削除関数です
def removeDuplicates[A](xs:List[A]):List[A] = {
  if(xs.isEmpty) xs
  // リストの各要素から重複を排除する処理を再帰処理で
  else xs.head :: removeDuplicates(
    // 先頭要素とそれ以外の全要素で一致しないかどうか判定します
    xs.tail filter ( x => x != xs.head)
  )  
} 

// 重複削除処理で包んでforクエリーを発行してやります
scala> removeDuplicates(
     |   for(b1 <- books; b2 <- books; if b1 != b2; 
     |     a1 <- b1.authors; a2 <- b2.authors; if a1 == a2)
     |     yield a1
     | )
// 結果です。ちゃんと重複が排除されました(`・ω・´)
res18: List[String] = List(島崎藤村)

ちなみに先程の重複削除処理ではfilterを利用しましたが、for式を使って次のように書き換えてやることも可能ですね(´・ω・`)

def removeDuplicates[A](xs:List[A]):List[A] = {
  if(xs.isEmpty) xs
  else xs.head :: removeDuplicates(
    // filterメソッドをfor式内のフィルタで置き換えます
    for(x <- xs.tail if x != xs.head) yield x
  )  
} 

for式の変換

すべてのfor式はmapやflatMap, filterの3つの高階関数によって表現され、実際コンパイラ様は各for式を各々の高階関数へと変換しているみたいです。各変換の様子をザザッとみていきますよ(`・ω・´)

ジェネレータが1個のときのfor式の変換

まずは次のような一番単純な形式のfor式です

for (x <- expr1) yield expr2

変換後はmapを利用した処理に置き換わります

expr1.map(x => expr2)

ここらへんはあっさりですな

1個のジェネレータと1個のフィルターで始まるfor式の変換

先程の単純for式にフィルタを追加してみます

for(x <- expr1 if expr2) yield expr3

まずはifによるフィルタをfilter関数に置き換えてみます

for(x <- expr1 filter(x => expr2)) yield expr3

次にジェネレータ部分を先ほどと同様にmap関数に置き換えて完了です(`・ω・´)

expr1 filter (x => expr2) map (x => expr3)

ちなみに今回追加したfilterのあとに新たにfor式の要素が追加された場合は、まずfilterの変換を行ってから次の要素の変換にとりかかるらしいです。例えば次のようなfor式があった場合を考えてみます。

for(x <- expr1 if expr2; if expr3) yield expr4

この場合はまず、最初のフィルタ要素を処理してやって

for(x <- expr1 filter(x => expr2); if expr3) yield expr4

次に二番目のフィルタ要素にかかるみたいです

for(x <- expr1 filter(x => expr2) filter(expr3 => x)) yield expr4

for式の要素が4つ以上に増えた場合も上記のように順繰りに変換される模様です(´・ω・`)

2個のジェネレータで始まるfor式の変換

ここでジェネレータが複数あるパターンを考えてみますネ(´・ω・`)例えば次のようなパターンを想定してみますよ

for(x <- expr1; y <- expr2); seq) yield expr3

この場合はまず1番目のジェネレータがflatMapに変換されて、残りの部分がこれまでのやり方で順次展開されていくみたいです(`・ω・´)

expr1 flatMap(x => for(y <- expr2;seq) yield expr3)

そんな感じで最終的には次のような形に変換されるのかしら?

expr1 flatMap(x => expr2 [seqを変換した要素] map (x => expr3))

ちなみに上記の複数ジェネレータパターン例として、最初の方で使った本棚データベースに対する2冊以上の著書を持っている作家検索の問い合わせを展開してみます。展開対象となるfor式は次のものですね

for(b1 <- books; b2<-books if b1 != b2;
  a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
  yield a1

上記に沿って、まず第1ジェネレーターがこんな感じで展開されますね

// まずは第1ジェネレータを展開
books flatMap(b1 =>
  for( b2<-books if b1 != b2;
    a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
    yield a1
)

上記に沿って、まず第1ジェネレーターがこんな感じで展開されますね

books flatMap(b1 =>
  for( b2<-books if b1 != b2;
    a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
    yield a1
)

続いてb2<-books if b1 != b2;の第2ジェネレータ&フィルタ部が次のように展開されます

books flatMap(b1 =>
  // 第2ジェネレータとソレに付随するフィルタを展開
  books filter(b2 => b1 != b2) flatMap (b2 =>
    for(a1 <- b1.authors; a2 <- b2.authors if a1 == a2)
    yield a1
  )
)

…と順に展開されることで最終的にこうなるみたいです

books flatMap(b1 =>
  books filter(b2 => b1 != b2) flatMap (b2 =>
    // 第3ジェネレータを展開
    b1.authors flatMap (a1 => 
      // 第4ジェネレータとソレに付随するフィルタを展開
      b2.authors filter (a2 => a1 == a2) map (a2 =>
       a1))))

とりあえずこんな感じで展開されていくみたいですね(`・ω・´)

ジェネレータに含まれるパターンの変換

for式のジェネレータの中には(x, y) <- exprの形式で表現されるものもあったので、こいつに関する展開方法を見ていきますよ

まずはジェネレータで表現されるのがタプルの場合を見ていきます。対象となるfor式はこんな感じですね

for((x1, x2, ... ,xn) <- expr1) yield expr2

こいつはこんな感じに展開されるそうです

expr1 map { case (x1, x2, ... ,xn) => expr2 }

とりあえずタプルに関しては通常のmap展開の代わりにケースを利用するだけでわりとあっさりと展開できるみたいです

上記と比較してジェネレータの左辺が単純でないパターンの場合は結構めんどくさいみたいです。例えば次のようなパターンを考えてみます。

for(pattern <- expr1) yield expr2

前回軽く触れたとおりfor式ジェネレータでのパターンマッチはmatchErrorを投げることはないので、実際には次のように展開されますデス(`・ω・´)

// まずはマッチするかどうかを判定します
expr1 filter {
  case pattern => true
  case _ => false
// マッチする場合は実際の反復処理を行ないます
} map {
  case pattern => expr2
}

上記のようにフィルタリング→マッピングのような流れになって、1クッションふえるような塩梅になるみたいですね。ここに他のジェネレータやフィルタや定義が追加された場合でも同じように展開されるとか。詳しくはScala Language Specification(参考文献)を参照しろとのことなので省略します(´・ω・`)

定義の変換

そういやすっかり残ってたfor式の要素である定義を変換してみますよ。定義の変換のサンプルとなるのはこの式です

for(x <- expr1; y= expr2; seq) yield expr3

定義部分を変換するとこんな感じになるみたいです。

for((x, y) <- (x <- expr1) yield (x, expr2);seq) yield expr3

上の変換をみてみるとexpr2はxの値が変更されるたびに再評価されるわけですな、なのでジェネレータによって新しく生成される変数を定義で利用しないような次のようなコードは結構無駄な評価を繰り返すことになるみたいです。

for(x <-1 to 1000; y = hoge) yield x * y

なのでこんなふうにして無駄な計算量を節約しましょう

val y = hoge
for(x <-1 to 1000) yield x * y
forループの変換

ここまではyieldを利用したfor式ばかりをみてきたのですが、forループ自体はそう変換されるのかを見ていきますネ。例えば次のような形のものです

 for (x <- expr1) body

これはforeachを使って次のように変換されるみたいです

expr1 foreach (x => body)

もう少し複雑なループを見てみますね、例えばこんな感じのforループです
>|scala
for(x <- expr1; if expr2; y <- expr3) body
|

これはこんな感じに変換されるみたいです。mapやflatMapがforeachに置き換わるけども、全体的な雰囲気は同じような感じですかね(´・ω・`)

expr1 filter(x => expr2) foreach(x => expr3 foreach (y => body))

またジェネレータによって生成された要素から新たにジェネレータを利用するパターンを見てみますよ、例えばこんなループがあったとすると

var sum = 0
for(xs <- xss; x <-xs) sum += x

こんな感じで変換されるわけですな(´・ω・`)なるほど

var sum = 0
xss foreach( xs =>
  xs foreach(x =>
    sum += x))

うん、for式がどう変換されるかを意識しておくとある程度効率のいいコードがかけるようになりそうな予感…あとは努力だな(´・ω・`)

いじょー

時間切れのためここまでです。次回でfor式関連は終りにしたいなぁ(´・ω・`)が、頑張ります。