2014年4月28日月曜日

Programming in Scala, First Edition Chapter 15

15. Case Classes and Pattern Matching
  pattern matchingについて説明する。
  case classを使えば簡単にpattern matchingを使える。
  木構造の様な再帰的なデータ構造にも使える。
  sealed classes, Option type等pattern matchingの応用についても説明する。
  最後に大規模な現実的な使用例も紹介する。

15.1 A simple example
  例として算術を表現するライブラリを作る場合を考える
    例えば、DSL(domain-specific language)の一部として設計している場合。
    abstract class Expr
    case class Var(name: String) extends Expr
    case class Number(num: Double) extends Expr
    case class UnOp(operator: String, arg: Expr) extends Expr
    case class BinOp(operator: String,
        left: Expr, right: Expr) extends Expr
    Varは x, Numberは 1.0, UnOpは +,-, BinOpは+,-等になる。
    実装が無いクラス定義では{}は省略できる。
      abstract class Expr {} と同じ意味
  Case classes
    case修飾子を付けたクラス。
    pattern matchingのmatch expressionとして使える!!
    compilerが自動的に機能を付け加えてくれる。
      factory methodの追加
        newなしでVar("x")と出来る。
        ?? 自動的にcompanion objectを生成している??
      constructorのparameterが自動的にval付き扱いになる。
        自動的にfieldとして追加されると言う事。
      toString, hashCode, equalsの自動追加。
        equalsが追加されるとそれを呼び出す==の振る舞いも定義される。
        ?? "natural" implementationsとはどう言う事??
      ** 型だけを定義するような実装の少ないクラスを書くときに特に便利。
  Pattern matching
    def simplifyTop(expr: Expr): Expr = expr match {
      case UnOp("-", UnOp("-", e))  => e   // Double negation
      case BinOp("+", e, Number(0)) => e   // Adding zero
      case BinOp("*", e, Number(1)) => e   // Multiplying by one
      case _ => expr
    }
    Javaのswitch文と似ている。
    caseに定数だけではなくて任意のexpressionを使える。
      ?? 任意と言うよりは case classで定義されたconstructorのみ?
      UnOp("-", UnOp("-", e))の様な再帰表現も使える。
        ?? C++のテンプレートに似ている? ループになる事は??
    複数matchする場合は上が適用される。
      ?? 評価は平行して行われるかもしれない??
    _ は全てにmatchする。defaultと同じ。一番最後に書く。
    => の後に何も書かないと何も実行しない。値は()になる。
    pattern matching全体として値を返す。ifやforと同様。
    fall throughしない
    どれにもmatchしないとMatchErrorの例外が投げられる。
      通常は最後にcase _ => を入れて例外を投げないようにすることが多い。

15.2 Kinds of patterns
  パターンは定義と透過的で見たままで分かる。
    どういうパターンがあるかについて気をつけるのみで良い。
  Wildcard patterns
    _は任意のオブジェクトにマッチさせられる。
    case BinOp(_, _, _) => println(expr +"is a binary operation")
  Constant patterns
    それ自身とのみマッチする。定数やvalやsingleton object。
    case 5 => "five"
    case true => "truth"
    case "hello" => "hi!"
    case Nil => "the empty list" // singleton object
  Variable patterns
    _と同様に任意のオブジェクトにマッチさせられる。
    マッチしたオブジェクトをvariableとしてbindして後で使える。
      ** perlの正規表現の$1,$2...と同じように強力。
    case somethingElse => "not zero: "+ somethingElse
  Variable or constant?
    小文字始まりだとパターン変数、それ以外は定数とみなされる!!
      this.piや`pi`とすると小文字始まりも定数とみなされる。
        ``は予約語を普通の識別子と認識させる場合にも使える。
  Constructor patterns
    case classのconstructorとマッチさせる。とても強力。
    constructorのパラメータにもパターンマッチが再帰的に適用される。
      パラメータが自分自身のConstructor patternでもOK。
    case BinOp("+", e, Number(0)) => println("a deep match")
      BinOpクラスのコンストラクタとマッチする。
        "+": 定数として同じオブジェクトとマッチする。
        e: 変数として任意のオブジェクトとマッチして、bindされる。
        Number(0): Numberクラスのコンストラクタとマッチする。
          0: 定数として同じオブジェクトとマッチする。
  Sequence patterns
    List or Arrayの様なsequence typesもcase classと同様にマッチさせられる。
      case List(0, _, _) => println("found it")
    0個以上の任意のエントリのときは_*を指定する。
      case List(0, _*) => println("found it")
      ?? _*以外に、0*で0の任意回の繰り返しとか、_+で1個以上とか出来ない??
  Tuple patterns
    tupleもsequenceと同様。
    case (a, b, c)  =>  println("matched "+ a + b + c)
  Typed patterns
    selectorのtypeとマッチするかを判定できる。
    case s: String => s.length
      selectorはAnyだがsはString。selectorをcastしている。
      ?? case s: _とか、case s: xとかで型を取り出したりは出来ない??
    The operators isInstanceOf and asInstanceOfを使えば似たようなことが出来る。
      if (x.isInstanceOf[String]) {
        val s = x.asInstanceOf[String]
        s.length
      } else ...
      上記のtype test & castは冗長。
       pattern matchに誘導するために意図的にそう言語設計している。
    case m: Map[_, _] => m.size
      _は任意の型にマッチするワイルドカード。
      (小文字始まりの!!)型変数を使える。
        ?? bindは出来ない?? 後で型引数として使う事も出来ない??
  Type erasure
    $ scala -uncheckedのオプションで上げて下記を実行
      scala>  def isIntIntMap(x: Any) = x match { case m: Map[Int, Int] => true }
      <console>:5: warning: non variable type-argument Int in
      type pattern is unchecked since it is eliminated by erasure
               case m: Map[Int, Int] => true
    scalaはjavaと同様にerasure modelを採用している。
      実行時には型情報が分からない。
        Map[]が[Int, Int]だと言う事が分からない。
    Arrayは例外で型情報を実行時も保持している。
  Variable binding
    @を使ってマッチしたパターンの一部を変数に束縛出来る。
    case UnOp("abs", e @ UnOp("abs", _)) => e
    全体がマッチした時に、eにはUnOp("abs", _)の実体が束縛される。

