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


Scalaコップ本の16章残りをやっていきましょうかね、今回はリストオブジェクトのメソッドからやっていきますよ

Listオブジェクトのメソッド

前回まではListクラスのメソッドとして実装された操作をやってきたのですが、今回はListクラスのコンパニオンオブジェクトであるscala.Listオブジェクトのメソッドをやっていきますよー

要素からリストを作る:List.apply

これまでリストを作成するときはList(1,2,3)みたいなリテラルを使ってきたのですが、同様のリスト構築を行うapplyメソッドがあったりします。

とりあえずこんな感じです

// リスト作ってみますよ
scala> List.apply(1,2,3)
res0: List[Int] = List(1, 2, 3)

ぶっちゃけリテラル版の別名みたいなもんだと考えればいいですかね(もしくはリテラルのほうがこっちのエイリアスみたいなもんかな?)

整数の範囲を作る:List.range

一定範囲の整数から構成されるリストを作るメソッドとしてrangeがありますね。List.range(from, until)でfrom以上、until未満の整数リストを作ってくれるみたいです

// 1以上5未満の整数から構成されるリストです
scala> List.range(1,5)
res1: List[Int] = List(1, 2, 3, 4)

また、第3パラメータを設定すると要素間のインクリメントの返すことが出来るみたいですね

// 1以上5未満の範囲で
// 1から2ずつ増やして作られる整数から構成されるリストです
scala> List.range(1,5,2) 
res2: List[Int] = List(1, 3)

ちなみにインクリメントされる値は負の数でもOKみたいです。その場合はList(from, until, step)とした場合from以下でuntilより大きい範囲でfromにstepで指定された数ずつ加え(負の数なので実際は引かれる)られた要素のListが構成されますねー

たとえばこんな感じです

// -5より大きく9以下の範囲で
// 9から-3ずつ増やして(3ずつ減らして)作られる整数から
// 構成されるリストです
scala> List.range(9, -5, -3)
res4: List[Int] = List(9, 6, 3, 0, -3)

うん、range → ループってのはPythonっぽくて好きです(´・ω・`)ねー

同じ値からリストを作る:List.make

makeを使うと0個以上の同じ要素からリストを作ることが出来るみたいです。イメージを固めるために実際に使ってみましょうかねー

List.make(num, element)でnum個のelement要素をもつリストを作りますねー

// 3という要素が5個存在するリストを作ります
scala> List.make(5,3)
res6: List[Int] = List(3, 3, 3, 3, 3)

// 5個のhelloを作りますよ
scala> List.make(5,"hello")
res7: List[java.lang.String] = List(hello, hello, hello, hello, hello)

なんだかf5的なメソッドですな…何に使うんだろ…テストとかかな?

リストのジッパー外し操作:unzip

Listクラスのzipメソッドでは2個のリストを基にペアを要素とする新しいリストを構築するものでしたが、その逆の操作を行うのがunzipデス

とりあえずzipメソッドはこんな感じでした

// 2つのリストをペアでまとめますよ
scala> List(1,2,3,4,5).zip(List('a','b','c'))
// ペアが作れないところは切り捨てです
res8: List[(Int, Char)] = List((1,a), (2,b), (3,c))

んじゃ上で作ったペアリストをunzipしてみますよ

// unzipしてみますよ
scala> val (intList, charList) = List.unzip(List((1,'a'), (2,'b'), (3,'c')))
// 2つのリストにもどされましたな
intList: List[Int] = List(1, 2, 3)
charList: List[Char] = List(a, b, c)

さすがに切り捨てられた要素は復活できませんが、ペアリストを2つのリストに戻すことができました(`・ω・´)

