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


Scalaコップ本の24章の続きをやっていきますよー、24章は抽出子についての内容をやっておりますですよ(`・ω・´)

可変個の引数をとる抽出子

前回はメールアドレスデータの分割ということで決まった数の要素を返す抽出子を見てきたのですが、例えばドメイン名のように可変個の文字列を使ったパターンマッチを擦る必要がある場合もあるのです(´・ω・`)そんなときにも抽出子をつかいましょう、という節ですよ

とりあえず可変個の引数を取るようなドメイン名パターン抽出子はこんなかんじで使いたいところですね(´・ω・`)

string match {
  // 引数が2個のパターンに対応しますよ 
  case Domain("org", "acm") => println("acm.org")
  // 引数が3個のパターンにも対応しますよ
  case Domain("com", "sun", "java") => println("java.sun.com")
  // 可変長の引数パターンにも対応しますよ
  case Domain("net", _*) => println("a .net domain")
}

可変個の引数に対応した上記のようなドメイン名パターンマッチを実現するための抽出子はunapplySeqという可変個マッチ専用メソッドを利用して次のように定義されますね

object Domain {
  // 注入メソッドです。引数は可変長ですな
  def apply(parts:String*):String = 
    parts.reverse.mkString(".")
    
  // 抽出メソッドです、可変個マッチ用のunapplySeqで定義します。
  def unapplySeq(whole:String):Option[Seq[String]] = 
    // 文字列をピリオドで分割したものを逆順で返します
    // 区切り文字の"."はエスケープしますね(´・ω・`)
    Some(whole.split("\\.").reverse)
}

とりあえず上記を利用したパターンマッチ処理を定義してみますよ

def isDotCom(s:String):Boolean = s match {
  // .comドメインかどうかを判定します
  case Domain( "com", _* ) => true
  case _ => false
}

//// 実行してみますよー
scala> isDotCom("hoge.com")
res10: Boolean = true

scala> isDotCom("hoge.net")
res11: Boolean = false

せっかくなので前回定義したEmailアドレス判定用抽出子と組み合わせてみますかね。前回定義したのはこんな感じでした。

object EMail {
  def apply(user:String, domain:String) = user + "@" + domain
  def unapply(str:String):Option[(String, String)]  = {
    val parts = str split "@"
    if(parts.length == 2) Some(parts(0), parts(1)) else None
  }
}

それではEmail判定をしつつ、ドメインも判定するパターンマッチサンプルをやってみますよ

def isJackInHogeDotNet(s:String) = s match {
  case EMail("jack", Domain("net", "hoge", _*)) => println("条件に合うジャックです")
  case _ => println("ちがいます")
}

//// 実行してみますよー
// とりあえずシンプルに
scala> isJackInHogeDotNet("jack@hoge.net")
条件に合うジャックです
// サブドメインにも対応してます
scala> isJackInHogeDotNet("jack@test.hoge.net")
条件に合うジャックです

// 条件外も判定してますね
scala> isJackInHogeDotNet("jacky@hoge.net")     
ちがいます
scala> isJackInHogeDotNet("jack@hogea.net")
ちがいます
scala> isJackInHogeDotNet("jack@hoge.com") 
ちがいます

複数の抽出子を組み合わせることで複雑なパターンマッチをこなしておりますね。またパターンマッチ時に抽出子からパターンマッチ構文に返す値を固定することも可能みたいです。コレは返す値を下記のようにタプルにすればいいみたいです。

// Email抽出の際にユーザ名は文字列で、ドメイン部分はシーケンスで返す抽出子デス
object ExpandedEMail{
  // 戻りはString, Seq[String]で抽出メソッドを定義しますね
  def unapplySeq(email:String):Option[(String, Seq[String])] = {
    val parts = email split "@"
    if(parts.length == 2)
      // 戻りをタプルで返します
      Some(parts(0), parts(1).split("\\.").reverse)
    else
      None
  }
}

上記抽出子を実際に使ってみるとこんな感じに実行出来るみたいですね

// ユーザ名(文字列)とドメイン(シーケンス)で取り出してやりますよ
scala> val ExpandedEMail(name, domain @ _ *) = s
name: String = jack
domain: Seq[String] = ArrayPR(com, moge, hoge)

// 今度はユーザ名(文字列)とトップドメイン(文字列)と
// サブドメイン(シーケンス)に分割して取り出してやりますよ
scala> val ExpandedEMail(name, topdom, subdoms @ _*) = s
name: String = jack
topdom: String = com
subdoms: Seq[String] = Array(moge, hoge)

