タイプのセキュリティとユーザーフレンドリーなRegex Builderの作成


受領番号

Description vs Evaluation


関数プログラミングの核心の一つは、記述と評価を分離することである.descriptionと評価の分離はlazy評価によって実現することができ、lazy評価はその名の通りdescriptionに対する評価を実行可能な最後のステップに推し進めることである.このlazy評価,dscriptおよび評価分離の概念によりscalaでよく用いられる種々のモードで発見できる.
  • の一級関数について、この関数がどのように実行されるかについて説明したが、実際の関数の実行はパラメータを渡すときに実行される.
  • 関数の実行時にエラーが発生した場合は,直ちにエラーを投げ出すことなく,Ether[E,A]に値を入れ,その値の評価を呼び出し者に依頼する.
  • 例えば、
  • Streamは、すぐに値を投げ出すことなく、必要に応じてデータを発行するだけで、不要な演算を最大限に減らすことができます.
  • ADT & Interpreter & DSL


    ADTと解釈器を用いた領域モデリングもまた,記述と評価を分離する設計モードの1つと見なすことができる.ADTはalgebric data typeの略であり,我々が興味を持っているドメインをデータ構造として抽象化しているといえる.ADTは単純なデータフォーマットであり、論理を含まないため、ADTだけでは完了したアプリケーションを作成できません.
    ADTに真の意味を持たせるためには、ADTを解釈し、実際に必要な論理を実行する解釈器が必要である.
    またADTは関数ではなくデータなので,それを用いてプログラムの記述を書くのは想像以上に見慣れず不便である.したがって、ADTのDSLを簡単に作成して処理することができる.
    ADT、Interpreter、およびDSLを使用するレルムモデリング技術の例には、SQL query builderが含まれる.
    dslで使用可能な構文は、
  • ADTによって定義される.
  • dslでqueryを記述します.
  • およびこのqueryは、解釈器によって解析され、実際のDBでqueryが送信される.
  • この方法の長所は、
  • DSLによる直感的かつ宣言的なプログラム記述
  • DSLは、その名の通りドメイン固有です.我々が解決すべき問題分野を最適化する言語であるため,dslを用いるとプログラムの動作を非常に直感的に「述べる」ことができる.DSLによってコアビジネスロジックを管理することができ、コードのメンテナンスとコラボレーションの観点から大きなメリットをもたらします.また、dslはADT定義の構文に基づいて作成されるため、typeセキュリティは失われません.
  • ADTを解釈器から分離することによって、関心のある
  • を分離する.
    注目点の分離は,どの部分のコードを記述する際にどの部分に注目するかの認識においても利点があるが,解釈器具に外部依存性が多くあれば,それを明確に分離することでコードのメンテナンス性を向上させることができる.実際には、上記の例のSQL query builderでは、解釈器が特定のデータベースに依存している可能性がありますが、ADT自体に依存していないため、純粋な構文しか定義できません.

    練習:Regex Builder


    今回の実験では,scalaを用いてADT&Interpreter&DSL方式でRegex Builderを作成することを試みる.regexを使用するときに感じる不便は以下の通りです.
  • regexは実際には単純な文字列であるため、->コンパイル時に
  • をチェックすることはありません.
  • regex文法は非常に混乱し、困難である.使うたびにグーグル化し直す必要がある
  • 今回の実践で作成したRegex Builderには、次のような利点が期待できます.
  • ADT定義された構文に従ってタイプセキュリティ
  • を保証する.
  • regexの実際の構文が分からなくても、dslで簡単にregex(ユーザーフレンドリー)
  • を作成できます.

    1. Regex ADT


    Regex ADTはdslの構文を定義します.scalaは、一般に、シール特性を用いてADTを記述する.
    sealed trait Regex
    
    object Regex {
      case object Empty                                     extends Regex
      case object AnyChar                                   extends Regex // except newline
      sealed trait CharSpecifier                            extends Regex
      sealed trait Quantified                               extends Regex
      sealed trait Anchored                                 extends Regex
      sealed trait Set                                      extends Regex
      case class Grouped(regex: Regex)                      extends Regex
      case class Concatenated(regex1: Regex, regex2: Regex) extends Regex
      case class Or(regex1: Regex, regex2: Regex)           extends Regex
    
      object CharSpecifier {
        case class Literal(char: Char)             extends CharSpecifier
        case object Word                           extends CharSpecifier // alphabet & underscore
        case object Digit                          extends CharSpecifier
        case object WhiteSpace                     extends CharSpecifier
        case object NotWord                        extends CharSpecifier
        case object NotDigit                       extends CharSpecifier
        case object NotWhiteSpace                  extends CharSpecifier
        case object Tab                            extends CharSpecifier
        case object NewLine                        extends CharSpecifier
        case class InRangeOf(from: Char, to: Char) extends CharSpecifier
      }
    
      object Quantified {
        case class QuantifiedRange(regex: Regex, min: Int, max: Int) extends Quantified
        case class QuantifiedMin(regex: Regex, min: Int)             extends Quantified
        case class QuantifiedExact(regex: Regex, quantity: Int)      extends Quantified
        case class OneOrMore(regex: Regex)                           extends Quantified
        case class ZeroOrMore(regex: Regex)                          extends Quantified
        case class Optional(regex: Regex)                            extends Quantified
      }
    
      object Anchored {
        case object WordBoundary    extends Anchored
        case object NotWordBoundary extends Anchored
        case object Beginning       extends Anchored
        case object Ending          extends Anchored
      }
    
      object Set {
        case class AnyOf(candidates: Seq[CharSpecifier])    extends Set
        case class NotAnyOf(candidates: Seq[CharSpecifier]) extends Set
      }
    }

    2. Interpreter


    次に、上記で定義したADTを説明して、実際のscalaを得る.解析器を作成し、utilパッケージから提供されるRegexに変換します.Interpreter[From, To]という一般的なtypeclassが作成され、Regex Interpreterにtypeclassが継承されます.
    trait Interpreter[-From, +To] {
      def interpret(from: From): To
    }
    
    val regexInterpreter: Interpreter[Regex, ScalaRegex] = new Interpreter[Regex, ScalaRegex] {
      override def interpret(from: Regex): ScalaRegex =
        toString(from).r
    
      private def toString(from: Regex): String = from match {
        case Regex.Empty                        => ""
        case Regex.AnyChar                      => "."
        case specifier: Regex.CharSpecifier     =>
          specifier match {
            case CharSpecifier.Literal(char)       =>
              if ("+*?^$\\.[]{}()|/".contains(char)) s"\\$char"
              else char.toString
            case CharSpecifier.Word                => "\\w"
            case CharSpecifier.Digit               => "\\d"
            case CharSpecifier.WhiteSpace          => "\\s"
            case CharSpecifier.NotWord             => "\\W"
            case CharSpecifier.NotDigit            => "\\D"
            case CharSpecifier.NotWhiteSpace       => "\\S"
            case CharSpecifier.Tab                 => "\\t"
            case CharSpecifier.NewLine             => "\\n"
            case CharSpecifier.InRangeOf(from, to) => s"[$from-$to]"
          }
        case quantified: Regex.Quantified       =>
          quantified match {
            case Quantified.QuantifiedRange(regex, min, max) => s"${toString(regex)}{$min,$max}"
            case Quantified.QuantifiedMin(regex, min)        => s"${toString(regex)}{$min,}"
            case Quantified.QuantifiedExact(regex, quantity) => s"${toString(regex)}{$quantity}"
            case Quantified.OneOrMore(regex)                 => s"${toString(regex)}+"
            case Quantified.ZeroOrMore(regex)                => s"${toString(regex)}*"
            case Quantified.Optional(regex)                  => s"${toString(regex)}?"
          }
        case anchored: Regex.Anchored           =>
          anchored match {
            case Anchored.WordBoundary    => "\\b"
            case Anchored.NotWordBoundary => "\\B"
            case Anchored.Beginning       => "$"
            case Anchored.Ending          => "^"
          }
        case set: Regex.Set                     =>
          def concat(candidates: Seq[CharSpecifier]): String =
            candidates.foldLeft("") { (acc, r) =>
              val str = r match {
                case CharSpecifier.InRangeOf(from, to) => s"$from-$to"
                case charSpecifier                     => toString(charSpecifier)
              }
              s"$acc$str"
            }
    
          set match {
            case Set.AnyOf(candidates)    => s"[${concat(candidates)}]"
            case Set.NotAnyOf(candidates) => s"[^${concat(candidates)}]"
          }
        case Regex.Grouped(regex)               => s"(${toString(regex)})"
        case Regex.Concatenated(regex1, regex2) => s"${toString(regex1)}${toString(regex2)}"
        case Regex.Or(regex1, regex2)           => s"(?:${toString(regex1)}|${toString(regex2)})"
      }
    }
    ADTで作成したプログラムでは、次の解釈器を使用して外部世界に会います.
    val regex: Regex = ???
    val interpreted: ScalaRegex = regexInterpreter.interpret(regex)

    3. DSL


    前述したように,ADTを用いてプログラムの記述を完了するだけでは厄介なことである.したがって、ADTの生成および操作が容易な関数からなるDSLを作成することができる.
    trait RegexSyntax {
      implicit def literal(char: Char): Regex  = Literal(char)
      implicit def literal(str: String): Regex = str.foldLeft(Empty: Regex) { (acc, r) => acc ++ Literal(r) }
    
      implicit class RegexOps(val regex: Regex) {
        def concatWith(regex2: Regex): Regex = Concatenated(regex, regex2)
    
        def oneOrMore: Regex  = OneOrMore(regex)
        def optional: Regex   = Optional(regex)
        def zeroOrMore: Regex = ZeroOrMore(regex)
    
        def quantified(min: Int, max: Int): Regex = Quantified.QuantifiedRange(regex, min, max)
        def quantifiedMin(min: Int): Regex        = Quantified.QuantifiedMin(regex, min)
        def quantifiedExact(quantity: Int): Regex = Quantified.QuantifiedExact(regex, quantity)
    
        def or(regex2: Regex): Regex = Or(regex, regex2)
    
        def ++(regex2: Regex): Regex = Concatenated(regex, regex2)
      }
    
      implicit class CharRegexOps(val char: Char) {
        def ~(char2: Char): Regex = InRangeOf(char, char2)
        def l: Regex              = Literal(char)
      }
    
      implicit class StringRegexOps(val str: String) {
        def l: Regex = literal(str)
      }
    }
    ADTを直接使用してregexを作成する方法と、DSLを使用してregexを作成する方法の違いは、次の方法で確認できます.
    ex)電話番号Regexの作成
      // make regex using ADT directly
      val koreanPhoneNumber1: Regex =
        Concatenated(
          Concatenated(
            Concatenated(
              Concatenated(
                Or(
                  Concatenated(Concatenated(Literal('0'),Literal('1')),Literal('0')),
                  Concatenated(Concatenated(Literal('0'),Literal('1')),Literal('1'))
                ),
                Literal('-')
              ),
              QuantifiedExact(Digit,4)
            ),
            Literal('-')
          ),
          QuantifiedExact(Digit,4)
        )
    
      // make regex using DSL
      val koreanPhoneNumber2: Regex =
        ("010".l or "011") ++ '-' ++
          Digit.quantifiedExact(4) ++ '-' ++
          Digit.quantifiedExact(4)
    次の例では、ISO-8601 datetime formatを表すregexを作成します.宣言的なインターフェースにより,使いやすく直感的なDSLが形成されているようである.
    // year: 0000~9999
    private val year: Regex  = Digit.quantifiedExact(4)
    // month: 01~12
    private val month: Regex =
        ('0'.l ++ '1' ~ '9') or ('1'.l ++ '0' ~ '2')
    // date: 01~31
    private val date: Regex  =
        ('0'.l ++ '1' ~ '9') or ('1' ~ '2' ++ Digit) or ('3'.l ++ '0' ~ '1')
    // hour: 01~23
    private val hour: Regex   = ('0' ~ '1' ++ Digit) or ('2'.l ++ '0' ~ '3')
    // minute: 00~59
    private val minute: Regex = '0' ~ '5' ++ Digit
    // second: 00~59
    private val second: Regex = '0' ~ '5' ++ Digit
    
    // iso8601: {YYYY}-{MM}-{DD}T{hh}:{mm}:{ss}Z
    val iso8601Format: Regex =
      year ++ '-' ++
        month ++ '-' ++
        date ++ 'T' ++
        hour ++ ':' ++
        minute ++ ':' ++
        second ++ 'Z'