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


Scalaコップ本の24章をやっていきますよ、24章は抽出子についてですね。どうやら抽出子とやらはパターンマッチに関わるものらしいのですが、詳しい内容は実際に見ていきますかねー

サンプル:メールアドレスの抽出

パターンマッチを便利に使うっぽい抽出子ですが、まずは抽出子とはなんぞや?ということを見ていきたいと思います(´・ω・`)とりあえずメールアドレスの抽出というテーマでサンプルを追っていきますよ

与えられた文字列をメールアドレスか判定して、メールアドレスであればユーザ部とドメイン部に分解するという操作をサンプルとして想定してみますが(´・ω・`)とりあえず安易に実装するとすればこんな感じですかね

// メールアドレスの形式かどうかを判定する式(処理は省略)
def isEMail(s:String):Boolean
// メールアドレスからドメイン部を抜き出す処理
def domain(s:String):String
// メールアドレスからユーザ部を抜き出す処理
def user(s:String):Srting

// 上記処理を組み合わせてメールアドレスの解析を行う
if(isEMail(s)) println(user(s) + " あっとまーく " domain(s))
else println("e-mailっぽくないけども") 

各関数の定義はともかく実際の抽出処理が複数の手順で実行されるので、複数を組み合わせたマッチングをしようとすると複雑になりそうな予感がしマス(´・ω・`)Scala的にはEMail(user, domain)のようなパターンを利用して、これくらいシンプルにパターンマッチするのが理想ぽいみたいです。

string match {
  case EMail(user, domain) => println(user + " あっとまーく " + domain)
  case _ => println("e-mailっぽくないけども") 
}

上記のようなパターンマッチを利用することで、例えばメールアドレスリスト内で、ユーザ部が2つの連続した電子メールアドレスを見つける、とかいった複雑な処理もサラッと書けるですね

list match {
  case EMail(u1, d1) :: EMail(u2, d2) :: _ if(u1 == u2) => println("条件OK")
  case _ => println("条件NG")
}

ちなみに前述の複数処理を組み合わせると…めんどくさくて考えたくもないですナ(´・ω・`)そもそもパターンマッチ使わないと文字を一つ一つ切り出すはめになりそう(´;ω;`)

さて、そんなパターンマッチですが、目下の問題はEMail(user, domain)なんていうパターン表現が存在しないことだったりします(´・ω・`)そこで抽出子を使って新しいパターンを定義してやるゼィ(`・ω・´)というのがこの章の内容です。

抽出子

Scalaでの抽出子はunapplyという値をマッチングして分解するメンバメソッドをもつオブジェクトで表現されるみたいです。またオプション的に分解した値から大元の値を構成しなおすapplyメソッドを持ってたりするみたいですが、こいつは必須ではないみたいです。

とりあえず前述のEmail(user, domain)を抽出しを使って実装してみますよ。ひとまずEmaiの条件は「文字列@文字列」となっているモノと簡易的に定義してやります。

object EMail {
  // 再構成用メソッドです
  def apply(user:String, domain:String) = user + "@" + domain
  // 実際のパターンマッチ処理です
  def unapply(str:String):Option[(String, String)]  = {
    // @前後で文字を分解
    val parts = str split "@"
    // 結果的に2つの文字列に分解できた場合はマッチングOKとして
    // 各文字列を返す
    if(parts.length == 2) Some(parts(0), parts(1)) else None
  }
}

実際のパターンマッチ構文内でEMailオブジェクトが呼び出されるとunapplyメソッドが実行されるみたいです。unapplyメソッドはパターンにマッチする場合は各変数を束縛したものを返して、マッチしない場合はNoneを返すみたいですね。

とりあえず実行してみますかね

// マッチするパターンです
scala> "hoge@hoge.com" match {
     |   case EMail(user, domain) => println(user + " あっとまーく " + domain)
     |   case _ => println("e-mailっぽくないけども") 
     | }
hoge あっとまーく hoge.com

// マッチしないのもやってみます
scala> "hoge-at-hoge.com" match {
     |   case EMail(user, domain) => println(user + " あっとまーく " + domain)
     |   case _ => println("e-mailっぽくないけども") 
     | }
e-mailっぽくないけども

// せっかくなので前述の複雑パターンもやってみるテスト
scala> List("hoge@hoge.com", "hoge@huga.com")  match {
     |   case EMail(u1, d1) :: EMail(u2, d2) :: _ if(u1 == u2) => println("条件OK")
     |   case _ => println("条件NG")
     | }
条件OK

とりあえず出来ましたな(`・ω・´)また抽出子が実際にどんな型のパラメータを取るかを定義するには下記のようにEmail抽出しにScalaの関数型を継承すればいいみたいです

