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


夏休み進行でしばらく間があいてしまいましたが、Scalaコップ本の15章の続きをやっていきますよー。15章はパターンマッチ関連ですねー

ひとまずサンプル用の算術式ライブラリをあげときますね

// 抽象基底クラス
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のパターンマッチではパターンを線形なものに制限しているらしく、パターン変数はパターンの中で1度しか登場させることができないらしです…線形って表現がイマイチピンと来ないですが、パターン変数は一回切りってとこだけ覚えておきますかね(´・ω・`)

まずは失敗パターンをサンプルを使って書いてみますよ、内容的には"x + x"の演算を"x * 2"に切り返すような式ですね。

def guardPattern(e:Expr) = e match {
  // 同じ要素の和は、2倍演算に切り替えますよ
  case BinOp("+", x ,x) => BinOp("*", x, Number(2))
  // 当てはまらない場合はそのまま返します
  case _ => e
}

//// 実行結果ですよー
// xは使っているからもう使えませんYo!と怒られました
<console>:11: error: x is already defined as value x
         case BinOp("+", x ,x) => BinOp("*", x, Number(2))
                            ^
パターンガードで回避するぜ(`・ω・´)

パターンガードとやらを使用するとパターンに対して条件文を指定できるみたいなので、こいつを使ってうまい具合に定義してみますよ。

上の失敗例をパターンガードを使って書き換えマス

def guardPattern(e:Expr) = e match {
  // パターンの後ろに条件式を記述してやりマス
  case BinOp("+", x ,y) if x == y => BinOp("*", x, Number(2))
  // 当てはまらない場合はそのまま返します
  case _ => e
}

//// 実行しマス
// 同じ要素の和を2との乗算に切り替えましたよ
scala> guardPattern(BinOp("+", Number(3), Number(3)))
res1: Expr = BinOp(*,Number(3.0),Number(2.0))

// 異なる要素の和の場合スルーしました
scala> guardPattern(BinOp("+", Number(3), Number(2)))
res2: Expr = BinOp(+,Number(3.0),Number(2.0))

うん、OKですね。条件式のパターンガードを適用することで結構複雑なパターンが使えるみたいですね

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

パターンは記述された順に実行されますよ!ってことなので、記述順は大事みたいです。基本は包括的な条件ほど下にすることですな。

例えば下のような再帰的な呼び出しの存在するパターンだと、1,2,3のような個別の単純化ルールを4,5のようなコンストラクタの要素パターンのような包括的ルールよりも上に持っていくのがいいみたいです。

def testAll(expr:Expr):Expr = expr match {
  // 1: 負の負は正にしますよ
  case UnOp("-", UnOp("-", e)) => testAll(e)
  // 2: 0を加えても変わりませんよ
  case BinOp("+", e, Number(0)) => testAll(e)
  // 3: 1をかけてもそのままです
  case BinOp("*", e, Number(1)) => testAll(e)
  // 4: 単項演算の要素にもパターンを適用させますよ
  case UnOp(op, e) => UnOp(op, testAll(e))
  // 5 :2項演算の要素にもパターンを適用させますよ
  case BinOp(op, l, r) => BinOp(op, testAll(l), testAll(r))
  // 6 :当てはまらなければ返しますよ
  case _ => expr
}

実行するとこんな感じです、実行順序としては2→1→6みたいな感じかしらね

scala> testAll(BinOp("+",  UnOp("-", UnOp("-", Number(1))), Number(0)))
res6: Expr = Number(1.0)

ちなみにこっちのパターンだと順序の関係か全部は実行されないみたい。5の内部演算が先に処理されるので適用パターンに鳴っているはずの3が実行されないです(´・ω・`)うん、順序難しい。

scala> testAll(BinOp("*", UnOp("-", UnOp("-", Number(1))), BinOp("+", Number(1), Number(0))))
res7: Expr = BinOp(*,Number(1.0),Number(1.0))

// もう一回やればOKデスけども
scala> testAll(BinOp("+",Number(1.0),Number(0.0)))
res9: Expr = Number(1.0)
コンパイラによるチェック

ちなみにパターンの順序性はコンパイラ様も確認してくれるらしく、ある程度の包含性順序チェックをやってくれるみたいです。

例えば上記サンプルの包含順序を入れ替えてやりますと怒られます

def testAll(expr:Expr):Expr = expr match {
  // 4: 単項演算の要素にもパターンを適用させますよ
  case UnOp(op, e) => UnOp(op, testAll(e))
  // 5 :2項演算の要素にもパターンを適用させますよ
  case BinOp(op, l, r) => BinOp(op, testAll(l), testAll(r))
  // 1: 負の負は正にしますよ
  case UnOp("-", UnOp("-", e)) => testAll(e)
  // 2: 0を加えても変わりませんよ
  case BinOp("+", e, Number(0)) => testAll(e)
  // 3: 1をかけてもそのままです
  case BinOp("*", e, Number(1)) => testAll(e)
  // 6 :当てはまらなければ返しますよ
  case _ => expr
}

//// 1,2,3には適用されないよ!エラーです
<console>:18: error: unreachable code
         case UnOp("-", UnOp("-", e)) => testAll(e)
                                         ^
<console>:20: error: unreachable code
         case BinOp("+", e, Number(0)) => testAll(e)
                                          ^
<console>:22: error: unreachable code
         case BinOp("*", e, Number(1)) => testAll(e)
                                          ^

そんなところまで見てもらえるとは、ありがたやありがたや(´・ω・`)

シールドクラス

スーパークラスをシールド化することでパターンマッチの漏れチェックをすることが出来るみたいです。

正確にはスーパークラスをシールド化することで、そのファイル以外にサブクラスを追加することができなくなるので「これ以上サブクラスで定義されるケースが増えないよ」宣言として使えるそうです。その宣言をうけてコンパイラ様がパターンの漏れチェックをおこないますよ...と、まあこんな感じのしくみになるみたいです。

まあシールド化は”勝手にサブクラス(ケース)が増えないからきちんと制御される安心出来るパターンマッチですよ”宣言のライセンス的なシロモノだと解釈できるみたいですねー

シールドクラス化はスーパークラスにsealedキーワードを付けるだけですね

// シールドクラス化しますよ
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(_) => "数字デス"
  case Var(_) => "変数デス"
  case UnOp(_, _) => "単項演算デス"
}

// コンパイラWarningです
// 二項演算子の処理が書かれていないよ!というWarningですな
// (仮に二項演算がきたらMatchError例外がなげられますな)
<console>:11: warning: match is not exhaustive!
missing combination          BinOp

       def describe(e:Expr):String = e match {
                                     ^
describe: (Expr)String

ちなみにシールドクラス適用前はWarningも出ないでスルーされます。

ちなみにコンパイラーを黙らせるにはデフォルトルール(包括的なルール)を追加するのも手ですが、下のように@uncheckedアノテーションを追加するのもありだそうです。

// @uncheckedアノテーションをつけるとWarningがでなくなりマス
def describe(e:Expr):String = (e: @unchecked) match {
  case Number(_) => "数字デス"
  case Var(_) => "変数デス"
  case UnOp(_, _) => "単項演算デス"
}

@uncheckedアノテーションはパターンマッチに対して徹底的なチェック(順序とかそのへん)を行わないよ!という指示になるそうです。

アノテーションについては25章でー

以上ー

とりあえず復習がてらにサラッとやりましたよー、次回はOption型あたりからです