今回扱う例:算術式を扱うサンプル 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 = xscala> 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))
ここまで述べたように、ケースクラスを定義すると色々やってくれる。最大のメリットはパターンマッチ。
算術式を単純にしたい、という想定。
-
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型が返される
さきほどまで見てきたパターンを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参照)
指定した定数とだけマッチする。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
任意のオブジェクトにマッチする。 マッチした変数を使う場合に使用。
def sample4(expr: Expr) = expr match {
case 0 => "zero"
case somethingElse => "not zero:" + somethingElse
}
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という文字列が変数パターンとして全部マッチするので、その後のコードが実行されない、という警告が出る。
先頭が小文字で定数として使う場合
-
- 何らかのオブジェクトのフィールド。例
obj.pi
- 何らかのオブジェクトのフィールド。例
-
- バッククォートで囲む。 例
pi
- バッククォートで囲む。 例
例 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かもチェックされる。
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 _ =>
}
タプルは異なる型の要素を持てる。
def tupleDemo(expr: Any) = expr match {
case (a, b, c) => println("matchd" + a + b + c)
case _ =>
}
scala> tupleDemo( ("a", 3 , "b") )
matchda3b
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 ......
さきほど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
変数パターンでは"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))
例: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' => ...
パターンは書かれた順序で評価される。
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
デフォルトケースがつけられない場合、もれなくケースを書きたい。 今の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"
}
余談: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型がコンパイラで区別され、チェックミスを防げる。
マッチ式以外でもパターンが使える。
タプルを分解して個々の要素を別々の変数に代入できる
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)
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で必ずチェックするような環境(フレームワークなど)なら、部分関数を使っても良い。
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
略