Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
コップ本 15章 ケースクラスとパターンマッチ

15.1 ケースクラスとパターンマッチ

今回扱う例:算術式を扱うサンプル Expr (Expression)

abstract classの中身を実装していく 変数、数値、単項演算(-, ++, !, ...)、二項演算(+, - , <, &&)

abstract class Expr
case class Var(name:String) extends Expr
case class Number(num:Double) extends Expr
case class UnOp(opearator:String, arg:Expr) extends Expr
case class BinOp(opearator:String, left:Expr, right:Expr) extends Expr

caseという修飾子をつけたクラス:ケースクラス

  • 特徴1 同じ名前のファクトリーメソッドを追加。 newが不要。

    scala> val v = Var("x")
    v: Var = Var(x)

    scala> val op = BinOp("+", Number(1), v)
    op: BinOp = BinOp(+,Number(1.0),Var(x))

  • 特徴2 ケースクラスのパラーメータにvalプレフィックスを追加

    scala> v.name
    res0: String = x

    scala> op.left
    res1: Expr = Number(1.0)

nameとかleftがvalになっている

  • 特徴3 toString, hashCode, equalsの「自然な」実装を追加

    scala> println(op)
    BinOp(+,Number(1.0),Var(x))

toStringされてる

scala> op.right == Var("x")
res5: Boolean = true

scalaの==はequalsが呼ばれる。ケースクラスの要素は構造的な関係も含めて比較される。

名前付きパラメータを使って変更してコピーできる。指定しなかった値はそのまま。

scala> op.copy(opearator = "-")
res5: BinOp = BinOp(-,Number(1.0),Var(x))

ここまで述べたように、ケースクラスを定義すると色々やってくれる。最大のメリットはパターンマッチ。

15.1.2 パターンマッチ

算術式を単純にしたい、という想定。

  • UnOp("-", UnOp("-", e)) => e // 負の負は正。例:-(-1)を1にする

  • BinOp("+", e , Number(0)) => e // 0を加算したら元のまま

  • BinOp("*", e , Number(1)) => e // 1の乗算は元のまま

    def sinmlifyTop(expr: Expr):Expr = expr match {
    case UnOp("-", UnOp("-", e)) => e
    case BinOp("+", e , Number(0)) => e
    case BinOp("*", e , Number(1)) => e
    case _ => expr
    }

    scala> sinmlifyTop( UnOp("-", UnOp("-", Var("x"))) )
    res7: Expr = Var(x)

match文は、 <セレクター式> match { <選択肢> } の形式。case文は、パターンがマッチすると、=>の右辺の式が評価される

  • 定数パターン("-"など)は、==で比較して等しい値にマッチ
  • 変数パターン(上の例ならe)は、全ての値にマッチし、右辺から参照される
  • ワイルドカードパターン( _ アンダーバー)も全ての値にマッチする。変数名を導入する必要が無い。上の例ならデフォルトパターンに使われている。
  • コンストラクターパターン( UnOp("-", e) )は、コンストラクタ自体でマッチングを行う。この例なら、第一引数が"-"、第二引数はすべての値にマッチ。

javaのswitchの場合

switch (式) {
	case 定数1:
	実行内容A
	break;
case 定数2:
	実行内容B
	break;
case 定数3:
	実行内容C
	break;
default:
	実行内容D
	break;
}
  • matchは式なので、結果値を返す。

  • 1つにマッチしたら次の選択肢に行かない

  • マッチするものが無ければMatchErrorの例外が投げられる

    def sample1(expr: Expr) = expr match {
    case BinOp(op, left, right) => println(expr + "is a binary op")
    case _ =>
    }

    scala> sample1( BinOp("+", Number(1) , Number(0)) )
    BinOp(+,Number(1.0),Number(0.0))is a binary op
    scala> sample1( Number(0) )

どちらもUnit型が返される

15.2 パターンの種類

さきほどまで見てきたパターンを1つずつ見て行く。

15.2.1 ワイルドカードパターン

あらゆるオブジェクトにマッチする

def sample1(expr: Expr) = expr match {
	case BinOp(op, left, right) => println(expr + "is a binary op")
	case _ => 
}

def sample2(expr: Expr) = expr match {
	case BinOp(_, _, _) => println(expr + "is a binary op")
	case _ => println("It's something else")
}

補足。ワイルドカードにマッチした値は右辺では使えない。以下はエラーになる。

def sample3(x:Any) = x match {
	case _ => println("x is" + _)
}

<console>:8: error: missing parameter type for expanded function ((x$1) => "x is".<$plus: error>(x$1))
   case _ => println("x is" + _)

