Scala 例 List

REPLをつかって、ぼちぼち試してみる。

関数型といわれるプログラミング言語はとにかくListの処理が充実している。

Scalaでも、それは同じ。

とりあえずの相棒は、Scala Standard Library 2.12.8 - scala.collection.immutable.Listこのページ

もっとも、Scalaのライブラリの階層を見ると、ListよりSeqをつかおうということらしい。Effective Scala(Effective Scala)

リストの生成と要素の追加

scala> Nil
res39: scala.collection.immutable.Nil.type = List()

scala> 1::Nil
res40: List[Int] = List(1)

scala> 1::2::Nil
res41: List[Int] = List(1, 2)

いきなり、とっつきにくいかもしれなけいど、ScalaっぽいListのつくりかたはこれだろうか。::で、Listの先頭に要素を追加する。

このリストの構造は、Javaだったら、java.util.LinkedListが、いちばん近いようだ。
Common Lispだったら、consというやつか。

は、Listについての、パターンマッチという機能でも使う。

実際に、Listをつくるメソッドを使ってみると、

scala> val list = List("a", "b")
list: List[java.lang.String] = List(a, b)

scala> "c" :: list
res11: List[java.lang.String] = List(c, a, b)

scala> "d" :: "c" :: list
res12: List[java.lang.String] = List(d, c, a, b)

scala> list :+ "c"
res2: List[java.lang.String] = List(a, b, c) 

scala> "c" +: list
res4: List[java.lang.String] = List(c, a, b)

scala> val list2 = List("c", "d")
list2: List[java.lang.String] = List(c, d)

scala> list ::: list2
res14: List[java.lang.String] = List(a, b, c, d)

scala> list ++ list2
res19: List[java.lang.String] = List(a, b, c, d)

scala> list
res20: List[java.lang.String] = List(a, b)

scala> list2
res21: List[java.lang.String] = List(c, d)

うえの場合の要素の追加では、もとのListは変化せず、もとのListに対して要素を追加した、別のListができる。JavaArrayListなんかとは、ちがうところ。それが、パッケージ名にもあるimmutableということ。mutableな、ListBufferもある。

java.util.ArrayListと、そのaddメソッドに相当するのは、Scala Standard Library 2.12.8 - scala.collection.mutable.ListBufferの+=メソッドになるんだろうか。

はなしがListからそれるけど

はなしがListからそれるけど、上の例からScalaについていくつかまとめておくと、

  • Listは、実際にはscala.collection.immutable.ListScala Standard Library 2.12.8 - scala.collection.immutable.Listのこと。
  • :+も+:もScalaではメソッド
    • メソッド名に記号を含むのはあり。
  • Javaではメソッドを実行するときかならず.をつけるが、Scalaではメソッドを呼び出すとき.をつけなくても良い場合がある。()も必須でなく、場合によっては、省略できる。
    • 引数がない場合
    • 引数がひとつの場合
  • scala> "c" +: list
    • Scalaではメソッド名の最後が:の場合は、メソッドの引数とオブジェクトを入れ替えて、メソッド呼び出しを記述できる。
    • a do: bという記述のメソッド呼び出しは、実際はb.do:(a)の意味になる。
  • list自体は、はじめに生成したオブジェクトまま変化していない。リストから別のリストを生成するメソッドということ。
  • Listとは、またぜんぜんべつのはなしだけど、Scalaのvalは、Javaのfinalのことで変更不可。

おなじみのメソッドでListクラスを調べてみる

ScalaのREPLを活用して、おなじみのメソッドでListを調べてみる。

scala> List().getClass
res4: java.lang.Class[_] = class scala.collection.immutable.Nil$

scala> List().getClass.getSuperclass
res5: java.lang.Class[_ >: ?0] = class scala.collection.immutable.List

scala> List() == Nil
res6: Boolean = true

scala> List[Int]()
res3: List[Int] = List()

scala> List[Int]().getClass
res4: java.lang.Class[_] = class scala.collection.immutable.Nil$

scala> List(1)
res5: List[Int] = List(1)

scala> List(1).getClass
res43: java.lang.Class[_] = class scala.collection.immutable.$colon$colon

scala> List(1,2)
res6: List[Int] = List(1, 2)

