: 패턴 매치의 생성자 패턴은 해당 클래스가 케이스 클래스이기 때문에 가능하다. 케이스 클래스는 만들고 싶지 않지만 생성자 패턴을 사용하고 싶고, 자신만의 패턴을 만들고 싶다면 익스트랙터를 사용한다.

 

26.1 예제 : 전자우편 주소 추출 - 전자 우편 주소를 표현하는 문자열 분석

def isEMail(s: String): Boolean    // 전자우편 주소인지 아닌지
def getDomain(s: String): String    // 전자 주소의 도메인 리턴
def getUser(s: String): String          // 전자 주소의 사용자 리턴

 

if (isEMail(s)) println(getUser(s) +" AT "+ getDomain(s))
else println("not an email address")

 

=>  전자 우편 문자열을 Email(user, domain) 으로 패턴 매치할 수 있다면?

s match {
  case EMail(user, domain) => println(user +" AT "+ domain)
  case _ => println("not an email address")
}

 

// 같은 사용자의 전자우편 주소 2개가 연속으로 있는 경우

ss match {
  case EMail(u1, d1) :: EMail(u2, d2) :: _ if (u1 == u2) => ...
...
}

: 세 가지 도우미 함수로 작성한 것보다 패턴 매치의 가독성이 좋다. 하지만 전자우편이 문자열이라 케이스 클래스가 아니다. => 문자열은 EMail(user, domain)으로 패턴 매치가 불가능하다. => 익스트랙터를 사용하면 기존 타입에 새로운 패턴을 정의할 수 있다.

 

26.2 익스트랙터

: 익스트랙터는 unapply라는 메소드가 있는 객체

: unapply 메소드의 목적은 값을 매치시켜 각 부분을 나누는 것

: 반대로 값을 만들어내는 apply라는 메소드도 존재(필수는 아님)

object EMail {

  def apply(user: String, domain: String) = user +"@"+ domain   // 인젝션 메소드 (선택적)
       // EMail("John", "epfl.ch") => "John@epfl.ch" 반환 : 두 문자열을 취해서 전자우편 주소 문자열을 만든다.

      // apply 메소드를 명시적으로 만들고 싶다면 Function2[String, String, String] 함수 타입 상속

      // object EMail extends ((String, String) => String) { ... }

 


  def unapply(str: String): Option[(String, String)] = {     // 익스트랙터 메소드(필수)
    val parts = str split "@"
    if (parts.length == 2) Some(parts(0), parts(1)) else None
  }

     // 전자우편 주소 문자열을 받아서 (사용자 문자열, 도메인 문자열)을 Option 타입으로 반환 : apply와 역으로 진행

    // str이 전자 우편 주소라면 Some(user, domain), 아니라면 None 반환
}

 

=> 패턴 매치 시 익스트랙터 객체를 참조하는 패턴을 만나면 항상 그 익스트렉터의 unapply 메소드를 설렉터 식에 대해 호출

 

selectorString match { case EMail(user, domain) => ... }   // == EMail.unapply(selectorString)

     // unapply에서 None이 반환되면 패턴 매치가 이뤄지지 않는다.

    // Some(u,d)이 반환되면 패턴이 매치되어 unapply가 반환한 값이 각 변수에 바인딩 => user가 u에 domain이 d에 바인딩

* 익스트랙터를 이용한 패턴 매치를 하려면 셀렉터 식의 타입은 unapply 인자 타입보다 일반적이어야 한다.

val x: Any = ...
x match { case EMail(user, domain) => ... }

: 패턴 매처가 위 코드를 본다면 x가 EMail의 unapply 메소드 인자 타입인 String과 부합하는 지 살펴본다. 부합하는 경우, 매처는 값을 String으로 캐스팅해서 처리하고 부합하지 않으면 매치가 바로 실패한다.

- apply 메소드 : 인젝션, 인자를 몇 가지 받아서 어떤 집합의 원소를 만들어 낸다.(익스트랙터 객체에 의무 X)

- unapply 메소드 : extraction, 어떤 집합에 속한 원소에서 여러 부분의 값을 뽑아낸다.

