snu-sf-class/pp202302

enum의 case-specific type parameter 타입 명시

Closed this issue · 6 comments

과제 2-3 수행 중 의문이 들어 질문드립니다.

enum PEGFunc[V]:
  case FStr(s: String, f: String => V)
  case FCat[A, B, C](p1: PEGFunc[A], p2: PEGFunc[B], f: (A, B) => C)
      extends PEGFunc[C] // A, B, C are case-specific type parameters

FCat에서 A, B, C처럼 enum의 특정 case에 한정된 타입을 pattern matching 시에 명시할 수 있나요?

def mapPeg[V](pf: PEGFunc[V], s: String): IOption[V] = {
  def helper[V](p: PEGFunc[V], target: String): IOption[(String, V)] = {
    p match {
      case FStr(str, f) => ...
      case FCat(p1, p2, f) => ... // p1: PEGFunc[A] causes an error
    }
  }
}

p1의 타입을 PEGFunc[A] 이런 식으로 명시하면, type A를 찾을 수 없다고 에러가 납니다.

구현에 필수적인 사항은 아니지만, 타입을 명시하는 게 좋을 것 같아서 질문드립니다.

MerHS commented

FCat[A, B, C] 에서 A, B, C는 Type Parameter로, 일종의 변수명 설정입니다.
즉, case FCat[A,B,C](p1: PEGFunc[A], ...) 에서 FCat 오른쪽에 A, B, C라는 (타입) 변수 선언을 해주었기 때문에 오른쪽에 p1: PEGFunc[A] 에서 A를 사용할 수 있는 것입니다.

아래쪽 코드에서는 p1: PEGFunc[A]에서 A라는 이름의 (타입) 변수가 선언되지 않았기 때문에 type A를 찾을 수 없다고 나오는 것입니다.

굳이 사용하고 싶으시다면 def helper[V, A](...) 등으로 함수명 오른쪽에 타입 변수를 설정하시면 됩니다.

def helper[V, A, B, C] 그리고 case FCat[A, B, C]와 같이 수정하니까 다음과 같은 경고가 나옵니다.

match may not be exhaustive
It would fail on pattern case: PEGFunc.FCat(_, _, _)
the type test for pp202302.assign2.PEGFunc.FCat[A, B, C] cannot be checked at runtime because its type arguments can't be determined from pp202302.assign2.PEGFunc[V]

스칼라에서 공식적으로 지원되는 방법은 아닌 것 같은데 권장되지 않는다고 생각하면 될까요?

MerHS commented

에러 메시지의 말이 맞습니다.

Scala를 비롯한 JVM 위에서 돌아가는 언어는 FCat[A, B, C]과 같이 내부에 있는 타입 파라미터 A, B, C의 실제 타입이 뭔지에 대한 정보를 런타임때 가지고 있지 않습니다. (JVM의 Type Erasure 때문)

즉, 다음과 같은 데이터 타입이 있다고 하면 val a: HasList = ... 라고 HasList 값을 하나 만들어냈을 때, 실제 실행 시점에선 a 안의 리스트 값이 List[Int] 타입인지 List[Float] 타입인지 일반적으로 알 수 없습니다. 안에 있는 값의 실제 타입이 뭔지는 List 안에 있는 값 하나를 가져와서 isInstanceOf 등의 런타임 타입체킹 함수 등으로 검사를 해봐야 압니다.

enum HasList:
  case SomeList[V](t: List[V]) 

구체적으로 설명하자면, helper[V, A, B, C] 를 어떤 코드 라인에서 실제로 사용할 때, 그 사용 장소의 코드를 분석해서 V, A, B, C의 타입이 실제 뭔지를 확정하게 됩니다. 좀 더 자세하게 들어가자면, V, A, B, C가 Int, Int, Float, String이란걸 해당 사용 장소에서 추론해낼 수 있다면, C++ 같은 template 기반 제네릭의 경우, 일반적인 helper 함수를 사용하는게 아니라 helper_Int_Int_Float_String 이라는 함수를 컴파일 타임에 새로 만들어서 해당 함수를 사용하게 됩니다. JVM의 경우에는 하위호환 및 최적화 등을 위해 새로운 함수를 만들어내지 않고 타입 검사는 컴파일 시간에만 수행하고 실제 함수는 helper[Object, Object, Object, Object]로 들어오는 값이 전부 일반적인 값임을 가정하고 함수를 하나만 만들어냅니다.

이 경우, case FCat[A, B, C] 와 같은 매치를 실행하면 FCat[Int, Float, String] 타입의 값만 매치를 할 수 있으므로 모든 경우에 대해 패턴매치가 불가능하다고 나오는 것이고 (위쪽 오류), 실제 런타입에 매칭하는 FCat 타입의 값은 이것이 FCat[Int, Float, String]인지 아니면 다른 타입을 가지고 있는 별개의 FCat 타입인지 검사하는 것이 불가능하므로 아래 오류가 나게 됩니다.

좀 더 알고싶으시면 JVM 또는 일반적인 프로그래밍 언어의 Generic 타입 구현에 대한 여러 글들을 참조해보시기 바랍니다.

네. 친절한 답변 감사합니다. 다음과 같이 이해했는데, 마지막으로 정확한지 확인해 주시면 감사하겠습니다.

case FCat[A, B, C]와 같이 표현하면 타입을 명시하는 것이 가능하다. 하지만 enum PEGFunc[V]가 정적으로 V 타입으로만 매개화되어 있기 때문에 컴파일 타임에 타입 검사를 수행할 수 없다(첫 번째 오류). 그리고 Scala 타입 시스템의 한계(타입 소거로 인해 런타임에 타입 추출 불가)로 인해 런타임에 타입 검사를 수행할 수 없다(두 번째 오류). 따라서 패턴 매칭 시에 타입을 추론하는 것이 불가능하다. 다만, 타입 A, B, C에 무관하게 FCat이 잘 작동한다면, 이 해결책에 문제는 없다.

MerHS commented

첫번째 오류에 대한 이해는 살짝 다릅니다. helper[A, B, C] 안에서 case FCat[A, B, C]를 사용하면 정확히 FCat[A, B, C]만 매칭할 수 있으므로 나머지 FCat 타입에 대해서는 match 내부에서 case가 존재하기 않기 때문에 "모든 경우에 대해 case가 존재하지 않는다" (match may not be exhaustive) 라는 오류가 나는 것입니다. (설령 저기서 쓰인 FCat[A, B, C]가 인간 두뇌의 해석상으로는 모든 타입을 가려낸다고 해도, 이를 일반적으로 기계적으로 판별할 방법은 (아마) 없을 것입니다.)

두 번째 오류의 이해에 대해서는 대략 맞습니다. 다만 scala에서 warning이 뜬다는 것 자체가 타입적으로 문제가 있는 것이므로 가능하면 하지 마시기 바랍니다. (이 문제에 대해서 저렇게 타입을 명시화할 이유가 없습니다.)

네 이해했습니다. 감사합니다!