Scala 3 Given 두번째 - 컨텍스트 파라미터와 Given을 활용한 스칼라의 다형성 구현
View: 129
0
0
작성자: 코딩!제이크
카테고리: Scala Language
발행: 2024-10-21
수정
2024-12-21
이 글은 Programming in Scala Fifth Edition의 Given에 대해 Study 한 내용을 좀 더 자세히 풀어서 설명한 글입니다.
시작합니다.
컨텍스트 파라미터(Context Parameter)는 앞에 명시적으로 작성된 파라미터 목록에 대한 타입 관련 정보를 제공하는 목적으로 많이 사용됩니다.
스칼라에서 함수를 작성할 때 ad hoc polymorphism (특정 타입에 대한 다형성)을 얻기 위해 꼭 필요한 방법입니다.
작성한 함수가 적절한 타입에 대해서는 잘 적용될 수 있으나, 다른 타입에 대해서는 컴파일이 되지 않을 수 있습니다.
이를 해결하기 위해 컨텍스트 파라미터를 사용해서 타입에 따른 정보를 추가로 제공해 줍니다.
다음 예시를 보겠습니다.
def isort(xs: List[Int]): List[Int] =
if xs.isEmpty then Nil
else insert(xs.head, isort(xs.tail))
def insert(x: Int, xs: List[Int]): List[Int] =
if xs.isEmpty || x <= xs.head then x :: xs
else xs.head :: insert(x, xs.tail)
위의 코드는 잘 동작합니다. 앞에서 차례대로 그 크기를 비교해서 정렬을 시킵니다.
다음 코드는 Generic을 사용해서 코드를 변경했습니다.
def isort[T](xs: List[T]): List[T] =
if xs.isEmpty then Nil
else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T]): List[T] =
if xs.isEmpty || x <= xs.head then x :: xs
else xs.head :: insert(x, xs.tail)
isort 함수는 크게 문제 없어 보입니다. 그러나 insert 함수에서 x <= xs.head
부분에서 문제가 발생합니다.
타입이 Int인 앞의 예제에서는 <=
(less than or equal to) 가 Int 타입에 정의가 되어 있기 때문에 정상적으로 동작합니다.
하지만 타입 파라미터 T를 사용했을 때에는 <=
가 타입 T의 멤버인지 보장할 수 없기 때문에 컴파일 에러가 발생하게 됩니다.
이를 해결하기 위해 타입 파라미터 T에 대한 추가 정보, 이를 테면 두 데이터를 비교하는 함수 리터럴과 같은 정보 제공이 필요합니다.
def isort[T](xs: List[T])(lteq: (T, T) => Boolean): List[T] =
if xs.isEmpty then Nil
else insert(xs.head, isort(xs.tail)(lteq))(lteq)
def insert[T](x: T, xs: List[T])(lteq: (T, T) => Boolean): List[T] =
if xs.isEmpty || lteq(x, xs.head) then x :: xs
else xs.head :: insert(x, xs.tail)(lteq)
이번 예제에서는 <=
에 대한 문제를 해결하기 위해 lteq라는 함수 리터럴에서 T 타입끼리 비교하는 함수형 파라미터를 만들었습니다.<=
대신에 lteq
라는 helper 함수를 사용해서 T 타입의 두 원소를 비교하도록 했고, 실제 비교 메커니즘은 isort 함수를 호출하는 사용자가 제공합니다.
Integer, String, Rational 타입에 대해 각각 다음과 같이 isort를 호출해서 사용할 수 있습니다.
// 1) Integer Type - Result: List(-10, 4, 10)
isort(List(4, -10, 10))((x: Int, y: Int) => x <= y)
// 2) String Type - Result: List(apple, blackberry, cherry, pear)
isort(List("cherry", "blackberry", "apple", "pear"))((x: String, y: String) => x.compareTo(y) <= 0)
// 3) Rational (분수) Type - Result: List(1/2, 5/6, 7/8)
isort(List(Rational(7, 8), Rational(5, 6), Rational(1, 2)))((x: Rational, y: Rational) => x.numer * y.denom <= x.denom * y.numer)
예제에서 함수 리터럴의 x, y 타입은 컴파일러가 앞의 파라미터 목록을 통해 추정 가능하기 때문에 생략 할 수 있습니다. 앞 선 파라미터 목록에 대해 동일한 타입의 값을 비교하는 추가 정보를 호출 시에 제공해 주었기 때문에 코드가 정상 동작하는 것을 확인할 수 있습니다. 그러나 만약에 각 타입에 대한 비교 방법이 일정하고 호출 시마다 매번 바꾸어 줄 필요가 없다면, 매 실행 때마다 값을 비교하기 위한 함수 리터럴을 제공하는 것은 매우 소모적이고 불필요한 일이 아닐 수 없습니다. 따라서 함수 리터럴이 제공되는 파라미터를 컨텍스트 파라미터로 만들어주어 불필요한 중복 코드를 제거합니다.
// 비교를 위한 trait를 작성합니다.
trait Ord[T]:
def compare(x: T, y: T): Int
def lteq(x: T, y: T): Boolean = compare(x, y) < 1
// isort 함수를 컨텍스트 파라미터를 적용해서 재 작성합니다. using 키워드를 사용합니다.
def isort[T](xs: List[T])(using ord: Ord[T]): List[T] =
if xs.isEmpty then Nil
else insert(xs.head, isort(xs.tail))
def insert[T](x: T, xs: List[T])(using ord: Ord[T]): List[T] =
if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs
else xs.head :: insert(x, xs.tail)
given을 사용해서 컨텍스트 파라미터를 정의합니다. given으로 정의된 객체들은 using 키워드를 써서 Context parameter로 제공되는 객체의 Companion object에 주로 작성됩니다. 예를 들어 given으로 Ord[Int]를 정의한다면 Ord 또는 Int 둘 중 하나의 Companion object에 작성됩니다. Int의 Companion object는 우리가 임의로 바꿀 수가 없으니 Ord의 Companion object에 작성할 수 있습니다. 컴파일러는 먼저 호출한 코드의 Scope 내에서 given 객체를 찾고, 그 다음에는 Context parameter가 사용된 Scope 내에서 찾으며, 마지막으로 Companion object에서 given 객체를 찾습니다.
예시로 Int 타입과 String 타입에 대해서 작성하겠습니다.
첫 번째 예제는 equal mark (=)을 중심으로 객체라는 값(value)에 given 키워드가 적용된 intOrd
라는 이름(name)이 주어지기 때문에 alias givens
로 불립니다. alias givens는 given 객체 선언에 대한 original 형태이며, 두 번째 예제와 같이 스칼라에서 제공하는 shorthand syntax를 적용해서 간소화 할 수 있습니다.
// Original 형태인 alias givens 형태로 작성
object Ord:
given intOrd: Ord[Int] =
new Ord[Int]:
def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then 1 else -1
// shorthand syntax인 with 키워드를 통해 `= new Ord[Int]:`을 생략
// Int 타입과 String 타입
object Ord:
given intOrd: Ord[Int] with
def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then 1 else -1
given stringOrd: Ord[String] with
def compare(s: String, t: String) = s.compareTo(t)
마지막으로 Context Parameter에 대해서 term inference
를 통해 given 선언(declaration)을 더욱 간단하게 나타낼 수 있습니다.
term inference란 암묵적으로 사용되는 값이나 파라미터를 컴파일러가 자동으로 찾아 대입해 주는 기능으로 given 선언문에서는 타입만 제공하면 해당 term을 자동으로 구성해주어 타입이 필요한 곳에 암묵적으로 그 term을 사용하게 합니다. 여기서 term은 값의 이름, 통상적으로 변수명을 의미하며 intOrd와 stringOrd를 의미합니다. 이와 같이 이름이 생략된 given을 Anonymous givens라고 하며, 위의 Ord[Int]와 Ort[String]에 대해 다음과 같이 간단히 나타낼 수 있습니다.
Object Ord:
given Ord[Int] with
def compare(x: Int, y: Int) =
if x == y then 0 else if x > y then -1 else 1
given Ord[String] with
def compare(s: String, t: String) = -s.compareTo(t)
지금까지 Parameterized given type과 Anonymous givens에 대해 알아보았습니다.
다소 어려워 보이지만
- 매개변수를 암묵적으로 사용하기 위해 using 키워드를 사용해서 컨텍스트 파라미터를 지정하고, given을 사용해서 이를 제공할 타입 인스턴스를 정의한다.
- given 타입을 정의하기 위해 original 형식인 alias given에서 shorthand syntax with와 Anonymous givens를 사용해서 더욱 간략히 나타낼 수 있다.
로 정리하면 될 것 같습니다.
다음 글에서는 이번 글에서 다루었던 코드에 대해 객체 지향 관점에서 타입클래스와 다형성에 대해 좀 더 자세히 살펴보겠습니다. 감사합니다.