- 인젝션과 익스트랙션 메소드는 서로 쌍대성 => 쌍대성은 좋은 설계 원칙으로 익스트랙터를 설계할 때 지키는 편이 좋다.

EMail.unapply(EMail.apply(user, domain))    =>  (user, domain)에 apply와 unapply를 적용하면 Some(user, domain) 반환

EMail.unapply(obj) match {    => user@domain에 unapply와 apply를 적용하면 Some(user@domain) 반환
  case Some(u, d) => EMail.apply(u, d)
}

 

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

: unapply로 N개의 변수를 바인딩하고 싶다면 N개의 원소로 된 튜플을 Some에 감싸서 반환하면 된다.

: 패턴이 변수를 하나만 바인딩해야 할 경우 스칼라에는 1튜플이 없기 때문에 unapply는 원소 자체를 Some으로 감싼다.

" 같은 부분 문자열을 두 번 반복해 만든 문자열 매치 "

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
  }
}

: 아무 변수도 바인딩하지 않을 경우 unapply 메소드는 Boolean 값을 반환한다. 매치 성공인 경우 true, 실패인 경우 false

" 문자열의 모든 문자가 대문자인지 확인 "

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"
}

scala> userTwiceUpper("DIDI@hotmail.com")
res0: java.lang.String = match: DI in domain hotmail.com

scala> userTwiceUpper("DIDO@hotmail.com")
res1: java.lang.String = no match

scala> userTwiceUpper("didi@hotmail.com")
res2: java.lang.String = no match

 

26.4 가변 인자 익스트랙터

: unapplySeq 메소드를 사용하면 가변 길이 매치를 할 수 있다. unapplySeq의 결과 타입은 꼭 Option[Seq[T]]와 부합해야 한다.

* unapply는 매치 성공 시 항상 고정된 숫자의 하위 원소를 반환했기 때문에 가변 인자에 적용 X

* Seq는 시퀀스를 나타내는 List, Array, WrappedString 등 여러 클래스의 공통 슈퍼 클래스

object Domain {

  def apply(parts: String*): String =
    parts.reverse.mkString(".")

  def unapplySeq(whole: String): Option[Seq[String]] =
    Some(whole.split("\\.").reverse)
}

 

" 이름이 tom이고 도메인이 .com인 전자우편 주소 검색 "

def isTomInDotCom(s: String): Boolean = s match {
  case EMail("tom", Domain("com", _*)) => true
  case _ => false
}

 

scala> isTomInDotCom("tom@sun.com")
res3: Boolean = true

scala> isTomInDotCom("peter@sun.com")
res4: Boolean = false

scala> isTomInDotCom("tom@acm.org")
res5: Boolean = false

: 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
  }
}

scala> val s = "tom@support.epfl.ch"
s: java.lang.String = tom@support.epfl.ch

scala> val ExpandedEMail(name, topdom, subdoms @ _*) = s
name: String = tom
topdom: String = ch
subdoms: Seq[String] = WrappedArray(epfl, support)

 

26.5 익스트랙터와 시퀀스 패턴

: 리스트나 배열의 원소를 시퀀스 패턴으로 접근할 수 있다.

List()
List(x, y, _*)
Array(x, 0, 0, _)

: 시퀀스 패턴은 모두 표준 스칼라 라이브러리의 익스트랙터를 사용해 구현한 것

Ex. List(...) 패턴이 가능한 이유는 scala.List 동반 객체에 unapplySeq 정의가 있기 때문

 

26.6 익스트랙터와 케이스 클래스

: 케이스 클래스는 아주 유용하지만 생성자 패턴에 있는 클래스 이름이 셀렉터 객체의 구체적인 표현 타입과 대응한다.(=데이터의 구체적인 표현이 드러난다.) => 케이스 클래스에 대해 패턴 매치를 하는 클라이언트 코드가 이미 있다면 케이스 클래스 이름을 바꾸거나 클래스 계층 구조를 변경하면 클라이언트 코드에 영향을 끼친다.

: 익스트랙터는 패턴과 그 패턴이 선택하는 객체의 내부 데이터 표현 사이에 아무런 관계가 없도록 만든다.(표현 독립성) => 클래스가 바뀌더라도 클라이언트 코드에는 영향을 미치지 않는다.