scala> List(1,2).getClass
res5: java.lang.Class[_] = class scala.collection.immutable.$colon$colon

scala> List(1,2).getClass.getSuperclass
res6: java.lang.Class[_ >: ?0] = class scala.collection.immutable.List

scala> List(1,2).getClass.getSuperclass.getSuperclass
res7: java.lang.Class[_ >: ?0] = class java.lang.Object

scala> List(1,2).getClass.getInterfaces
res8: Array[java.lang.Class[_]] = Array(interface scala.ScalaObject, interface scala.Product, interface scala.Serializable)
scala> List(1, 2).getClass.getMethods.map(_.getName).toList.sortWith(_ <= _).distinct.foreach { println _ }
$colon$bslash
$colon$colon
$colon$colon$colon
....
zip
zipAll
zipWithIndex
  • Listは、けっこう複雑なクラス階層のなかにあるようだ。
  • $colon$colonは、::クラス
  • $colon$colonは、::メソッド。::クラスだけでなく::メソッドもある。
  • :の$colonのだけでなく、記号を含んだメソッドがたくさん。
  • とにかくListは処理が充実して、メソッドがたくさん。

コップ本を復習しよう。。。。

なんだか:などの記号をやたらとメソッド名につかっていることはわかったからちょっとfilterして、

scala> List().getClass.getMethods.map{_.getName}.toList.sortWith{_ <= _}.distinct.filter{! _.contains("$") }.foreach { println _ }
addString
aggregate
andThen
....
zip
zipAll
zipWithIndex

なるほどね!
Listのメソッドの一覧を取得したところで、Listのメソッドとその挙動を調べる作業にもどる。

リストの生成と要素の追加のつづき

scala> var list = List("a", "b")
list: List[java.lang.String] = List(a, b)

scala> list :+= "c"

scala> list
res9: List[java.lang.String] = List(a, b, c)

scala> list +:= "d"

scala> list
res12: List[java.lang.String] = List(d, a, b, c)
  • listは、はじめに生成したListオブジェクトではなくなっている。

Listから要素を取得する

ほかの言語でもおなじみの、インデックス指定で要素をとりだすことができる。

scala> val list = List(1,2,3,4)
list: List[Int] = List(1, 2, 3, 4)

scala> list(0)
res7: Int = 1

scala> list(3)
res9: Int = 4

scala> list(4)
java.lang.IndexOutOfBoundsException: 4
        at scala.collection.LinearSeqOptimized$class.apply(LinearSeqOptimized.scala:51)
        at scala.collection.immutable.List.apply(List.scala:45)

おなじみのforループ

は、scalaのREPLで、例をつくっているために、行が変わったときに がついているだけで、特別な意味はない。
scala> for (e <- list) {
     |     println(e)
     | }
1
2
3
4

いかにもList操作のためにあるようなメソッド

scala> val list = List(1,2,3,4)
list: List[Int] = List(1, 2, 3, 4)

scala> list.head
res0: Int = 1

scala> list.tail
res1: List[Int] = List(2, 3, 4)

scala> list.init
res2: List[Int] = List(1, 2, 3)

scala> list.last
res4: Int = 4

Listのsort

なにげに、JavaのListは、sortメソッドないよな。java.util.Collections.sortとかつかう。

ScalaのListは、sortedとかがもともとついている。

scala> val list = scala.util.Random.shuffle(1 to 20 toList)
list: List[Int] = List(4, 2, 12, 19, 9, 14, 8, 7, 15, 18, 20, 10, 16, 17, 1, 13, 11, 5, 3, 6)

scala> list sorted
res0: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

scala> list map { "%d" format _ }  sorted
res3: List[String] = List(1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2, 20, 3, 4, 5, 6, 7, 8, 9)

scala> list map { "%02d" format _ }  sorted
res4: List[String] = List(01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)

sortBy、、、、だと?

scala>  case class Person(val name: String, val age: Int)
defined class Person

scala> val list = List(Person("c", 2), Person("a", 3), Person("b", 1))
list: List[Person] = List(Person(c,2), Person(a,3), Person(b,1))

scala> list sortBy {_.name}
res11: List[Person] = List(Person(a,3), Person(b,1), Person(c,2))

scala> list sortBy {_.age}
res12: List[Person] = List(Person(b,1), Person(c,2), Person(a,3))

