[코틀린] 코틀린 제네릭 클래스의 타입 특화 제약과 invoke를 통한 해결방법


코틀린 제네릭스의 타입 특화 제약과 invoke를 통한 해결 방법

제네릭스의 타입 특화 제약

코틀린에서는(자바에서도) 클래스 이름은 같지만 제네릭 타입 파라미터만 다른 클래스들을 정의할 수 없다.

$ cat Foo.kt
class FOO<T:Any> { fun doit() { println("Any") } }
class FOO<T:Number> { fun doit() { println("Number") } }
class FOO<T:Int> { fun doit() { println("Int") } }

$ kotlinc Foo.kt
Foo.kt:1:7: error: redeclaration: FOO
class FOO<T:Any> { fun doit() { println("Any") } }
      ^
Foo.kt:2:7: error: redeclaration: FOO
class FOO<T:Number> { fun doit() { println("Number") } }
      ^
Foo.kt:3:7: error: redeclaration: FOO
class FOO<T:Int> { fun doit() { println("Int") } }
      ^

스크립트에서는 클래스 재정의 에러는 안나지만(아마도 컴파일러가 스크립트 모드일 때는 줄마다 다른 패키지 안에 해당 클래스의 정의를 넣기 때문이 아닌가 싶다), 타입 소거 때문에 다양한 타입 파라미터를 가지는 가장 적합한 타입 파라미터를 못 찾아줘서 타입별 특화(specialization)가 안된다.

$ cat Foo.kts
class FOO<T:Any> { fun doit() { println("Any") } }
class FOO<T:Number> { fun doit() { println("Number") } }
class FOO<T:Int> { fun doit() { println("Int") } }

FOO<String>().doit()
FOO<Int>().doit()
FOO<Double>().doit()

$ kotlinc -script Foo.kts
Int
Int
Int
Test.kts:3:13: warning: 'Int' is a final type, and thus a value of the type parameter is predetermined
class FOO<T:Int> { fun doit() { println("Int") } }
            ^

함수 오버로딩 해소(overloading resolution)를 활용한 최적 타입 결정

이럴때는 함수 오버로딩을 사용하면 제네릭 함수의 타입을 잘 알아맞춰주는 것 같다. 오버로딩 해소시 함수 파라미터 타입이 가장 잘 들어맞는 함수를 사용하기 때문에 적절한 클래스 타입을 찾아줄 수 있다.

>>> fun <T1:Any, T2:Any> bar(v1:T1,v2:T2) { println("AA: ${v1 to v2}") }
>>> fun <T1:Number, T2:Any> bar(v1:T1,v2:T2) { println("NA: ${v1 to v2}") }
>>> fun <T1:Any, T2:Number> bar(v1:T1,v2:T2) { println("AN: ${v1 to v2}") }
>>> fun <T1:Number, T2:Number> bar(v1:T1,v2:T2) { println("NN: ${v1 to v2}") }
>>> bar(1,1)
NN: (1, 1)
>>> bar(1,"test")
NA: (1, test)
>>> bar("test","test")
AA: (test, test)
>>> bar("test",1)
AN: (test, 1)

invoke와 동반객체를 사용한 특화 타입 객체 생성 방법

동반객체 안에 invoke를 정의하면 객체 생성시 다양한 타입에 따라 특화시키는 일도 가능하다. 공통 기능을 인터페이스로 뽑아두고 이를 상속해 타입별로 특화된 기능을 제공할 수도 있다.

$ cat Foo2.kt
abstract class FOO {
  abstract fun doit()

  companion object {
    operator fun <T:Any> invoke(v:T) = object: FOO() { override fun doit() { println("Any: ${v}") } }
    operator fun <T:Number> invoke(v:T) = object: FOO() { override fun doit() { println("Number: ${v}") } }
    operator fun <T:Int> invoke(v:T) = object: FOO() { override fun doit() { println("Int: ${v}") } }
  }
}

fun main() {
  FOO("10").doit()
  FOO(10).doit()
  FOO(false).doit()
  FOO(10.0).doit()
}

$ kotlinc Foo2.kt -include-runtime -d Foo2.jar
$ java -jar Foo2.jar
Any: 10
Int: 10
Any: false
Number: 10.0

다만, 오버로딩이 이뤄질 수 없는 경우에는 이런 기법을 사용할 수 없다. 자바에서는 함수 반환 타입이 함수 시그니처에 들어가지 않기 때문에 반환 타입만 다른 오버로딩 함수를 정의할 수는 없다. 이럴때는 어쩔 수 없이 메서드 이름을 달리 하거나 가짜 인자를 도입해야 한다.

$ cat Foo3.kt
abstract class Foo3 {
  abstract fun doit()
  companion object {
    operator fun <T:Any> invoke() = object: Foo3() { override fun doit() { println("Any") } }
    operator fun <T:Number> invoke() = object: Foo3() { override fun doit() { println("Number") } }
    operator fun <T:Int> invoke() = object: Foo3() { override fun doit() { println("Int") } }
  }
}

$ kotlinc Foo3.kt
Foo3.kt:5:6: error: conflicting overloads: public final operator fun <T : Any> invoke(): ...
...

Related Posts

[프로그래밍기초]정적 타입 지정과 변성

정적 타입 지정과 변성에 대해 정리

[하스켈 기초][CIS194] 폴드와 모노이드

CIS194 7강 폴드와 모노이드에 대해 배운다

[하스켈 기초][CIS194] 다형성과 타입 클래스 연습문제 풀이

CIS194 5강 다형성과 타입 클래스 연습문제 풀이입니다.

[하스켈 기초][CIS194] 지연 계산

CIS194 6강 미리계산, 지연계산, 부수효과, 순수성에 대해 설명하고 패턴 매칭이 식을 평가하는 과정을 어떻게 이끄는지 살펴본 다음, 지연 계산이 프로그램 실행에 끼치는 영향에 대해 알아본다.

[하스켈 기초][CIS194] 다형성과 타입 클래스

CIS194 5강 다형성에 대해 설명하고 타입 클래스에 대해 설명한다. 연습 문제에서는 타입 클래스를 활용해 DSL을 편리하게 작성할 수 있음을 보여준다.

[하스켈 기초][CIS194] 고차 프로그래밍과 타입 추론 연습문제 풀이

CIS194 4강 고차 프로그래밍과 타입 추론 연습문제 풀이

[하스켈 기초][CIS194] 4강 - 고차 프로그래밍과 타입 추론

무명 함수(람다) 정의 방법을 알려주고, 함수 합성, 커링, 부분 적용, 폴드와 같은 고차 함수 프로그래밍에 대해 설명한다.

[하스켈 기초][CIS194] 재귀 연습문제 풀이

CIS194 3강 재귀 관련 연습문제 풀이

[하스켈 기초][CIS194] 재귀 패턴, 다형성, 프렐류드

실용적인 재귀 패턴을 설명하고 재귀 패턴을 추상화한 몇몇 함수를 정리한 다음, 하스켈 프렐류드에 대해 설명한다

[하스켈][팁] 하스켈 연산자 검색

하스켈 연산자를 검색하고 싶을 때 사용할 수 있는 도구 hoogle을 소개한다.