結構細かく取得出来るんですな(´・ω・`)最後の @ _*とかはシーケンスで取るってことかしらん?

上記抽出子を利用して前述のisJackInHogeDotNetを拡張してやりますよ、上記例で@ _*で展開するシーケンスは, 区切りで記述すればいいみたいですな(`・ω・´)

def isJackInHogeDotNetExpanded(s:String) = s match {
  case ExpandedEMail("jack", "net", "hoge",  _*) => println("条件に合うジャックです")
  case _ => println("ちがいます")
}

//// 実行しますよ
// 条件を満たすパターンです
scala> isJackInHogeDotNetExpanded("jack@hoge.net")
条件に合うジャックです
scala> isJackInHogeDotNetExpanded("jack@test.hoge.net")
条件に合うジャックです

// 条件を満たさないパターンです
scala> isJackInHogeDotNetExpanded("jacky@hoge.net")     
ちがいます
scala> isJackInHogeDotNetExpanded("jack@hogea.net")
ちがいます
scala> isJackInHogeDotNetExpanded("jack@hoge.com") 
ちがいます

うーん、抽出子便利だなぁ(`・ω・´)でも便利な反面テストパターンをしっかり考えないとな。結局のところ実装前にテストを規定しないと行けない雰囲気ですな。

抽出子とシーケンスパターン

さっきからなんか以下出ている次のようなパターンをシーケンスパターンというそうです

// 空ですな
List()
// 第1、第2要素のみを束縛してあとは可変長
List(x, y, _*)
// 第1~3要素を束縛して第四要素は何でも( ゚Д゚)∂゛コイヤ
List(x, 0, 0, _)

上記のようなシーケンスパターンはScalaの標準ライブラリの抽出子を利用して実装されているのだとか…せっかくなので実際の定義をチラ見してみましょうかね

package scala
// scala.Listコンパニオンオブジェクト上に抽出子を実装してるみたいです
// なのでList(...)で定義できるのですな
object List {
  def apply[T](elems:T*) elems.toList
  // パターンマッチでリストが使えるのはコレのおかげなんですな(´・ω・`)
  def unapplySeq[T](x:List[T]):Option[Seq[T]] = Some(x)
}

ちなみにここで定義されている注入メソッドapplyのおかげでListの定義を次のように行うことが出来るのですなぁ…こんなところでapplyメソッドの話が出てくるとは(´・ω・`)

scala> List()
res34: List[Nothing] = List()

scala> List(1,2,3)
res35: List[Int] = List(1, 2, 3)

scala> List("one","two","three")
res36: List[java.lang.String] = List(one, two, three)

scala.Arrayオブジェクトでも同様に注入・抽出をサポートするそうです

抽出子とケースクラス

色々な補強が自動で行われる上パターンマッチにも利用出来るという優れものだったのですが、パターンマッチに使うことでセレクターオブジェクトの具象表現型にクラスが対応している(どういう型になっているかを)晒してしまうという欠点があるみたいです。つまり、クライアントコードのパターンマッチでケースクラスを使いまくると、ケースクラスの変更が思い切り影響を及ぼしてしまうということになるみたいです。

その一方、同じように利用出来る抽出子は対象オブジェクトのデータ型とは無関係なパターンを書けるという表現独立性を持つので、クライアントの利用状況にかかわらずコンポーネントやライブラリの実装を変更するのに適しているとのことです。なので例えばクライアントがパターンとして利用しているケースクラスを変更する場合に、抽出子をデータ表現とビュー(クライアントから見える)層の間にクッション層としていれることで問題を抑えることが出来る人のことです。

ケースクラスのメリット

ただし抽出子に比べてケースクラスが優れている点としては以下の3つをあげられるみたいですね

  • コードが少ない
  • Scalaコンパイラがパターンマッチ時のコードを最適化しやすい
  • シールドクラスを継承することで値の組み合わせでパターンをチェックできる

逆に抽出子のほうはunapplySeqの定義によってほとんどなんでもできるという柔軟性をもっていると言い換えることもできるみたいですが(´・ω・`)

ケースクラスと抽出子の使い分け

とりあえず次のような基準で使い分けるのが良さそうです

  • ケースクラス
    • 外部に公開しない箇所での簡潔性・高速性・静的方チェックが要求うされる場合
  • 抽出子
    • 未知のクライアントに対して型を公開するような場合

