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에 대해 알아보았습니다.
다소 어려워 보이지만

  1. 매개변수를 암묵적으로 사용하기 위해 using 키워드를 사용해서 컨텍스트 파라미터를 지정하고, given을 사용해서 이를 제공할 타입 인스턴스를 정의한다.
  2. given 타입을 정의하기 위해 original 형식인 alias given에서 shorthand syntax with와 Anonymous givens를 사용해서 더욱 간략히 나타낼 수 있다.

로 정리하면 될 것 같습니다.

다음 글에서는 이번 글에서 다루었던 코드에 대해 객체 지향 관점에서 타입클래스와 다형성에 대해 좀 더 자세히 살펴보겠습니다. 감사합니다.

comments 0