Scala 例 パターンマッチ pattern matching

Scalaの便利機能、パターンマッチを試してみる。パターンマッチは、条件分岐に使う。

Scalaのパターンマッチ、"match-case"は、Javaの"switch-case"と見た目はちょっと似ているけれど、もうまったくの別物!パターンマッチは、HaskellやF#にもでてきた。

おなじみのif-elseを差し置いて、いかにも使いたくなる例ってなんだろうな。

基本

matchとcase

"match-case"は、下のような書き方をする。
これは、Javaの"switch-case"とたいして違わない例

scala> def assignStringToNumber(x: Int): String = x match {
     |     case 1 => "a"
     |     case 2 => "b"
     |     case 3 => "c"
     |     case _ => "others"
     | }
assignStringToNumber: (x: Int)String

scala> assignStringToNumber(1)
res5: String = a

scala> assignStringToNumber(4)
res7: String = others

Scalaのパターンマッチ、"match-case"は、Int型以外にも、さまざまな型で、利用できる。
したは、Tupleに利用している。

scala> 1 to 100 map { 
     |         e => ( e % 3, e % 5) match {
     |         case (0, 0) => "FizzBuzz"
     |         case (0, _) => "Fizz"
     |         case (_, 0) => "Buzz"
     |         case _ => ""
     |     }
     | } foreach { println }

just another scala quant
FizzBuzzいろいろ

ガード条件

caseに指定する値について、ifで条件付けした値を指定することができる。

これをつかって、ディレクトリを再帰的に検索するコードを書いてみる。

package myapp.search
import java.io.File

object Main {
    def main(args: Array[String]): Unit = {
        DirFinder(new File("test-data/myapp/search/Main")).find().foreach { println(_) };
    }
}

class DirFinder(val rootDir: File) {

    def find(): List[File] = findIn(rootDir)

    def findIn(file: File): List[File] = file match {
        case file if file.isFile() => List(file)
        case file if file.isDirectory() =>
            file :: file.listFiles().foldLeft(List[File]()){ _ ::: findIn(_) }
        case _ => List[File]()
    }
}

object DirFinder {
    def apply(file: File): DirFinder = {
        new DirFinder(file);
    }
}

実行

test-data\myapp\search\Main
test-data\myapp\search\Main\1a
test-data\myapp\search\Main\1b
test-data\myapp\search\Main\1b\2a
test-data\myapp\search\Main\1b\2b
test-data\myapp\search\Main\1b\2b\3c
test-data\myapp\search\Main\1b\2b\3c\4p.txt
test-data\myapp\search\Main\1b\2p.txt
test-data\myapp\search\Main\1p.txt
F:\Data\Eclipse\workspaces\Scala\ScalaNote\test-data\myapp\search>tree /F Main
フォルダ パスの一覧
ボリューム シリアル番号は 781F-C3ED です
F:\DATA\ECLIPSE\WORKSPACES\SCALA\SCALANOTE\TEST-DATA\MYAPP\SEARCH\MAIN
│  1p.txt
│
├─1a
└─1b
    ├─2a
    ├─2b
    │  └─3c
    │          4p.txt
    │
    └─2p.txt


書いてはみたものの、Scalaでも、これは、プチめんどくさいな。

Javaでも、パッと一行ですむ方法はなくて、やっぱり再帰で、やるんだろうけど、再帰の便利APIがない分、割り増しでめんどうくさそうだ。

Commons IO http://commons.apache.org/io/api-release/org/apache/commons/io/FileUtils.htmlのlistFilesメソッドとかなのかなあ。

Javaのライブラリは、Scalaからも使うことができる。ためしにやってみよう。

package myapp.search
import java.io.File
import org.apache.commons.io.filefilter.IOFileFilter
import org.apache.commons.io.FileUtils
import org.apache.commons.io.filefilter.DirectoryFileFilter
import org.apache.commons.io.filefilter.TrueFileFilter
import org.apache.commons.io.filefilter.FileFilterUtils
import scala.collection.JavaConverters._