なお、AnyとはScalaの全ての値クラスの親クラス(コップ本p.207参照)

15.2.2 定数パターン

指定した定数とだけマッチする。Nilなら空リストにだけマッチする。

def describe(x:Any) = x match {
	case 5 => "five"
	case true => "truth"
	case "hello" => "hi!!"
	case Nil => "the empty list"
	case _ => "something else"
}

scala> describe(5)
res12: String = five

15.2.3 変数パターン

任意のオブジェクトにマッチする。 マッチした変数を使う場合に使用。

def sample4(expr: Expr) = expr match {
	case 0 => "zero"
	case somethingElse => "not zero:" + somethingElse
}

15.2.3.1 変数か定数か

scalaでPiなど定数が定義されているが、その定数と、変数パターンの変数はどう区別されるか。

scala> import math.{E, Pi}
scala> E match {
	case Pi => "strange match? Pi = " + Pi
	case _ => "OK"
}
res13: String = OK

予想通り、EはPiにマッチしない。 先頭が小文字ならパターン変数、そうでないものは定数とみなされる。

scala> val pi = math.Pi
scala> E match {
	case pi => "strange match? Pi = " + Pi
	case _ => "OK"
}
res14: String = strange match? Pi = 3.141592653589793

<console>:11: warning: unreachable code due to variable pattern 'pi' on line 10
          case _ => "OK"

piという文字列が変数パターンとして全部マッチするので、その後のコードが実行されない、という警告が出る。

先頭が小文字で定数として使う場合

    1. 何らかのオブジェクトのフィールド。例 obj.pi
    1. バッククォートで囲む。 例 pi

15.2.4 コンストラクターパターン

例 BinOp("+", e, Number(0))

  • 名前BinOpと、パターン("+", e, Number(0))から構成される。

  • まず、渡されたオブジェクトが、BinOpのメンバーか調べる

  • 次に、パターン("+", e, Number(0))がさらにパターンを提供しているか調べる

  • トップレベルオブジェクトだけがチェックされるのではなく、入れ子のパターンでもマッチする。

  • scalaは深いパターンマッチをサポートする。

    expr match {
    case BinOp("+", e , Number(0)) => println("a deep match")
    case _ =>
    }

  • BinOp("+", e, Number(0))は、BinOpかどうかのチェックが行われた後、Numberで値が0かもチェックされる。

15.2.5 シーケンスパターン

ArrayやListに対してのマッチ

scala> expr match {
	case List(0, _, _) => println("found it")
	case _ => 
}

Listの長さを指定しない場合 先頭が0で後は任意個(0個を含む)にマッチ

scala> expr match {
	case List(0, _*) => println("found it")
	case _ => 
}

15.2.6 タプルパターン

タプルは異なる型の要素を持てる。

def tupleDemo(expr: Any) = expr match {
	case (a, b, c) => println("matchd" + a + b + c)
	case _ => 
}
scala> tupleDemo( ("a", 3 , "b") )
matchda3b

15.2.7 型付きパターン

case文に型を書ける

def generalSize(x: Any) = x match {
	case s:String => s.length
	case m:Map[_,_] => m.size
	case _ => -1
}

m:Map[,]は型付きパターンの表記。キーと値からなるMap。

scala> generalSize("abc")
res16: Int = 3

scala> generalSize( Map(1->'a', 2->'b') )
res17: Int = 2

scala> generalSize( 123 )
res18: Int = -1

xはAnyだが、sはStringになっている。 Any型はlengthを持っていないが、キャストされた事により、s.lengthが使える。

この型付きパターンと同じ効果は、型テストをしてから、型キャストすれば得られる。 型テストは expr.isInstanceOf[String]
型キャストは expr.asInstanceOf[String]

さっきのcase文は、こう書ける

if(x.isInstanceOf[String]){
	val s = x.asInstanceOf[String]
	s.length
}else ......

15.2.7.1 型消去

さきほどm:Map[,]と表記したが、Map[Int,Int]のように型指定は可能か?

def isIntIntMap(x: Any) = x match {
	case m:Map[Int,Int] => true
	case _ => false
}

<console>:9: warning: non-variable type argument Int in type pattern Map[Int,Int] is unchecked since it is eliminated by erasure
   case m:Map[Int,Int] => true
          ^
isIntIntMap: (x: Any)Boolean

消去モデルと言って、実行時に型引数の情報を管理しない。そのため、Mapオブジェクトが2個のIntから作られているのか、他の型なのか確かめられない。 以下のように、Int Intでなくてもマッチしてしまっている。