* 표현 독립성은 익스트랙터의 장점이다.

* 케이스 클래스는 설정하고 정의하기 훨씬 쉽고 코드도 적게 필요하다.

* 케이스 클래스는 익스트랙터보다 더 효과적인 패턴매치가 가능하다.(케이스 클래스의 메커니즘은 변하지 않는 반면 익스트랙터의 unapply 안에서는 아무 일이나 할 수 있기 때문에 스칼라 컴파일러가 케이스 클래스의 패턴 매치를 익스트랙터의 패턴 매치보다 더 잘 최적화한다.)

* 케이스 클래스를 봉인된 케이스 클래스로 만들 경우 패턴 매치가 모든 가능한 패턴을 다 다루는지 스칼라 컴파일러가 검사해서 그렇지 않은 경우 경고를 해준다.

=> 여러 클라이언트에게 노출해야 한다면 표현 독립성을 위해 익스트랙터를 사용하고 아니면 케이스 클래스를 사용한다

* 익스트랙터나 케이스 클래스의 패턴 매치는 똑같이 보이기 때문에 케이스 클래스로 시작한 다음 필요에 따라 익스트랙터로 바꾸면 된다.

 

26.7 정규 표현식

< 정규 표현식 만들기 >

- ab? : 'a' or 'ab'

- \d+ : \d는 숫자(0~9), 하나 이상의 숫자로 구성된 문자열

- [a-dA-D]\w* : a부터 d까지 대문자/소문자로 시작하는 단어, \w는 단어를 이루는 문자(알파벳, 숫자, 밑줄 문자), *는 0개 이상의 반복

- (-)?(\d+)(\.\d*)? : 맨 앞에 음수가 있을 수 있고, 그 뒤에 1개 이상의 숫자가 필수, 그 뒤에 선택적으로 소수점과 0개 이상의 숫자가 오는 문자열 의미한다. 이 패턴에는 음수 부호, 소수점 앞의 숫자들, 소수 부분 세가지 그룹으로 나뉜다. 각 그룹은 괄호로 둘러싼다.

scala> import scala.util.matching.Regex

scala> val Decimal = new Regex("(-)?(\\d+)(\\.\\d*)?")
Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?

scala> val Decimal = new Regex("""(-)?(\d+)(\.\d*)?""")
Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?

scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r
Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?

package scala.runtime
import scala.util.matching.Regex

class StringOps(self: String) ... {
  ...
  def r = new Regex(self)
}

 

< 정규 표현식 검색 >

regex findFirstIn str : str 문자열 안에 regex 정규 표현식과 매치되는 가장 첫 번째 부분 문자열 검색

regex findAllIn str : str 문자열 안에 regex 정규 표현식과 매치되는 모든 문자열 반환

regex findPrefix str : str 문자열의 맨 앞 부분부터 검사해 정규 표현식 regex와 매치시킬 수 있는 접두사를 반환

scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r
Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?

scala> val input = "for -1.0 to 99 by 3"
input: java.lang.String = for -1.0 to 99 by 3

scala> for (s <- Decimal findAllIn input)
  | println(s)
-1.0
99
3

scala> Decimal findFirstIn input
res7: Option[String] = Some(-1.0)

scala> Decimal findPrefixOf input
res8: Option[String] = None

 

< 정규 표현식 뽑아내기 >

: 스칼라의 모든 정규 표현식은 익스트랙터를 정의한다. 익스트랙터를 사용해 정규 표현식 안의 그룹과 매치하는 부분 문자열을 구별할 수 있다.

scala> val Decimal = """(-)?(\d+)(\.\d*)?""".r
Decimal: scala.util.matching.Regex = (-)?(\d+)(\.\d*)?

scala> val Decimal(sign, integerpart, decimalpart) = "-1.23"
sign: String = -
  integerpart: String = 1
decimalpart: String = .23


scala> val Decimal(sign, integerpart, decimalpart) = "1.0"
sign: String = null                   // 어떤 그룹이 빠진 경우 해당 값은 null이 된다.
integerpart: String = 1
decimalpart: String = .0

+ Recent posts