Scala Standard Library 2.12.8をみなおすと、SQLのselectっぽいことがいろいろできるようだ。このあたり、JavaのListより便利でいいなあ。

Listのforを書かかずにすます

自分は、Javaプログラマー2,3年生のときにhttp://commons.apache.org/collections/api-release/org/apache/commons/collections/CollectionUtils.html(最近では、Apache Commons Functorという独立したプロダクトもあるようだ)
を見て、「えっ、リストのループって、Transfomer, Predicate, Closureの3つでなんとかなっちゃうの?ループでやりたいことって、実はこの3とおりしかないの?」ってひどく驚いたことがあった。(このClosureっていう単語だけは、あのクロージャなのかと誤解してしまいそうだが。)

そのあと、関数型言語(はじめは、Common Lisp)というものにふれて、ようやく得心がいった。「ああー、あれは、これだったのか」と

map

Listを別のListに変換する。

Listの要素ひとつひとつを、別のオブジェクトに変換して、別のListをつくる。

scala> List(1,2,3,4).map{e => "%04d".format(e)}
res25: List[String] = List(0001, 0002, 0003, 0004)

この例では、要素をeという名前をつけてあつかっている。

filter

ListからListを抽出する。

こいつは、SQLのselectのイメージか。テーブルのレコード全体から、あるレコードを抽出するイメージ

scala> List(1,2,3,4).filter{_ >= 3}
res18: List[Int] = List(3, 4)

この例では、要素に名前をつけず、_という便利な記号であつかっている。

foreach

Listの要素ひとつひとつについて、何らかの処理を実行する。

たとえば、for文をまわして、要素をprintlnっていうあれだ。

scala> List(1,2,3,4).foreach{println}
1
2
3
4

この例では、要素に名前をつけず、省略している。

foldLeft, foldRight

Listを再帰的に処理して、集約して、別のオブジェクトをつくりだす。

じぶんの表現がへたなのか、われながら何いってるかピンとこない。for文をつかって、リストを処理して、何か値をひとつ、取得したい場合がままあるんだよな。

foldは、プログラミングの、日本語の用語?だと「畳み込み」という表現になるらしい。

関数型言語のListの処理で、いちばんはじめに、とっつきづらさを味わったのはこいつだったとおもう。
例を見ても、動きがイメージできないし、つかいどころなんてまったくわからない。

畳み込まれていくようすをていねいにひとつひとつ式に書いて説明してくれている教科書もあるのだけれど、コップ本に描いてあるような要素と演算のツリーを思い浮かべるのが一番わかりやすいと思う。

foldLeft, foldRightのほかに、reduceLeft, reduceRightなんてのもある。



合計を計算する例

accは、「accumulator 蓄積するもの」の略なんだそうな。この手のサンプルでわりとみかける。eは、ここでは、「element Listの要素」ってことで。

scala> List(1,2,3,4).foldLeft(0){(acc, e) => acc + e}
res12: Int = 10

scala> List(1,2,3,4).foldLeft(0){_ + _}
res11: Int = 10

scala> List(1,2,3,4).reduceLeft{_ + _}
res0: Int = 10

上の、合計を出すのをJavaでやってみる。

forを使うならば、

public class SumA {
    
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        System.out.println(sum(list));
    }
    
    public static Integer sum(List<Integer> list) {
        Integer acc = 0;
        for (Integer e : list) {
            acc = acc + e;
        }
        return acc;
    }

}

プログラマーだったらだれでも1回ぐらいは、このような、Listをループで処理して、集約して、別のオブジェクトをつくりだすというコードを書いていると思う。このような処理と、同じ結果を出す処理を、パッと書けるのがfoldということのようだ。


Javaで、foldっぽいの書いてみると、こんな感じになるのかなあ

public class SumB {

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        System.out.println(sum(list));
    }

    private static Integer sum(List<Integer> list) {
        LinkedList<Integer> listCopy = new LinkedList<Integer>(list);
        return applyOperatorRecursively(0, listCopy, new Addition());
    }

    //foldLeftのつもり
    private static <A, B> A applyOperatorRecursively(A acc, LinkedList<B> list, 
            Operator<A, B> operator) {
        if (list.isEmpty()) {
            return acc;
        }
        else {
            B e = list.removeFirst();
            A newAcc = operator.op(acc, e);
            return applyOperatorRecursively(newAcc, list, operator);
        }
    }

    private static interface Operator<A, B> {
        A op(A a, B b);
    }

    private static class Addition implements Operator<Integer, Integer> {
        @Override
        public Integer op(Integer a, Integer b) {
            return a + b;
        }
    }

}

