: 패턴 매치의 생성자 패턴은 해당 클래스가 케이스 클래스이기 때문에 가능하다. 케이스 클래스는 만들고 싶지 않지만 생성자 패턴을 사용하고 싶고, 자신만의 패턴을 만들고 싶다면 익스트랙터를 사용한다.
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] 함수 타입 상속
: 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*)?
scala> val Decimal(sign, integerpart, decimalpart) = "1.0" sign: String = null // 어떤 그룹이 빠진 경우 해당 값은 null이 된다. integerpart: String = 1 decimalpart: String = .0
: 구현 트레이트는 타입 파라미터로 원소의 타입과 컬렉션이 표현하는 타입(List, Seq)을 지정한다.
package scala.collection
class TraversableLike[+Elem, +Repr] { def newBuilder: Builder[Elem, Repr] // deferred def foreach[U](f: Elem => U) // deferred ... def filter(p: Elem => Boolean): Repr = { val b = newBuilder foreach { elem => if (p(elem)) b += elem } b.result } }
: Repr은 Traversable의 서브 타입이 아닌 타입이 가능 => String이나 Array처럼 컬렉션 계층 구조에 없는 클래스도 컬렉션 구현 트레이트가 정의하는 모든 연산 사용 가능
: TraversableLike 트레이트는 newBuilder, foreach 추상 멤버가 존재하고 구체적인 컬렉션 클래스에서 정의한다. filter의 구현은 newBuilder와 foreach 추상 메소드를 사용하며 모든 컬렉션에 대해 동일하다. => newBuilder와 foreach만 구현하면 모든 컬렉션에 대해 filter을 사용할 수 있다.
: filter => newBuilder을 사용해 Elem 원소를 담고 Repr 컬렉션에 대한 빌더 생성 => 현재 컬렉션의 모든 원소를 foreach를 사용해 방문 => 원소 x가 술어를 만족하면 빌더에 추가 => 빌더의 result를 호출해 빌더에서 모은 원소들을 Repr 컬렉션 타입의 인스턴스로 반환
: map 연산의 경우 Array[String]이 Array[Int]로 원소의 타입이 변경되기 때문에 newBuilder와 foreach로 충분하지 않다.
=> newBuilder은 원래의 컬렉션과 같은 타입의 인스턴스만 만든다. map의 결과 타입에 따라 일일이 메소드를 정의하는 것도 힘들다. 결과 타입은 map에 들어가는 함수 타입에 의존적이다.
scala> Map("a" -> 1, "b" -> 2) map { case (x, y) => (y, x) } res3: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> a, 2 -> b)
scala> Map("a" -> 1, "b" -> 2) map { case (x, y) => y } res4: scala.collection.immutable.Iterable[Int] = List(1, 2)
: map을 제한해서 항상 같은 종류의 컬렉션을 반환하게 만들 수 있다. 하지만 제약을 가하면 리스코프 치환 법칙을 어겨 올바르지 않다.
Ex. Map은 Iterable이기도 하므로 Iterable에서 할 수 있는 일은 Map에서도 할 수 있어야 한다.
* 리스코프 치환 원칙 : U 타입의 값이 필요한 모든 경우를 T 타입의 값으로 대치할 수 있다면 T 타입을 U 타입의 서브타입으로 가정해도 안전하다.
=> 스칼라는 암시적 파라미터를 통한 오버로드를 사용해 문제를 해결
" TraversableLike의 map 구현 "
def map[B, That](p: Elem => B) (implicit bf: CanBuildFrom[Repr, B, That]): That = { // ConBuildFrom 타입의 빌더 팩토리 val b = bf(this) for (x <- this) b += f(x) b.result }
" CanBuildFrom 트레이트 "
package scala.collection.generic
trait CanBuildFrom[-From, -Elem, +To] { def apply(from: From): Builder[Elem, To] // 새로운 빌더를 만든다. }
: From 타입의 컬렉션을 받아서 Elem 타입의 원소를 갖는 컬렉션 To를 반환하는 빌더 생성
=> CanBuildFrom의 암시적 정의를 제대로 하면 map의 타입 변환을 필요에 따라 변경할 수 있다.
Ex. BitSet
: BitSet의 동반 객체에는 CanBuildFrom[BitSet, Int, BitSet]이 있다. BitSet에 대해 map 연산을 적용할 때 만들려는 결과 컬렉션의 원소 타입이 Int인 새 BitSet을 만들 수 있다. => 이를 만족시킬 수 없다면 다른 암시적 빌드 팩토리 시도 => 더 일반적인 mutable.Set의 동반 객체에 있는 CanBuildFrom[Set[_], A, Set[A]] 적용한다. A의 타입과 관계없이 다시 Set을 만들 수 있다.
: 가장 적당하면서 최대한 상세한 빌더를 찾는다.
scala> val xs: Iterable[Int] = List(1, 2, 3) xs: Iterable[Int] = List(1, 2, 3) // 정적 타입은 Iterable, 동적 타입은 List
scala> val ys = xs map (x => x * x) ys: Iterable[Int] = List(1, 4, 9) // map 결과 타입이 동적 타입에 매칭
: CanBuildFrom의 apply 메소드가 원래 컬렉션의 인자로 넘어가고 apply 호출을 genericBuilder에 있는 메소드에 넘긴다. genericBuilder 메소드는 실제 그 메소드가 정의된 컬렉션의 빌더를 호출한다.
=> 스칼라는 정적인 암시 파라미터 해결을 사용해 맵의 타입에 대한 제약을 해결하고 가상 디스패치를 사용해 이런 제약을 만족하는 가장 좋은 동적인 타입을 가져온다.
25.3 새 컬렉션 통합
: 새로운 컬렉션 클래스를 만들면서 기존에 정의된 컬렉션 연산이 새 타입 위에 잘 동작하도록 통합하는 예
< RNA 가닥을 표현하는 시퀀스 타입 >
: RNA 가닥은 A, T, G, U 염기의 시퀀스
" RNA 염기들 "
abstract class Base case object A extends Base case object T extends Base case object G extends Base case object U extends Base
object Base { val fromInt: Int => Base = Array(A, T, G, U) // 정수를 Base 값으로 바꾸는 배열 val toInt: Base => Int = Map(A -> 0, T -> 1, G -> 2, U -> 3) // Base 값을 정수로 바꾸는 맵 }
: RNA 가닥은 단지 Seq[Base] 이지만 네 가지 염기밖에 없기 때문에 2비트만을 사용해 염기를 구별 => 정수에는 2비트 값인 염기를 16개 저장 => Seq[Base]에 특화된 서브 클래스를 만들어 압축한 내부 표현 사용
final class RNA1 private (val groups: Array[Int], // 비트로 압축된 RNA 정보 val length: Int) extends IndexedSeq[Base] {
// length : 배열 안의 염기 개수, IndexedSeq에는 length, apply 추상 메소드 존재
import RNA1._
def apply(idx: Int): Base = { // 인덱스의 염기 반환 if (idx < 0 || length <= idx) throw new IndexOutOfBoundsException Base.fromInt(groups(idx / N) >> (idx % N * S) & M) } }
object RNA1 { private val S = 2 // 염기를 표현하는 비트 수
private val N = 32 / S // Int(32 bit)에 들어갈 그룹의 수
private val M = (1 << S) - 1 // 어떤 그룹만 떼어내기 위한 비트 마스크(11)
def fromSeq(buf: Seq[Base]): RNA1 = { val groups = new Array[Int]((buf.length + N - 1) / N) for (i <- 0 until buf.length) groups(i / N) |= Base.toInt(buf(i)) << (i % N * S) new RNA1(groups, buf.length) }
def apply(bases: Base*) = fromSeq(bases) }
: 생성자 비공개 => 클라이언트가 RNA 시퀀스의 내부 표현을 볼 수 없다. => 클라이언트 코드에는 영향을 주지 않으면서 표현을 바꿀 수 있다. => 동반 객체를 활용해 팩토리 메소드 제공
scala> val xs = List(A, G, T, A) xs: List[Product with Base] = List(A, G, T, A)
scala> RNA1.fromSeq(xs) res1: RNA1 = RNA1(A, G, T, A)
scala> val rna1 = RNA1(A, U, G, G, T) rna1: RNA1 = RNA1(A, U, G, G, T)
< RNA 메소드의 결과 타입 변환 >
scala> rna1.length res2: Int = 5
scala> rna1.last res3: Base = T
scala> rna1.take(3) res4: IndexedSeq[Base] = Vector(A, U, G)
: IndexedSeq에 IndexedSeq를 반환하는 take 메소드가 있고 IndexedSeq 구현이 Vector => RNA1 X
def apply(idx: Int): Base = { if (idx < 0 || length <= idx) throw new IndexOutOfBoundsException Base.fromInt(groups(idx / N) >> (idx % N * S) & M) }
override def foreach[U](f: Base => U): Unit = { var i = 0 var b = 0 while (i < length) { b = if (i % N == 0) groups(i / N) else b >>> S f(Base.fromInt(b & M)) i += 1 } } }
object RNA {
private val S = 2 private val M = (1 << S) - 1 private val N = 32 / S
def fromSeq(buf: Seq[Base]): RNA = { val groups = new Array[Int]((buf.length + N - 1) / N) for (i <- 0 until buf.length) groups(i / N) |= Base.toInt(buf(i)) << (i % N * S) new RNA(groups, buf.length) }
def apply(bases: Base*) = fromSeq(bases)
def newBuilder: Builder[Base, RNA] = new ArrayBuffer mapResult fromSeq
scala> m withPrefix "a" // "a"로 시작하는 접두사 맵 획득 res14: PrefixMap[Int] = Map((bc,0), (bd,1), (l,2), (ll,3))
" 패트리샤 트라이를 사용한 접두사 맵 구현 "
import collection._
class PrefixMap[T]
extends mutable.Map[String, T] with mutable.MapLike[String, T, PrefixMap[T]] {
// MapLike 구현 클래스 상속을 통해 filter 같은 메소드에서 올바른 타입의 결과를 얻고 쉽게 컬렉션 메소드를 사용할 수 있다.
var suffixes: immutable.Map[Char, PrefixMap[T]] = Map.empty // 문자에 해당하는 PrefixMap 값을 보내는 맵 var value: Option[T] = None // 해당 노드와 관련된 Option 값
def get(s: String): Option[T] = // s키를 가진 노드의 value 리턴 if (s.isEmpty) value else suffixes get (s(0)) flatMap (_.get(s substring 1))
def withPrefix(s: String): PrefixMap[T] = // 접두사 s로 시작하는 모든 하위 컬렉션 리턴 if (s.isEmpty) this else { val leading = s(0) suffixes get leading match { case None => suffixes = suffixes + (leading -> empty) case _ => } suffixes(leading) withPrefix (s substring 1) }