15.3 Pattern guards
  patternの後ろにifを付けてguardする事が出来る。
    パターンにマッチして更にifが真ならマッチ。
  case BinOp("+", x, x) => BinOp("*", x, Number(2))
    これはエラーになる。パターン変数を一回しか書けないため。
  case BinOp("+", x, y) if x == y => BinOp("*", x, Number(2))
    これでx==yの時のみマッチする。
  case n: Int if 0 < n => ...
    Intで正の時のみマッチ。
  case s: String if s(0) == 'a' => ...
    文字列で一文字目が'a'の時のみマッチ。

15.4 Pattern overlaps
  複数caseにマッチするような場合は上のcaseが適用される。
  到達しないcaseがある場合はコンパイルエラーになる。

15.5 Sealed classes
  scalaではコンパイル時にcaseに漏れがある事を検出出来ない。
    部分コンパイルなので、一緒にコンパイルされてない部分での追加が分からない。
  sealed classにすれば同一ファイル以外でのサブクラスの追加を禁止できる。
    sealed abstract class Expr
   case classを作る時はsealed出来ないか検討すべき。
    sealedすればコンパイラがcase漏れを警告してくれる。
  逆にコンテクストから入らないcaseがある事が分かっている場合の警告の抑制。
    @unchecked anotationを使う。Chapter 25
    (e: @unchecked) match {...}

15.6 The Option type
  Optionと言う標準の型がある。
    Some(x): 実際の値がxの場合。
    None: 値が無い場合。
  collectionに対する操作の結果として使われる場合が多い。
    Mapのgetメソッド等。
  pattern matchを使って値を取り出す。
    x match {
      case Some(s) => s
      case None => "?"
    }
  Option TypeはScalaでは良くつかわれる。
  javaだと値が無いときはnullを返す物が多い。
    何がnullを返すのかを知っている必要がある。
    nullチェックを忘れるリスクが高い。
    nullを返す場合が少ないとnullチェックの漏れをテストでも検出出来ない。
    scalaだとnullをvalue typeの型には返せない。
  scalaだとOption typeを使う事を推奨。
    値が返らない可能性がある事を型から把握できる。
    nullチェックを忘れるとコンパイルが通らなくなるで直ぐに分かる。