scala> isIntIntMap( Map(1->1) )
res19: Boolean = true

scala> isIntIntMap( Map("aa"->"bbb") )
res20: Boolean = true

ただし、配列の要素型はパターンマッチに使える。

def isStringArray(x: Any) = x match {
	case a:Array[String] => "yes"
	case _ => "no"
}

scala> isStringArray( Array("abc") )
res21: String = yes

scala> isStringArray( Array(1,3,5) )
res22: String = no

15.2.8 変数の束縛

変数パターンでは"a"や1などを取り上げたが、ここではパターンそのものを変数に格納させられる

def sample6(expr:Expr) = expr match{
	case UnOp("abs", e @ UnOp("abs", _)) => e
	case _ =>
}

scala> sample6( UnOp("abs", UnOp("abs", Number(123))) )
res24: Any = UnOp(abs,Number(123.0))

15.3 パターンガード

例:e+eをe*2に置き換えたい

def simplifyAdd(e:Expr) = e match {
	case BinOp("+", x ,x) => BinOp("*", x, Number(2))
	case _ => e
}

<console>:13: error: x is already defined as value x
   case BinOp("+", x ,x) => BinOp("*", x, Number(2))

エラーになる。パターン変数は、パターンの中で1度しか登場させられない。パターンガードにより書き換える。

def simplifyAdd(e:Expr) = e match {
	case BinOp("+", x ,y) if x==y => BinOp("*", x, Number(2))
	case _ => e
}

パターンの後ろにifをつけ、論理式を書く。これがtrueと評価された場合のみ、マッチが成功する。

//正整数にのみマッチ
case n: Int if 0 < n => ...
//先頭の文字が'a'の文字列にのみマッチ
case s:String if s(0) == 'a' => ...

15.4 パターンのオーバーラップ

パターンは書かれた順序で評価される。

def simplifyAll(expr:Expr):Expr = expr match {
	case UnOp("-", UnOp("-", e)) => simplifyAll(e)
	case BinOp("+", e, Number(0)) => simplifyAll(e)
	case BinOp("*", e, Number(1)) => simplifyAll(e)
	case UnOp(op, e) => UnOp(op, simplifyAll(e))	//全ての単項演算子にマッチ
	case BinOp(op, l, r) => BinOp(op, simplifyAll(l), simplifyAll(r))	//全ての二項演算子にマッチ
	case _ => expr
}

包括的なケースを後ろに配意する。包括的なケースが前に配置すると、コンパイラが警告を出す。

def simplifyAll(expr:Expr):Expr = expr match {
	case UnOp(op, e) => UnOp(op, simplifyAll(e))	//全ての単項演算子にマッチ
	case UnOp("-", UnOp("-", e)) => e
}

<console>:12: warning: unreachable code
   case UnOp("-", UnOp("-", e)) => e

15.5 シールドクラス

デフォルトケースがつけられない場合、もれなくケースを書きたい。 今のExprクラスには4つのケースクラスが定義されているが、第五のケースクラスが追加されてしまうかもしれない。 ケースクラスのスーパークラスをシールドクラスにすることで、サブクラスを追加できなくする。

sealed abstract class Expr
case class Var(name:String) extends Expr
case class Number(num:Double) extends Expr
case class UnOp(opearator:String, arg:Expr) extends Expr
case class BinOp(opearator:String, left:Expr, right:Expr) extends Expr

これに対して漏れのあるパターンマッチを定義してみる。

def describe(e:Expr):String = e match {
	case Number(_) => "a number"
	case Var(_) => "a variable"
}