object EMail extends (String, String) => String { // 実際の処理 } 

なお抽出子を使った実際のパターン処理では、パターンマッチに与えられた値が要求する型であるかどうか(EMailならStringかどうか)をチェックして当てはまる場合はキャストした後にパターンマッチが行われるみたいです。

注入と抽出の相補性

抽出子を表現するオブジェクトではapplyメソッドを注入、unapplyメソッドを抽出と呼ぶみたいです(´・ω・`)それぞれは一つのオブジェクトの中でセットで扱われることが多いみたいです。ただしapplyメソッドは必須ではないのでこのオブジェクトを抽出子と呼ぶのだとか(´・ω・`)

また、注入・抽出メソッドは相補的手である必要があるために次のような計算が成り立つ必要があるとのことデス

// このコードの処理結果は
EMail.unapply(EMail.aply(user, domain))

// こうなる必要があるとのことです
EMail(user, domain)

// また上記とは玉方向の展開ではあるものの
// こんなコードが成り立つはずだとか
Email.unapply(obj) match {
  case Some(user, domain) => EMail.apply(user, domain)
}

Scalaの言語上ではこのようにapplyメソッドとunapplyメソッドの相補性が成り立つ必要性は要求されないみたいなのですが、コップ本的には抽出子設計の原則としてこれらの相補性を確保したほうがよろしいよ、とのことです。

変数が1個以下のパターン

それでは色々な抽出しのパターンを見ていきますよ(`・ω・´)まずは変数が1個だけのパターンです。とりあえずザザっといろんなパターンを見ていきますよ

まずは文字列が同じ単語の繰り返しになっているかどうかを判定する抽出子デス

object Twice {
  def apply(s:String):String = s + s
  // 文字列を半分に分解して、分解した2つの文字列が同じかどうか判定します
  def unapply(s:String):Option[String] = {
    val length = s.length / 2
    val half = s.substring(0, length)
    if(half == s.substring(length)) Some(half) else None
  }
}

// 実際に動かしてみますよ
scala> "hogehoge" match {
     |   case Twice(s) => println( s + " × 2")
     |   case _ => println("none")
     | }
hoge × 2

抽出子は変数の束縛をせずにTrue or Falseを返すだけのパターンも作れるみたいです。例えば次のようにUpperCaseかどうかの判定をする抽出子はそのパターンになりますね

// 実際に分解した値を作るわけではないので
// 構築用のapplyは定義しません
object UpperCase {
  // UpperCaseを適用したものと、もともとのパラメータが同じかどうかを比較します
  def unapply(s:String):Boolean = s.toUpperCase == s 
}

// 実際に動かしてみますよ
scala> "HOGE" match {
     |   case UpperCase() => println("upper")
     |   case _ => println("lower")
     | }
upper


これまで定義した全抽出子を組み合わせたゴチャ混ぜパターンマッチをやってみますよ(`・ω・´)対象となるのはユーザ名が繰り返しで、かつ大文字で表記されているメールアドレスデス

def userTwiceUpper(s:String) = s match {
  // これまででてきた抽出子を組み合わせます
  case EMail(Twice(x @ UpperCase()), domain) =>
    "match: " + x + " in domain " + domain
  case _ =>
    "no match"
}

//// 実際に試してみますよ
// 小文字はダメです
scala> userTwiceUpper("hogehoge@hugahuga.com")
res31: java.lang.String = no match

// 繰り返してないのもダメです
scala> userTwiceUpper("HOGEHUGA@hugahuga.com")    
res32: java.lang.String = no match

// 大文字繰り返しならOKデス(`・ω・´)
scala> userTwiceUpper("HOGEHOGE@hugahuga.com")
res33: java.lang.String = match: HOGE in domain hugahuga.com

抽出子を組み合わせることで複雑なパターンマッチもあっさり書けるわけですな(`・ω・´)

いじょー

時間切れで以上です(´・ω・`)次回は可変個の引数を取る抽出子からやりますデス