ちなみにunzipがListオブジェクトのメソッドなのは、unzipが特定の(ペアを要素とする)リストにしか使えないメソッドだからみたいです。「Scalaの型システムはクラスのメソッドはそのクラスの全てのインスタンスに使えなければならない( ー`дー´)キリッ」ということでListオブジェクトのメソッドに追いやられてるそうです。

ためしに適当なリストに対して適応して、あえなく怒られてみます

scala> List.unzip(List(1,2,3,4,5))                                          
// このリストじゃ使えねーよ(#゚Д゚)と言われました
<console>:5: error: overloaded method value unzip with alternatives [A,B](Iterable[(A, B)])(List[A], List[B]) <and> [A,B](List[(A, B)])(List[A], List[B]) cannot be applied to (List[Int])
       List.unzip(List(1,2,3,4,5))
            ^

将来的に一部のインスタンスにしか使えないメソッドもクラスインスタンスに含むよ!っていうふうにScalaの型システムが拡張されたら、もしかしたらListクラス入りするかもね(´・ω・`)っていうことが書いてありますが…たぶん無いだろうなぁ

リストの連結:List.flatten, List.concat

前回くらいにもでてきたリスト内リストの要素を一つのリストとしてまとめてしまうflattenメソッドです

// 3種混合リストをひとつに纏めてみましたよ(`・ω・´)
scala> List.flatten(ListList(1,2,3), List('a','b','c'), List("Hello", "World")))
res13: List[Any] = List(1, 2, 3, a, b, c, Hello, World)

こいつがListオブジェクトのメソッドなのもunzipと同じでリスト要素をもつリストにしか適用できないからですねぇ(´・ω・`)

でもリスト内リストじゃなくて複数のリストの要素を結合したい場合の方が多い気がするのですが、そんなときはconcatメソッドを使うといいみたいです。

// 複数のリストの要素を一つのリスト要素してまとめマス
scala> List.concat(List(1,2,3), List('a','b','c'), List("Hello", "World")) 
res14: List[Any] = List(1, 2, 3, a, b, c, Hello, World)

// パラメータはいくつでもいいそうです
scala> List.concat(List(1,2,3), List('a','b','c'), Nil)                   
res15: List[AnyVal] = List(1, 2, 3, a, b, c)

// 空リストだけでもOKです
scala> List.concat(Nil)                                
res16: List[Nothing] = List()
リストのペアのマッピングやテスト:List.map2,List.forall2,List.exists2

Listクラスのmap, forall, existに似ているけどもパラメータとして「2個のリスト」と「2個のパラメータをとる述語関数」をとるmap2, forall2, exists2です。それぞれ2つのリストの要素の先頭から順にペアとして述語関数に適応していくような感じですかねー

まずはmap2を動かしてみますかね。こいつは述語関数の結果を要素としたリストを構築するみたいですね

// 2つのリストの要素をペアで述語関数に適応した結果がリストになりますね
scala> List.map2(List(1,2, 3), List(1,2,3))(_ * _) 
res18: List[Int] = List(1, 4, 9)

// ペアが作れない場合は切り捨てられますな
scala> List.map2(List(1,2), List(1,2,3))(_ * _)   
res19: List[Int] = List(1, 4)

んじゃforall2を動かしてみますよ。こちらは述語関数の結果がall trueになるかどうかを観るわけですねー

// 全てのペアが条件を満たせばtrueですな
scala> List.forall2(List(1,2,3), List(2,3,4))(_ < _)
res20: Boolean = true

//ひとつでも条件外だとfalseになります
scala> List.forall2(List(1,2,3), List(2,1,4))(_ < _)
res21: Boolean = false

// ちなみにペアが作られないと要素が切り捨てられてなかった事になるみたいです
scala> List.forall2(List(1,2,3), List(2,3))(_ < _)  
res22: Boolean = true

最後にexists2ですねー。こちらはforall2に対して条件に合うものが一つでも存在すればOKみたいです

// ひとつでも条件にあえばOKです
scala> List.exists2(List(1,2,3), List(0,4,2))(_ < _)
res23: Boolean = true

// 全部が条件外だとダメダメですな
scala> List.exists2(List(1,2,3), List(0,1,2))(_ < _)
res24: Boolean = false

// こちらもペアが作られないと捨てられます(´・ω・`)
scala> List.exists2(List(1,2,3), List(0,1))(_ < _)  
res25: Boolean = false

基本的に2つのリストをゴニョゴニョしたりするときにはこちらの2トリオを使うのがヨサゲですね

まずは型推論の可否をサンプルベースでみていきます

以前に作成した俺俺マージソート実装を利用してScala型推論アルゴリズムを見ていきますよ