<console>:12: warning: match may not be exhaustive.
It would fail on the following inputs: BinOp(_, _, _), UnOp(_, _)
	   def describe(e:Expr):String = e match {
    	                             ^

マッチに漏れがある事を警告してくれる。

文脈上NumberかVer以外を想定しなくて良い場合はどうするか。
括的なケースを追加する場合。

def describe(e:Expr):String = e match {
	case Number(_) => "a number"
	case Var(_) => "a variable"
	case _ => throw new RuntimeException
}

アノテーションを追加する場合。

def describe(e:Expr):String = (e: @unchecked) match {
	case Number(_) => "a number"
	case Var(_) => "a variable"
}

15.6 Option型

余談:null参照の考案は10億ドル単位の過ち? | スラッシュドット・ジャパン デベロッパー

Option型には、実際の値を持っているSome(x)と、値が無い事を表すNoneがある。

scala> val capitals = Map("France"->"Paris", "Japan"->"Tokyo")
capitals: scala.collection.immutable.Map[String,String] = Map(France -> Paris, Japan -> Tokyo)

scala> capitals get "France"
res0: Option[String] = Some(Paris)

scala> capitals get "North Pole"
res1: Option[String] = None

パターンマッチと組み合わせる。

scala> def show(x:Option[String]) = x match {
	case Some(s) => s
	case None => "?"
}
show: (x: Option[String])String

scala> show(capitals get "Japan")
res2: String = Tokyo

scala> show(capitals get "North Pole")
res3: String = ?
  • Javaの場合、nullチェックを忘れるとNullPointExceptionが発生する。
  • Option型により、nullになる可能性がある事を明示する。
  • Option[String]型とString型がコンパイラで区別され、チェックミスを防げる。

15.7 どこでもパターンを

マッチ式以外でもパターンが使える。

15.7.1 変数定義におけるパターン

タプルを分解して個々の要素を別々の変数に代入できる

scala> val myTuple = (123, "abc")
myTuple: (Int, String) = (123,abc)

scala> val (number, string) = myTuple
number: Int = 123
string: String = abc

ケースクラスの場合

scala> val exp = new BinOp("*", Number(5), Number(1))
exp: BinOp = BinOp(*,Number(5.0),Number(1.0))

scala> val BinOp(op, left, right) = exp
op: String = *
left: Expr = Number(5.0)
right: Expr = Number(1.0)

15.7.2 部分関数としてのケースクラス

case文は、関数リテラル(関数を式として記述したもの)が使える場所なら使える。

val withDefault:Option[Int] => Int = {
	case Some(x) => x
	case None => 0
}
withDefault: Option[Int] => Int = <function1>

scala> withDefault(Some(10))
res8: Int = 10

scala> withDefault(None)
res9: Int = 0

例:アクターライブラリ
マルチスレッド処理を行うためのもの。アクター間でメッセージをやりとりする。 reactメソッドではメッセージを受信する。

react {	//メッセージを受信するメソッド
	//ここがパターンマッチ
	case(name:String, actor:Actor) => {
		actor ! getip(name)	//受け取ったnameを何か処理してメッセージ送信
		act()
	}
	case msg => {
		println("Unhandled message: " + msg)
		act()
	}
}

例:部分関数

val second: List[Int] => Int = {
	case x :: y :: _ => y
}

<console>:7: warning: match may not be exhaustive.
It would fail on the following input: List(_)
	   val second: List[Int] => Int = {
                                      ^

//補足 ::は、リストの先頭に要素を加える
scala> 9 :: List(1, 2, 3)
res3: List[Int] = List(9, 1, 2, 3)

マッチが網羅的でない、という警告が出る。この関数は要素が2個以上のリストを渡さないとエラーになる。

scala> second(List(1))
scala.MatchError:

scala> second(List(1,2,3))
res18: Int = 2

部分関数にすると、一部の引数にしか対応していなくても警告は出ない。

scala> val second: PartialFunction[List[Int], Int]  = {
	case x :: y :: _ => y
}
second: PartialFunction[List[Int],Int] = <function1>

部分関数はisDefinedAtメソッドを持つ。これを使うと、特定の値に対して関数が定義されているか調べられる。

scala> second.isDefinedAt(List(5,6,7))
res19: Boolean = true

scala> second.isDefinedAt(List())
res20: Boolean = false

なお、先程の部分関数は、コンパイラが以下のように変換されている。

new PartialFunction[List[Int], Int] {
	def apply(xs:List[Int]) = xs match {
		case x :: y :: _ => y
	}
	def isDefinedAt(xs: List[Int]) = xs match {
		case x :: y :: _ => true
		case _ => false
	}
}
  • 部分関数はランタイムエラーになる可能性がある
  • 一般には全関数を使ったほうがいい。(全関数よくわからない。部分関数でないもの?)
  • 処理できない値が渡されない事がわかっている場合や、isDefinedAtで必ずチェックするような環境(フレームワークなど)なら、部分関数を使っても良い。

15.7.3 for式内のパターン

for文の中でパターンを使える。

val capitals = Map("France"->"Paris", "Japan"->"Tokyo")
for ((country, city) <- capitals){
	println("The capital of " + country + " is " + city)
}
The capital of France is Paris
The capital of Japan is Tokyo

パターンにマッチしなかった値は捨てられる。

scala> val results = List(Some("apple"), None, Some("orange"))
results: List[Option[String]] = List(Some(apple), None, Some(orange))

scala> for (Some(fruit) <- results) println(fruit)
apple
orange

15.8 より大きなサンプル

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment