합집합 타입

합집합 타입 (Union Types)

타입에 | 연산자를 쓰면 이른바 **합집합 타입(union type)**이 만들어져요. A | B 타입은 A 타입이거나 B 타입인 값을 나타내죠.

다음 예제에서 help 메서드는 Username | Password라는 합집합 타입의 id 파라미터를 받는데, 그 값은 Username이거나 Password일 수 있어요:

case class Username(name: String)
case class Password(hash: Hash)

def help(id: Username | Password) =
  val user = id match
    case Username(name) => lookupName(name)
    case Password(hash) => lookupPassword(hash)
  // more code here ...

help 메서드는 패턴 매칭으로 두 대안을 구분해서 구현해요.

이 코드는 유연하고 타입 안전한 해법이에요. Username이나 Password 외의 타입을 넘기려 하면 컴파일러가 오류로 표시하죠:

help("hi")   // error: Found: ("hi" : String)
             //        Required: Username | Password

match 표현식에 Username이나 Password 타입과 맞지 않는 케이스를 추가하려 해도 오류가 나요:

case 1.0 => ???   // ERROR: this line won’t compile

출처: Scala 3 Book

본문

합집합 타입의 대안

앞서 봤듯이 합집합 타입은 서로 다른 여러 타입의 대안을 나타내는 데 쓸 수 있어요. 그 타입들이 직접 짠 클래스 계층의 일부일 필요도, 명시적인 감싸기(wrapping)가 필요하지도 않죠.

클래스 계층을 미리 설계하는 방법

합집합 타입이 없다면 클래스 계층을 미리 설계해야 해요. 다음 예제가 그걸 보여줘요:

trait UsernameOrPassword
case class Username(name: String) extends UsernameOrPassword
case class Password(hash: Hash) extends UsernameOrPassword
def help(id: UsernameOrPassword) = ...

미리 설계하는 방식은 잘 확장되지 않아요. 예를 들어 API 사용자의 요구사항을 미리 예측할 수 없는 경우가 있으니까요. 게다가 UsernameOrPassword 같은 마커 트레이트로 타입 계층을 어지럽히는 것은 코드를 읽기 더 어렵게 만들어요.

태그된 합집합(Tagged Unions)

또 다른 대안은 다음과 같은 별도의 enumeration 타입을 정의하는 거예요:

enum UsernameOrPassword:
  case IsUsername(u: Username)
  case IsPassword(p: Password)

enum UsernameOrPasswordUsernamePassword의 태그된 합집합을 나타내요. 하지만 이런 방식으로 합집합을 모델링하려면 명시적인 감싸기와 풀기가 필요하고, 예를 들어 UsernameUsernameOrPassword의 하위 타입이 아니에요.

합집합 타입의 추론

컴파일러는 표현식에 합집합 타입이 명시적으로 주어졌을 때만 그 타입을 할당해요. 예를 들어 이런 값들이 있다고 할게요:

val name = Username("Eve")     // name: Username = Username(Eve)
val password = Password(123)   // password: Password = Password(123)

이 REPL 예제는 if/else 표현식의 결과를 변수에 바인딩할 때 합집합 타입을 쓸 수 있음을 보여줘요:

scala> val a = if true then name else password
val a: Object = Username(Eve)

scala> val b: Password | Username = if true then name else password
val b: Password | Username = Username(Eve)

a의 타입은 Object인데, 이는 UsernamePassword의 슈퍼타입이지만 최소 슈퍼타입(Password | Username)은 아니에요. 최소 슈퍼타입을 원한다면 b에서처럼 명시적으로 주어야 합니다.

합집합 타입은 교집합 타입(intersection type)의 쌍대(dual)예요. 그리고 교집합의 &처럼, 합집합의 |도 교환 법칙이 성립해요: A | BB | A와 같은 타입입니다.

더 알아보기

  • 교집합 타입(intersection types)에 대한 설명은 "Intersection Types" 페이지에서 다룹니다.
  • 패턴 매칭은 "Control Structures" 페이지에서 더 자세히 볼 수 있어요.