以前に実装したのマージソートはこんな感じでした

// カリー化を使って比較ルールとソート対象リストを別に受け付けるようにしますよ
def msort[T](less:(T,T) => Boolean) (xs:List[T]):List[T] = {
  // ソート比較用メソッドデス
  def merge(xs:List[T], ys:List[T]):List[T] = (xs, ys)match {
    // 比較対象のリストの片方が空の場合は残ったほうを返します
    case (Nil, _) => ys
    case (_, Nil) => xs
    // 各々のリストの先頭の値を比較して
    // 条件に合う方を先頭にしたリストを再帰的に構築します
    // (残った方に再帰的に同じ処理を繰り返します)
    case (x :: xs1, y :: ys1) => {
      if(less(x,y)) x :: merge(xs1, ys) else y :: merge(xs, ys1) 
    } 
  }
  // リストの真ん中の位置を取得します
  val n = xs.length / 2
  // 要素が1つのみの場合はそのまま返します
  if (n == 0 ) xs
  else {
    // 前半、後半のリストに分けます
    val (ys, zs) = xs.splitAt(n)
    // 再帰的に分割したリスト要素を使って比較します
    // lessはパラメータとして与えられた比較ルールですね
    merge(msort(less)(ys), msort(less)(zs))  
  }
}

んじゃ、このマージソートとListクラスのsortを比較してみます

// 俺俺マージソートです
scala> msort((x:Char, y:Char) => x > y)(List('a','b','c','d','e'))
res31: List[Char] = List(e, d, c, b, a)

// Listクラスのソートです
scala> List('a','b','c','d','e').sort(_ > _)                      
res32: List[Char] = List(e, d, c, b, a)

この2つの式を見比べてみると比較関数の書き方がmsortのほうは名前付きパラメータに型を明示してあるけれども、sortのほうはアンダースコアで省略しています。

実際msortで省略形を使うと怒られます。

scala> msort(_ > _)(List('a','b','c','d','e')) 
// 型がわかんねーよ(#゚Д゚)って怒られました
<console>:13: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$greater(x$2))
       msort(_ > _)(List('a','b','c','d','e'))
       
// 一方sortは冗長な書き方が出来るみたいですな
scala> List('a','b','c','d','e').sort((x:Char, y:Char) => x > y)
res34: List[Char] = List(e, d, c, b, a)

なんでこんな違いがあるかをScala型推論の流れを追いつつみていきますかねー

Scala型推論の戦略

Scala型推論は処理フローによって次のように戦略を変えるみたいです。ここではm(args)というメソッドを考えてみます

  • mというメソッドが既知の型をもっているかどうかをチェック

  • mが既知の型を持っている場合

  • その型を使って引数がどのような型になっていなければならないかを推論

  • 上記で解決しない場合

戦略を実際の処理に当てはめる:sort編

上記の型推論戦略をList('a', 'b', 'c', 'd', 'e').sort(_ > _)に当てはめて考えてみます

まずList('a', 'b', 'c', 'd', 'e')の型はList[Char]なので、sortにパラメータとして渡される比較関数は((Char, Char) => Boolean)型になるはずで、sortの戻りじたいもList[Char]になるはず!と推論されます。

この推論をもとにsortに渡される比較関数(_ > _)は((x:Char, y:Char) => x > y) に展開されるみたいです。

戦略を実際の処理に当てはめる:msort編

逆に、実際にはコンパイラ様に怒られるmsort(_ > _)(List('a', 'b', 'c', 'd', 'e'))について考えてみます。

msortはカリー化された多相的なメソッド型なので不明な型としてのTを使って、比較関数は((T, T) => Boolean)型でmsortの戻り値はList[T]になると推測されます。この結果、msortでは明示的に型を指定しないとインスタンス型がわからないという状態のため、コンパイラはメソッドのの引数型による推論を行なおうとします。

...しかしながら(_ > _)という略記法の関数リテラルのため型の情報がまったくなく、結果的に型推論がおこなえないことになります。

msortで略記法を使うためには

以上を踏まえてmsortに明示的な型パラメータを与えることで略記法による比較関数を使用することが出来るみたいです。