もっと、立派なライブラリはApache Commons Functor 1.0-SNAPSHOT APIとか、http://functionaljava.org/examples/1.5/とかあるみたいだ。

Javaじゃなくて、Scalaにもどろう。


Listの大きさを求める例

scala> list.foldLeft(0){(acc, e) => acc + 1}
res6: Int = 4

scala> list.size
res7: Int = 4

scala> list.count { _ => true }
res8: Int = 4


最大値を求める例

scala> val list = scala.util.Random.shuffle(1 to 10 toList)
list: List[Int] = List(3, 9, 6, 2, 10, 8, 5, 7, 4, 1)

scala> list.reduceLeft{(acc, e) => if (acc > e) acc else e }
res0: Int = 10

scala> list.max
res9: Int = 10

LeftとRightのちがい。
Leftは先頭から処理、Rightは後尾から処理

scala> List(1,2,3,4).foldLeft(List[Int]()){(acc, e) => e :: acc}
res75: List[Int] = List(4, 3, 2, 1)

scala> List(1,2,3,4).foldRight(List[Int]()){(e, acc) => e :: acc}
res76: List[Int] = List(1, 2, 3, 4)
Listの、その他のメソッドと例
scala> val p = "([A-Z])".r
p: scala.util.matching.Regex = ([A-Z])

scala> "abCdEf".toList
res0: List[Char] = List(a, b, C, d, E, f)

scala> "abCdEf".toList.map { e => e.toString }.map { e => e match {
     |     case p(c) => "_" + c
     |     case _ => e.toUpperCase
     | }} mkString
res68: String = AB_CD_EF
scala> "AB_CD_EF".split("_")
res1: Array[java.lang.String] = Array(AB, CD, EF)

scala> "AB_CD_EF".split("_").zipWithIndex
res2: Array[(java.lang.String, Int)] = Array((AB,0), (CD,1), (EF,2))

scala> "AB_CD_EF".split("_").zipWithIndex.map { e => e match {
     |     case (h, 0) => h.toLowerCase
     |     case (t, _) => t.toLowerCase.capitalize
     | }} mkString
res72: String = abCdEf

Listの例じゃなくなっちゃった。Scala Standard Library 2.12.8 - scala.Arrayには、zipWithIndexメソッドはない。Scala Standard Library 2.12.8 - scala.Predefのimplicit def wrapRefArrayの効用。

ほかのList

scala.collection.mutable.ListBuffer

Scala Standard Library 2.12.8 - scala.collection.immutable.Listのようにリストに要素を追加したら新しいリストができるのでなくて、java.util.ArrayListのように、おなじリストに要素を追加したいならScala Standard Library 2.12.8 - scala.collection.mutable.ListBufferがある。

scala> val listBuffer = ListBuffer()
listBuffer: scala.collection.mutable.ListBuffer[Nothing] = ListBuffer()

scala> listBuffer += "a"
<console>:10: error: type mismatch;
 found   : java.lang.String("a")
 required: Nothing
       listBuffer += "a"

あら?だめだ。型を指定して、もう一度、、、、

scala> val listBuffer = ListBuffer[String]()
listBuffer: scala.collection.mutable.ListBuffer[String] = ListBuffer()

scala> listBuffer += "a"
res12: listBuffer.type = ListBuffer(a)

scala> listBuffer
res13: scala.collection.mutable.ListBuffer[String] = ListBuffer(a)

scala> listBuffer += "b"
res14: listBuffer.type = ListBuffer(a, b)

scala> listBuffer
res15: scala.collection.mutable.ListBuffer[String] = ListBuffer(a, b)

scala> "c" +=: listBuffer
res16: listBuffer.type = ListBuffer(c, a, b)

scala> listBuffer
res17: scala.collection.mutable.ListBuffer[String] = ListBuffer(c, a, b)

同一のListBufferオブジェクトに対して、a,b,cが追加されているのがわかる。