ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라] 익스트랙터(extractor)
    스칼라 2022. 3. 2. 18:00

    Programming in scala 4th edition 26장

     

    1. 익스트랙터

    익스트랙터는 unapply 메서드를 갖고 있는 객체이다. 패턴 매치 시 익스트랙터 객체를 참조하는 패턴을 만나면 unapply메서드가 호출된다.

    // selectorString match { case EMail(user, domain) => ... }
    // 위는 아래의 호출을 일으킴
    // EMail(selectorString)
    
    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
      }
    }
    
    val x: Any = "bees1114@naver.com"
    
    x match {
      case Email(user, domain) => println(user, domain)
      case _ => println("default")
    }

    패턴 매치 식은 x의 타입을 확인해서 Email.unapply의 인자 타입과 비교한다. unapply에 x를 대입할 수 있으면 해당 라인을 실행한다. 

     

    2. 변수가 없거나 1개만 있는 패턴

    변수가 1개만 있는 패턴: 변수 2개를 반환할 때와 마찬가지로 Some으로 감싸 리턴하면 됨

    변수가 하나도 없는 패턴: 익스트랙터 패턴이 아무 변수도 바인딩하지 않는 경우도 있음. 이런경우 Boolean을 리턴함

    object Twice {
      def apply(s: String): String = s + s
      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
      }
    }
    
    object UpperCase {
      def unapply(s: String): Boolean = s.toUpperCase == s
    }
    
    def userTwiceUpper(s: String) = s match {
      case Email(Twice(x @ UpperCase()), domain) =>
        "match: " + x + " in domain " + domain
      case _ => "no match"
    }
    
    userTwiceUpper("ABCDABCD@gmail.com")
    // match: ABCD in domain gmail.com

    위의 예제에서 userTwiceUpper에서 "ABCDABCD@gmail.com"이 들어가면 x @ UpperCase()에 의해서 UpperCase를 만족하는 s를 x에 맵핑시킨다. 이후 Twice.unapply, Email.unapply가 적용되어 x와 domain에 유저와 도메인이 할당된다.

     

    3. 가변 인자 익스트랙터

    unapply는 Option[T]의 타입을 리턴하게 되어있다. 가변 길이를 가진 인자를 반환하는 unapply를 정의하는 방법도 있다. unapplySeq를 정의해서 Option[Seq[T]] 타입을 리턴하면 된다.

    object Domain {
      def apply(parts: String*): String =
        parts.reverse.mkString(".")
    
      def unapplySeq(whole: String): Option[Seq[String]] =
        Some(whole.split("\\.").reverse)
    }
    
    "net" match {
      case Domain("org", "acm") => println("acm.org")
      case Domain("com", "sun", "java") => println("java.sun.com")
      case Domain("net", _*) => println("a .net domain")
    } // "a .net domain"
    
    "acm.org" match {
      case Domain("org", "acm") => println("acm.org")
      case Domain("com", "sun", "java") => println("java.sun.com")
      case Domain("net", _*) => println("a .net domain")
    } // "acm.org"

    위의 unapplySeq는 패턴 매치의 셀렉터를 받아 Option[Seq[String]]을 반환한다. 그 결과물을 가지고 패턴을 정의한 모습이다.

    unapplySeq는 고정 요소와 가변 길이를 함께 반환할 수도 있다.

    object ExpandedEMail {
      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
      }
    }
    
    "abcd@company.gmail.com" match {
      case ExpandedEMail(user, others @ _*) => println(user, others)
    } // 출력 (abcd,ArraySeq(com, gmail, company))

     

    4. 익스트랙터와 시퀀스 패턴

    val List(x, y, r @ _*) = List(1, 2, 3, 4, 5)
    print(x, y, r)

    패턴 매치를 이용해서 위처럼 Seq 안의 원소들을 바로 변수에 할당 할 수 있었다. 이건 실제로 List 객체 안에 unapplySeq가 정의되어있기 때문이다.

     

    5. 익스트랙터와 케이스 클래스

    익스트랙터와 케이스클래스가 하는 기능은 유사하다. 아래는 익스트랙터와 케이스 클래스의 장단점이다.

    • 익스트랙터는 표현독립성이 유지된다. 객체 내부 데이터 구조와 패턴 사이의 관계가 없어 객체 내부 데이터 추가/제거에 더 유리하다. 케이스클래스는 객체 내부 데이터와 패턴이 연관되어 변경할 때 고려해야 할 점이 더 많다.
    • 케이스클래스는 정의가 쉽고 코드가 적다. 컴파일러가 컴파일 할 때 익스트랙터의 패턴 매치보다 좀 더 최적화 할 수 있다.
    • Sealed case class 를 사용할 경우 컴파일러가 모든 패턴을 다 다루는지 아닌지에 대해서 경고해줄 수 있다.

     

    6. 정규표현식

    스칼라 정규표현식 클래스는 scala.util.matching 패키지에 들어있다.

    import scala.util.matching.Regex
    
    val regexp = new Regex("\\d+")
    val numbers = regexp.findAllMatchIn("a123def4").toList
    numbers  // List(123, 4)

    정규표현식은 일반적인 문자열로 표현하면 \\을 붙여야 \으로 활용할 수 있다. raw string을 활용하여 \만으로 정규표현식을 나타낼 수도 있다.

    val regexp2 = new Regex("""(-)(\d)+""")
    regexp2.matches("-10") // true
    regexp2.matches("10")  // false

    new Regex 대신 StringOps 클래스 안에 정의된 r을 이용하여 정규표현식을 정의할 수도 있다.

    val regexp3 = """[A-Z]\w*""".r
    
    regexp3.matches("abcd") // false
    regexp3.matches("ABCD") // true

     

    정규표현식과 익스트랙터

    스칼라 정규표현식은 그룹별로 매치하는 부분 문자열을 뽑아낼 수 있는 익스트랙터를 제공한다.

    val regexp3 = """(-)?([A-Z]+)(\d*)""".r
    
    var regexp3(sign, word, number) = "-ABCD123"
    print(sign, word, number) // -, ABCD, 123 출력됨
    
    var regexp3(sign, word, number) = "ABCD"
    print(sign, word, number) // null, ABCD, "" 출력됨

     

    댓글

Designed by Tistory.