왜 Vector는 ::로 요소를 추출할 수 없는 걸까? Java / Scala / Groovy

스칼라 문법의 주요 특징 중 하나는 패턴 매칭이다. 언어에 익숙한 사용자라면 리스트(List)의 각 요소를 다음과 같은 방식으로 추출할 수 있다는 것을 알 것이다.

val a :: b :: Nil = Seq(1, 2)

위 코드에서 a와 b에는 각각 1, 2가 담긴다. 만일 할당할 시퀀스가 이보다 크다면 MatchError가 발생한다. 이것을 패턴 매칭에서도 그대로 사용할 수 있기에 다음과 같은 코드가 가능하다.

val targets: Seq[File] = Seq(...)
val extension: String = targets match {
  case head :: Nil =>
    head.getExtension
  case _ =>
    "zip"
}

코드의 의도는, targets를 사용자가 선택한 파일들이라 가정한 뒤, 선택된 파일이 1개이면 그 파일의 확장자를, 아니면 "zip"이라는 확장자 extension에 할당하는 것이다. (다른 좋은 방법들이 있겠지만, 예시니까 이해하자) 그런데 여기에는 함정이 있다. 모든 Seq 유형이 ::를 이용한 분해를 지원하는 건 아니라는 사실이다. 다음 코드는 이전의 코드와 targets를 할당하는 방식만 다르고 모든 것이 동일하다.

val targets: Seq[File] = for (i <- 1 to files.length) yield { ... }
val extension: String = targets match {
  case head :: Nil =>
    head.getExtension
  case _ =>
    "zip"
}

Scala 2.11 기준으로, 이 코드의 결과는 targets의 길이에 상관없이 "zip"이다. 그 얘기인즉, "case head :: Nil =>" 로 시작되는 PartialFunction[Any, Unit] 유형의 부분함수에 isDefinedAt(targets) 메서드를 호출했을 때 결과가 false라는 뜻이다. 이유는, 이 코드에서 for-yield 문의 실제 반환형이 Vector이며 이것은 Seq를 구현하지만 ::를 지원하지 않기 때문이다. 

scala> val a :: b :: Nil = Seq(1,2)
a: Int = 1
b: Int = 2

scala> val a :: b :: Nil = Vector(1,2)
<console>:13: error: constructor cannot be instantiated to expected type;
 found   : scala.collection.immutable.::[B]
 required: scala.collection.immutable.Vector[Int]
       val a :: b :: Nil = Vector(1,2)
             ^

::를 지원한다는 건 무엇일까? 에러 메시지로부터 알 수 있는 흥미로운 사실은, ::가 키워드가 아니라 유형이라는 점이다. 이것은 scala.collection.Immutable 패키지의 List.scala에 있는데, 정의는 다음과 같다.

final case class ::[B](override val head: B, private[scala] var tl: List[B]) extends List[B] {
  override def tail : List[B] = tl
  override def isEmpty: Boolean = false
}

::는 케이스 클래스이기 때문에 다음처럼 사용이 가능하다. (REPL에서 콜론은 예약어이므로 `를 붙였다.)

scala> `::`(1, List(2))
res14: scala.collection.immutable.::[Int] = List(1, 2)

scala> val `::`(a, _) = `::`(1, Nil)
a: Int = 1

scala> val `::`(a, `::`(b, _)) = `::`(1, `::`(2, Nil)) // deep match
a: Int = 1
b: Int = 2

그런데 사실, 바로 위 코드의 ::(a, ::(b, _))와 처음에 썼던 'a :: b :: Nil'은 동일한 기능을 한다. 둘다 스칼라가 지원하는 생성자 패턴(constructor pattern)이기 때문이다. 즉 케이스 클래스로부터 변수들을 다음과 같이 추출할 수 있다.

scala> case class A(a:Int, b:Int)
defined class A

scala> val a A b = A(1,2)
a: Int = 1
b: Int = 2

다소 생소하긴 하지만, 덕분에 ::라는 케이스 클래스를 만들고 val a :: b = ... 와 같은 생성자 패턴을 통해 변수들을 추출하는 것이 가능하다. 정리하면, 스칼라 컬렉션 패키지에 기본 내장된 :: 케이스 클래스는 List 유형을 대상으로 만들어졌기 때문에 Vector에는 사용할 수 없다. 또 그렇기 때문에 Vector.toList를 호출해 벡터를 리스트로 변환하면 문제없이 사용이 가능하다. 보통 List를 직접 쓰기보다는 Seq를 사용하기 때문에(Seq.apply의 반환형은 Seq지만, 반환되는 값의 실제 유형은 List다) a :: b 와 같은 생성자 패턴의 매칭 또한 별 생각없이 시도하다가 실수하기 쉽다. 특히 부분함수 중에 case _ => 와 같은 완전함수가 존재한다면, 오류가 존재하는 것을 모르고 지나칠 수 있기 때문에 주의가 필요하다.

참고
Tag :
, , ,

Leave Comments