ただし、とりあえずケースクラスで開発→必要になったら抽出子に乗換っていうのが気軽にできるのであまり気にしなくても良さそうです(´・ω・`)まあデータ表現型次第では抽出子以外の選択肢が無くなる場合もあるらしいですが

正規表現

抽出しが特に役に立つのは正規表現みたいですね。Scalaではライブラリで正規表現を提供しているものの抽出子を使うことでより便利に出来るみたいです(`・ω・´)

正規表現の形成

ScalaではJavaの流れに従ってPerl正規表現を使いますね、とりあえず正規表現のさんぷるをちらっと

 // aかab
ab?

 //1桁以上の数値
\d+

// 先頭が大文字・小文字のaからdのいずれかで
// 後ろに0個以上の英数字または_
[a-dA-D]\w* 

// 先頭にマイナスが付いたりつかなかったりで
// 1桁以上の数字が続いて
// その後ろに小数点以下の数値が続く場合もある
// かつそれぞれ3つのグループとして表現される
(-)?(\d+)(\.\d*)?

おお、よく見るヤツですな(´・ω・`)それでは抽出子的正規表現に入っていきますよ。まずScala正規表現scala.util.machingパッケージに含まれているみたいですね、なので利用する場合はimportしてやります

import scala.util.matching.Regex

んじゃ、とりあえず正規表現を定義してみますかね

// 先程のサンプルの最後のヤツを定義します
// \はエスケープ文字なので\\2個 で\1個として解釈されますね
val Decimal = new Regex("(-)?(\\d+)(\\.\\d*)?")

このエスケープ文字がめんどくさい時には"を3つ並べたScalaでの生文字列を使うといいみたいです

// エスケープ文字を無くしてやります
val Decimal = new Regex("""(-)?(\d+)(\.\d*)?""")

さらに短くするために省略形での定義をしてやります。文字列定義の末尾に.rを付与すると正規表現になるみたいですね

val Decimal = """(-)?(\d+)(\.\d*)?""".r

この省略形.rはScalaのRichStringで定義されているみたいですね。RichStringでの定義は次のようになりますね

package scala.runtime
import scala.util.matching.Regex
class RicheString(self:String) ... {
  ...
  def r = new Regex(self)
  ...
}
正規表現による探索

正規表現を実際に使って文字列探索処理をするには次のようなメソッドを使いますね

// 正規表現が最初に現れる場所を探して結果値をOptionで返す
regex findFirstIn str

// 文字列に含まれる正規表現をすべて抜き出して結果をIteratorとして返す
regex findAllIn str

// 正規表現が先頭に正規表現があるかどうかを判定して
// 条件に合う場合は結果値をOptionで返す
regex findPrefixOf str

実際に上記メソッドを使ってみますかね

// まずは正規表現にマッチする最初の要素を取り出します
scala> Decimal findFirstIn input                   
res40: Option[String] = Some(-1.0)

// まずは正規表現にマッチする全ての要素をイテレータで取り出します
scala> for(s <- Decimal findAllIn input) println(s)
-1.0
99
3

// 先頭に正規表現がある場合はソレを取得します
scala> Decimal findPrefixOf input
// 残念(´・ω・`)無いですね
res42: Option[String] = None

…と探索は以上にして実際の抽出にはいっていきますよ

正規表現による抽出

Scalaの全ての正規表現では抽出子を定義できるみたいのなので実際にやってみますかね。具体的には正規表現定義にまとめたグループに対して変数を束縛するような処理みたいです

// 符号部、整数部、小数部にグループ分けをしますです
scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r
Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?

// それでは抽出をしてやりますよ
// グループにマッチする値がない場合はnullが返りますね
scala> val Decimal(sign, integenpart, decimalpart) = "1.23"
sign: String = null
integenpart: String = 1
decimalpart: String = .23


ちなみにfor式を組み合わせることで抽出子と正規表現による探索を結合することが出来るみたいです、例えばこんな感じですかね

scala> for(Decimal(s, i, d) <- Decimal findAllIn input)              
          // 束縛した変数を利用して処理を行う感じですね
     |   println("sign: " + s + ", integer: " + i + ", decimal: "+ d)

// 結果の出力ですね
sign: -, integer: 1, decimal: .0
sign: null, integer: 99, decimal: null
sign: null, integer: 3, decimal: null

確かに抽出子を使うと正規表現が便利になるなぁ(´・ω・`)抽出子ってScalaの中でもかなりの便利ツールなんじゃないかな、と思いました。

いじょー

とりあえず抽出子的24章は終了です、次回は25章のアノテーションに進みますよー