15.7 Patterns everywhere
  match以外にも色々な場所でpatternを使える。
  Patterns in variable definitions
    valやvarで値を定義するとき。
    val myTuple = (123, "abc")
    val (number, string) = myTuple // tupleを分解
    val exp = new BinOp("*", Number(5), Number(1)) // case classを分解
    val BinOp(op, left, right) = exp
  Case sequences as partial functions
    {}内のcaseの並びは関数リテラルになっている。
      エントリポイント(パラメータリスト)と実体の組が複数ある。
      val withDefault: Option[Int] => Int = {
        case Some(x) => x
        case None => 0
      }
    一部のパターンにのみ対応したpartial functionsにも出来る。
      val second: PartialFunction[List[Int],Int] = {
        case x :: y :: _ => y
      }
      PartialFunction型にしてコンパイラにPartialFunctionを示す。
      isDefinedAtメソッドでcaseの定義済、未定義をチェックできる。
    一般的には可能なら全てのcaseを網羅したfull関数を作ろうとするべき。
      partialにする場合
        来ないcaseに確信がある場合
        isDefinedAtメソッドで定義済かを事前に調べる場合
  Patterns in for expressions
    for (Some(fruit) <- results) println(fruit)
    for式の値の抽出に使える。
    Some(fruit)にマッチしなかったら捨てられる。filterと同じ動き。
    None以外だけを処理することが出来る。

15.8 A larger example
  大規模で現実的なpattern matchingの応用として、数式の表現を考える。
    Chapter 10で作成したlayout libraryも活用する。
    内部表現はこの章で作成したExprクラスを活用する。
      逆ポーランド記法に似た内部表現になる。
    ()についても省略出来るところは省略するようにする。
  ExprFormatter: formatter class
    ((a / (b * c) + 1 / n) / 3) これを下記の様に表示する。
    BinOp("/", BinOp("+", BinOp("/", Var("a"),
                                     BinOp("*", Var("b"), Var("c")),
                          BinOp("/", Number(1),
                                     Var("n")),
               Number(3))
      a     1
    ----- + -
    b * c   n
    ---------
        3
  括弧を付ける条件を検討する。
    2項演算子の優先順位を定義する。
      "/"については今回の表記では括弧を気にする必要はない。常に不要。
    Mapで手動で定義する方法もあるが、定数を自分でincrementするのがイマイチ。
      Map("|" -> 0, "||" -> 0, "&" -> 1, "&&" -> 1, ...)
    優先度が同じグループ単位に配列に登録して、そこから自動生成する。
      private val opGroups = Array(Set("|", "||"), Set("&", "&&"), ...)
      private val precedence = {
        val assocs = for {
          i <- 0 until opGroups.length
          op <- opGroups(i)
        } yield op -> i
        Map() ++ assocs
      }
      private val unaryPrecedence = opGroups.length
      private val fractionPrecedence = -1
      "yield op -> i"は"(op, i)"と同じ意味。
      assocsはIndexedSeq[(String, Int)]だがMap()と++すると暗黙変換でMapになる
      "opGroups.length"で最大優先度より一つ高い優先度が得られる。
      "/"のfractionPrecedenceは特別な値にする。
    pattern matchingを使って各演算子を再帰的に処理する。
      private def format(e: Expr, enclPrec: Int): Element =
        e match {
          case Var(name) => elem(name)
          case Number(num) =>
            def stripDot(s: String) =
              if (s endsWith ".0") s.substring(0, s.length - 2)
              else s
            elem(stripDot(num.toString))
          case UnOp(op, arg) => elem(op) beside format(arg, unaryPrecedence)
          case BinOp("/", left, right) =>
            val top = format(left, fractionPrecedence)
            val bot = format(right, fractionPrecedence)
            val line = elem('-', top.width max bot.width, 1)
            val frac = top above line above bot
            if (enclPrec != fractionPrecedence) frac
            else elem(" ") beside frac beside elem(" ")
          case BinOp(op, left, right) =>
            val opPrec = precedence(op)
            val l = format(left, opPrec)
            val r = format(right, opPrec + 1)
            val oper = l beside elem(" "+ op +" ") beside r
            if (enclPrec <= opPrec) oper
            else elem("(") beside oper beside elem(")")
        }

        def format(e: Expr): Element = format(e, 0)
      }
      "enclPrec"はenclosing the expressionで一個外側のoperatorの優先度。
        2項演算時に外側より内側の方が優先度が低かったら括弧が必要。
      fraction,"/"が連続する場合は演算順序が分かるようにする必要がある。
        後から割る方の横線を長くする。
          先に割る方を返すときにスペースで膨らませることで実現。
      "2.0"等の".0"を削除する処理も実装。
      Elementクラスがセンタリングするように実装されているためそのまま使える。
        ** 凄い!!

15.9 Conclusion
  case classesとpattern matchingを使う事で普通のオブジェクト指向言語より簡素に書ける。
  scalaのpattern matchingはここで説明したものよりもっと強力。
  自分のクラスに自分はpattern matchingを使いたいが、外部には公開したくない場合
    extractors described in Chapter 24が使える。

0 件のコメント:

コメントを投稿