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


Scalaコップ本の15章に入って行きますよー、15章は個人的にScala的特徴の一つと思っているケースクラスとパターンマッチについてですねー

とりあえず今回はケースクラスについてやりたいと思いますよー

単純なサンプル

パターンマッチの動きを具体的に見ていくためのサンプルライブラリーを最初に定義してしまいますね。内容的には変数、数値、単行、二項演算から構成される算術式を入力データとするDSLの一種を想定するそうです。

DSL(ドメイン固有言語)…ってそういやid:hrsthの発表で聞いたsinatoraみたいなやつか...とりあえずwikipedia的にはこんなかんじですね→wikipedia:ドメイン固有言語

特定のタスクに特化した言語って解釈でいいのかしら…とりあえず手を動かしたほうが実感しやすそうなのでサンプルクラスを定義してみますかね

// 抽象基底クラス
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

ライブラリの各クラスはケースクラスとして定義されておりますねー

ケースクラス

ケースクラスとしてクラス定義を行うとScalaコンパイラーは該当クラスを下記のように補強してくれるそうです。

  • そのクラスと同名のファクトリーメソッドを追加
  • ケースクラスのパラメーターリスト内の全てのパラメーターにvalプレフィックスを追加
  • クラスにtoString, hashCode, equalsの各メソッドを自然に実装
ケースクラスの特徴1:ファクトリーメソッド

んじゃ、実際に試してみましょうかねー、まずはファクトリーメソッドの追加です

// ファクトリーメソッドが追加されたのでnewキーワードを省略しましたよ
scala> val v = Var("x")
v: Var = Var(x)

ファクトリーメソッドを使用することでnewキーワードを省略できるので、入れ子構造の式でも構造が見やすくなりますねー

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

ファクトリーメソッドを使わないとこんな感じですかね

// (´ε`;)ウーン…もっと階層深くなると嫌になりそうだ
 val op = new BinOp("+", new Number(1), v)
ケースクラスの特徴2:パラメーターのval化

Scalaコンパイラーはケースクラスのパラメーターをval化(フィールド化)してくれるので、ケースクラスのパラメーターは外部からのアクセスが可能になるのですね

// Varオブジェクトのnameパラメーターにアクセスしますよ
scala> val v = Var("x")                                                      
v: Var = Var(x)

scala> v.name
res0: String = x

// BinOpオブジェクトのleftパラメーターにアクセスしますよ
scala>  val op = new BinOp("+", new Number(1), v)
op: BinOp = BinOp(+,Number(1.0),Var(x))

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

パラメーターでval定義すればいいんだろうけど、ちょっと一手間が省略できるのはとてもありがたいですなー

ケースクラスの特徴3:便利メソッドの追加

ケースクラスではtoString、hashCode、equalsメソッドを自然な形で追加してくれるとのことデス

試しにやってみますかねー

// インスタンス生成しますよ
scala> val v = Var("x")
v: Var = Var(x)
scala>  val op = new BinOp("+", new Number(1), v)
op: BinOp = BinOp(+,Number(1.0),Var(x))

// toStringデス
scala> v.toString
res2: String = Var(x)
// hashCodeデス
scala> v.hashCode
res3: Int = 1158350037
// equals(== はequalsを呼び出しマス)デス
scala> op.right == v
res4: Boolean = true

うn、きっちり追加されておりますな。

ケースクラスまとめ

case修飾子を書くだけでここまでサポートされるわけですな(`・ω・´)ちょっとした一手間を減らしてくれるケースクラスはありかもデス

ケースクラスはパターンマッチが使えるよ!

さて上の方ではケースクラスの特徴を羅列しましたが、上記3つの特徴以上に強力なものとしてケースクラスではパターンマッチをサポートするのですね(`・ω・´)

詳しくはあとからやるとして、ざらっとパターンマッチを眺めてみましょうか。Scalaのパターンマッチはこんな感じで書くみたいですねー

// パターンマッチを使った関数定義デス
def hoge(i:Int):String = i match {
  // 1が渡ってきた時の処理
  case 1 => "1です"
  // デフォルト値の設定ですなー
  case _ => "ソレ以外の何かです"
}