// 明示的に型パラメータを与えてみますよ
scala> msort[Char](_ > _)(List('a','b','c','d','e'))                   
// おお!できた
res36: List[Char] = List(e, d, c, b, a)

また、msort自体を書き直すという手もあるみたいです。具体的にはパラメータの位置を逆にすることで。第1パラメータに適用されるリストの型を利用して比較関数の型を推測できるようにします。

修正したものはこんな感じです

// 引数の順序を逆にすることで第1パラメータの型からの推論を可能にします
def msort[T] (xs:List[T])(less:(T,T) => Boolean):List[T] = {
  def merge(xs:List[T], ys:List[T]):List[T] = (xs, ys)match {
    case (Nil, _) => ys
    case (_, Nil) => xs
    case (x :: xs1, y :: ys1) => {
      if(less(x,y)) x :: merge(xs1, ys) else y :: merge(xs, ys1) 
    } 
  }
  val n = xs.length / 2
  if (n == 0 ) xs
  else {
    val (ys, zs) = xs.splitAt(n)
    // 呼び出し順もかえますね
    merge(msort(ys)(less), msort(zs)(less))  
  }
}

////実行します
scala> msort(List('a','b','c','d','e'))(_ >_) 
// 第1パラメータのList[Char]型を使って無事推論できました。
res37: List[Char] = List(e, d, c, b, a)
上記を踏まえて

ライブラリーなどで関数以外の引数と関数引数を取るような多相的なメソッドを設計する際には、カリー化されたパラメータリストの最後に関数をいれることで型推論できるようにスべし!ということみたいですね。

型推論できるようにすることで、ライブラリのユーザーも入力項目(型とか)を省略できるので使いやすいものにすることが出来ますし…うん、覚えておこうc⌒っ゚д゚)っφ メモメモ...

畳込み演算の型推論

より複雑な型推論のケースとして畳込み演算を考えてみますよ

サンプルとして右畳込みを例にやっていきますねー、右畳込みは前回やったとおりに次のように右側(末端)の要素から2項演算を行っていくメソッドでしたー

scala> (List(1,2,3,4,5) :\ List(1))(_ :: _)
res47: List[Int] = List(1, 2, 3, 4, 5, 1)

ちなみに右畳込みは空リストを使う場合には明示的に型を指定する必要がありました

// 空リストに型を指定してやりますよ
scala> (List(1,2,3,4,5) :\ List[Int]())(_ :: _)
res48: List[Int] = List(1, 2, 3, 4, 5)

// 型がちがうよーと型エラーが出されます(´・ω・`)
scala> (List(1,2,3,4,5) :\ List())(_ :: _)
<console>:12: error: type mismatch;
 found   : Int
 required: Nothing
       (List(1,2,3,4,5) :\ List())(_ :: _)

今度はこの現象を追っていきますかねー

型推論のパラメータ事情

関数に対する型推論を行う場合は関数に渡されるパラメータによって判断されます。なので、今回の空リストにリスト要素を結合していく演算では先頭値になるList()というNothing型があらわれるせいで、次のような型であると判定されるみたいです

(List[T], List[Nothing]) => List[Nothing]

このように型推論機の推論が速すぎた結果、生成されるのがList[Nothing]型であるという推論に合致しない値が返された故にエラーになってしまうらしいのです…カリー化の際は第一引数リストを考慮するという(普通ならとても役にたつ)原則がアダになってしまうわけですなー

結果的に型をしっかりと指定してやらないとダメよ(´・ω・`)となってしまうわけです

型推論的なまとめのアレ

結論としてScala型推論は限界があるから不明なエラーに悩んだら型アノテーションつけて回避してちょ(´・ω・`)ってことですねー

MLやHaskelみたいなHindley-Milnerスタイルだとこういう問題は置きないらしいんですが、Scalaでやってるようなオブジェクト指向のサブ型の取り扱いを実現するためのトレードオフです!とのことなので、まあそんなもんだなぁ…と思うのが吉ですかね…

とりあえずHindley-Milnerスタイルは時間があったら調べてみようかな…

いじょうー

ようやくScala的リストの世界である16章がおわりです…次は17章コレクションのあれやこれだっけか…が、頑張ります(´・ω・`)