object Main2 {
    def main(args: Array[String]): Unit = {
        val fileFilter = FileFilterUtils.trueFileFilter
        val dirFilter = FileFilterUtils.trueFileFilter
        FileUtils.listFiles(new File("test-data/myapp/search/Main"), fileFilter, dirFilter).asScala.foreach { println(_) }

    }
}

Scala Standard Library 2.12.8 - scala.collection.JavaConvertersという便利クラスがあり、asScalaメソッドで、Javaのコレクション系のオブジェクトをScalaのコレクション系のオブジェクトに変換できるようだ。

実行

test-data\myapp\search\Main\1b\2b\3c\4p.txt
test-data\myapp\search\Main\1p.txt

実行したら、ディレクトリは抽出されなくて、ファイルしか抽出されないな。パターンマッチを試しているのに、またはなしがそれすぎるから、もういいや。

applyとunapply

パターンマッチいろいろ

Scalaのパターンマッチ、"match-case"は、さまざまオブジェクトに利用することができる。

コンストラク

Scalaのcaseは、コンストラクタで場合わけができる。

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

scala> val list = List(Person("a", 10), Person("b", 20), Person("c", 30))
list: List[Person] = List(Person(a,10), Person(b,20), Person(c,30))

scala> list filter { e => e match {
     |         case Person(_, x) if x >= 20 => true
     |         case _ => false
     |     }
     | }
res10: List[Person] = List(Person(b,20), Person(c,30))
List

先頭の要素と残りの要素

scala> def sum(list: List[Int]): Int = list match {
     |     case Nil => 0
     |     case x :: xs => x + sum(xs)
     | }
sum: (list: List[Int])Int

scala> sum(List(1,2,3,4))
res3: Int = 10
scala> def sum(list: List[Int]): Int = list match {
     |     case List() => 0
     |     case List(x,xs@_*) => x + sum(xs)
     | }
<console>:10: error: type mismatch;
 found   : Seq[Int]
 required: List[Int]
           case List(x,xs@_*) => x + sum(xs)

scala> def sum(list: List[Int]): Int = list match {
     |     case List() => 0
     |     case List(x,xs@_*) => x + sum(xs.toList)
     | }
sum: (list: List[Int])Int

scala> sum(List(1,2,3,4))
res6: Int = 10
Tuple(タプル)
List((1, "a"), (2, "b"), (3, "c"), (4, "d")) foreach {
    _ match {
        case (n, s) if n == 1 => println(n + " " + s)
        case (2, s) => println(2 + " " + s)
        case (n, s@"c") => println(n + " " + s)
        case _ =>
    }
}

タプルとは特に関係ないけれど、@は、パターンマッチの条件を指定しつつ変数を割り当てたいときに使うようだ。

1 a
2 b
3 c
正規表現
scala> val p = "([A-Z])".r
p: scala.util.matching.Regex = ([A-Z])

scala> "abCdEf".toList.map { e => e.toString }.map { e => e match {
     |     case p(c) => "_" + c
     |     case _ => e.toUpperCase
     | }} mkString
res68: String = AB_CD_EF

caseのところで、"([A-Z])".rを定義するとエラーになってしまって、だめだった。

Scalaのcaseは、型で場合分けができる。

Javaでも、型での場合分け、instanceofのをつかっての場合分けは、妙に苦しさを感じながらたまにやるな。

case class SomethingDto(val n:Int, val s:String)

List(1, "b", new SomethingDto(3, "c"), (4, "d")) foreach {
    _ match {
        case n: Int  => println(n)
        case s: String  => println(s)
        case o: SomethingDto => println(o)
        case t: Tuple2[Int, String] => println(t)
        case _ =>
    }
}
1
b
SomethingDto(3,c)
(4,d)

Extractor 抽出子

Scalaのパターンマッチ、"match-case"のcaseの裏では、unapplyメソッドが実行される。

複雑なパターンマッチをunapplyメソッドとして、自分で定義しておくことができる。こういうのをextractor 抽出子という。

A Tour of Scala: Extractor Objects | The Scala Programming Language