// 実行してみますよ
scala> hoge(1)
res5: String = 1です

scala> hoge(3)
res7: String = ソレ以外の何かです

Scalaのパターンマッチはセレクターをmatchキーワードの前に各形式ですねー、JavaのSwitch文なんかと同様に上から順に評価されますが、一致したところ以降の式は評価されないみたいデス。

ちなみに上のように低数値による完全一致の場合を定数パターンと呼ぶみたいですね

変数を使ってパターンマッチをするよ!

またパターンマッチでは評価に使った変数をうまい具合に加工することもできますね

// 振分していないので、あんまり良くない例ですが(´・ω・`)
def moge(s:String):String = s match {
  // セレクターのsをnameという変数で使用します
  // 結果に反映することもできますねー
  case name => "Hello " + name
}

// 実行結果です
scala> moge("hogehoge")                     
res9: String = Hello hogehoge

こいつを変数パターンのパターンマッチと呼ぶようです

コンストラクタ的パターンマッチ

実際Scalaのパターンマッチはもっと複雑なことが出来るよ!って例を前述のDSLライブラリもどきを使ったサンプルで表現してみますねー

// 算術式をパラメーターとして渡すパターンマッチですよー
def simplifyTop(expr:Expr):Expr = expr match {
  // 負の数の負を求めるよ!という表現の算術式がきた場合
  // (数値は変わりませんけども)
  case UnOp("-", UnOp("-", e)) => e
  // 0を加えるよ!という表現の算術式がきた場合
  // (数値は変わりませんけども)
  case BinOp("+", e, Number(0)) => e
  // 1を掛けてるよ!という表現の算術式がきた場合
  // (数値は変わりませんけども)
  case BinOp("*", e, Number(1)) => e
  
  // 上記以外の計算はできないのでそのまま返します
  case _ => expr
}

//// 実行しますよー
// 負の負を計算
scala> simplifyTop(UnOp("-", UnOp("-", Var("5"))))
res14: Expr = Var(5)
// 0を足しマス
scala> simplifyTop(BinOp("+", Var("5"), Number(0)))
res16: Expr = Var(5)
// 1をかけマス
scala> simplifyTop(BinOp("*", Var("5"), Number(1)))
res17: Expr = Var(5)

// パターンマッチ外のものを入力しますよー
// 1を加える演算は対象外なのでそのまま返します
scala> simplifyTop(BinOp("+", Var("5"), Number(1)))
res18: Expr = BinOp(+,Var(5),Number(1.0))

上記の式はそれぞれUnOpやBinOpのコンストラクターにパラメーターを渡した状態のものをベースにパターンマッチをしておりますねー。小難しいことを写経するとソレゾレのパラメーター自身も変数パターンや定数パターンになっているらしいですな。

これをコンストラクターパターンと呼ぶそうです

matchとswitchの違い

match式はJavaのswitchの一般化だと思われるらしいんですが3点ほどの違いがあるのであげときますねー

  • matchはScalaの式なので必ず結果を返す(Unitも結果なのデス)
  • 各条件は上から順に評価されるが、該当するものがあった場合それ以降は評価されない
  • 評価式は全てのパターンを網羅しなければならない
    • マッチするパターンがないとMatchErrorの例外が投げられます

こんな感じですねー、試しにMatchError発生でもやってみましょうかねー

def hige(i:Int):String = i match {
  case 1 => "ヒゲが1本生えてます"
}

// マッチすれば値を返すけども
scala> hige(1)
res19: String = ヒゲが1本生えてます

// マッチするものがなければ例外を投げますよー
scala> hige(2)
scala.MatchError: 2
	at .hige(<console>:4)
	at .<init>(<console>:6)
	at .<clinit>(<console>)
	at RequestResult$.<init>(<console>:3)
	at RequestResult$.<clinit>(<console>)
	at RequestResult$result(<console>)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
	at sun.reflect.DelegatingMethodAc...

とりあえずデフォルトケースを追加しとけ!って感じですかね

以上ー

今回はケースクラスとパターンマッチの触りをやったので、次回はパターンマッチの詳しい部分にはいっていきますよー