The Matchable 트레잇
The Matchable 트레잇 (The Matchable Trait)
패턴 매칭은 추상화를 우회하는 지름길로 쓰이곤 해요. 특히 불변 배열처럼 내부 표현을 감추고 싶은 타입이 있을 때 문제가 되죠. Scala 3의 Matchable 트레잇은 어떤 타입이 패턴 매칭의 대상이 될 수 있는지를 제어해서, 이런 구멍을 막는 역할을 합니다.
본문
새로운 트레잇 Matchable이 패턴 매칭할 수 있는 능력을 제어해요.
문제 (The Problem)
Scala 3 표준 라이브러리에는 불변 배열용 타입 IArray가 있어요. 다음과 같이 정의되죠:
opaque type IArray[+T] = Array[? <: T]
IArray 타입은 length와 apply용 확장 메서드를 제공하지만 update는 제공하지 않아요. 그래서 IArray 타입의 값은 갱신할 수 없어 보입니다.
그런데 패턴 매칭 때문에 잠재적인 구멍이 있어요. 이걸 보세요:
val imm: IArray[Int] = ...
imm match
case a: Array[Int] => a(0) = 1
이 테스트는 런타임에 성공해요. IArray가 런타임에는 Array로 표현되거든요. 하지만 이것을 허용한다면 불변 배열의 근본적인 추상화가 깨져 버려요.
참고: 캐스팅으로도 같은 효과를 낼 수 있어요.
imm.asInstanceOf[Array[Int]](0) = 1
그런데 이건 그렇게 큰 문제가 아니에요. Scala에서 asInstanceOf는 저수준이고 안전하지 않다고 이해되기 때문이에요. 반면 경고나 오류 없이 컴파일되는 패턴 매칭은 추상화를 깨뜨리면 안 됩니다.
또한 문제가 opaque 타입에만 한정된 게 아니라는 점도 기억해 두세요. 파라메트릭 타입 T의 값을 매치 선택자로 쓰는 다음 변형도 같은 문제로 이어져요:
def f[T](x: T) = x match
case a: Array[Int] => a(0) = 0
f(imm)
마지막으로, 이 문제가 opaque 타입에만 국한된 건 아니에요. 상한이 없는 타입 파라미터나 추상 타입도 패턴 매칭으로 분해될 수 없어야 합니다.
해결책 (The Solution)
패턴 매칭을 제어하는 새 타입 scala.Matchable이 있어요. 생성자 패턴 C(...)이나 타입 패턴 _: C의 패턴 매칭을 타이핑할 때, 선택자 타입이 Matchable에 적합할 것이 요구돼요. 그렇지 않으면 경고가 발행됩니다. 예를 들어 이 섹션 시작 부분의 예시를 컴파일하면 이런 경고가 나와요:
> sc ../new/test.scala -source future
-- Warning: ../new/test.scala:4:12 ---------------------------------------------
4 | case a: Array[Int] => a(0) = 0
| ^^^^^^^^^^
| pattern selector should be an instance of Matchable,
| but it has unmatchable type IArray[Int] instead
Scala 2에서의 마이그레이션과 Scala 2/3 크로스 컴파일링을 허용하기 위해, 이 경고는 -source future-migration 이상에서만 켜집니다.
Matchable은 Any를 부모 클래스로 가진 보편 트레잇(universal trait)이에요. AnyVal과 AnyRef 둘 다가 이를 확장해요. Matchable이 모든 구체 값·참조 클래스의 상위 타입이므로, 그런 클래스의 인스턴스는 예전처럼 매칭될 수 있어요. 그러나 다음 타입의 매치 선택자는 경고를 만들어 냅니다:
- 타입
Any: 패턴 매칭이 필요하면 그 대신Matchable을 써야 해요. - 상한이 없는 타입 파라미터와 추상 타입: 패턴 매칭이 필요하면
Matchable상한을 가져야 해요. - 어떤 보편 트레잇으로만 제한된 타입 파라미터와 추상 타입: 역시
Matchable을 상한으로 추가해야 해요.
여기 최상위 클래스와 트레잇의 계층 구조와 정의된 메서드를 보여드릴게요:
abstract class Any:
def getClass
def isInstanceOf
def asInstanceOf
def ==
def !=
def ##
def equals
def hashCode
def toString
trait Matchable extends Any
class AnyVal extends Any, Matchable
class Object extends Any, Matchable
Matchable은 현재 메서드가 없는 마커 트레잇이에요. 시간이 지나면서 getClass와 isInstanceOf 메서드를 여기로 옮길 수도 있는데, 이 둘은 패턴 매칭과 밀접한 관련이 있기 때문이에요.
Matchable과 보편 동등성 (Matchable and Universal Equality)
Any 타입의 선택자에 대해 패턴 매칭을 하는 메서드는 Matchable 경고가 켜지면 캐스트가 필요해요. 가장 흔한 그런 메서드가 보편 equals 메서드죠. 다음 예시처럼 써야 합니다:
class C(val x: String):
override def equals(that: Any): Boolean =
that.asInstanceOf[Matchable] match
case that: C => this.x == that.x
case _ => false
that을 Matchable로 캐스팅하는 것은, 추상 타입과 opaque 타입이 존재할 때 보편 동등성이 안전하지 않다는 사실을 나타내요. 타입의 의미와 그 표현을 제대로 구분할 수 없기 때문이죠. Any와 Matchable 둘 다 Object로 이레이즈되므로, 이 캐스트는 런타임에 성공함이 보장됩니다.
예를 들어 다음 정의를 생각해 봅시다.
opaque type Meter = Double
def Meter(x: Double): Meter = x
opaque type Second = Double
def Second(x: Double): Second = x
여기서 보편 equals는 다음에 대해 true를 돌려줘요.
Meter(10).equals(Second(10))
수학적으로는 분명히 거짓인데도 말이죠. 다중 보편 동등성(multiversal equality)을 쓰면 다음을 타입 오류로 만들어 그 문제를 어느 정도 완화할 수 있어요.
import scala.language.strictEquality
Meter(10) == Second(10)