Scala 3 Given 세번째 - ad hoc polymorphism (애드혹 다형성)을 위한 타입클래스

View: 91 0 0
작성자: 코딩!제이크
카테고리: Scala Language
발행: 2024-12-22 수정 2024-12-24

스칼라 Given 세 번째 이야기 입니다.

오늘은 좀 더 깊이 있는 주제를 다루려고 합니다. 바로 다형성에 대한 내용입니다.

일반적으로 프로그래밍 언어에서 다형성(polymorphism)은 크게 세 가지 범주로 나눌 수 있습니다.

  1. 파라메트릭 다형성 (Parametric Polymorphism)
    함수나 자료구조가 구체적인 타입에 의존하지 않고, 타입 파라미터를 통해 다양한 타입에 대해 동일한 로직으로 적용할 수 있게 하는 다형성으로 제네릭(generics)을 사용하는 컬렉션이 대표적인 예입니다. 예를 들면 List[T]는 어떤 타입 T에 대해서도 동일한 방식으로 동작하는 파라메트릭 다형성의 대표적인 자료구조입니다.

  2. 서브타이핑 다형성 (Subtyping Polymorphism)
    객체 지향 언어에서 흔히 볼 수 있는 형태로, 상위 타입을 기대하는 곳에 그 하위 타입의 인스턴스를 전달하는 방식입니다. 클린 코드의 저자 Robert C. Martin, 일명 Uncle Bob으로 불리는 분이 정리한 SOLID 개념 중 L에 해당하는 Liskov Substitution Principle에 해당하는 형식입니다. 예를 들면 Animal 타입을 인자로 받는 함수에 Animal의 하위 타입인 Dog나 Cat 인스턴스를 넘길 수 있습니다. 주로 트레이트 믹스인(mixin), 인터페이스 또는 클래스의 상속을 통해 구현되며, 런타임 때 실제 인스턴스 타입에 따라 메서드가 동적으로 전달됩니다.

  3. Ad-Hoc 다형성 (Ad-hoc Polymorphism)
    동일한 이름의 함수나 연산자를 타입에 따라 다르게 구현해서 동일한 기능을 하도록 하는 형태입니다. 일반적으로 오버로딩(overloading)이 대표적인 예로, 같은 함수 이름을 갖더라도 매개변수 타입에 따라 구현을 달리 가져갈 수 있습니다. 보다 발전된 형태로 이 글에서 다루고자 하는 타입클래스(Typeclass)를 통한 구현 방법이 있습니다. 타입클래스는 동일한 기능을 가지는 클래스들의 묶음(class)을 의미하며, 특정 타입에 대한 동작을 외부에서 정의해 주어 해당 타입에 맞는 구현을 컴파일 타임에 정적으로 결합해서 사용합니다.

타입클래스(Typeclass)

타입클래스는 함수형 프로그래밍 언어, 특히 하스켈(Haskell)에서 널리 알려진 개념으로, Sorting과 같이 동일한 기능을 가지고 있는 특정 연산을 수행할 수 있는 타입들의 집합이며, 서로 다른 타입에 대해서 동일한 기능을 수행하도록 연산자 또는 함수를 구현해야 하기 때문에 타입 별로 그 구현이 다를 수 있습니다.

이전 글에서 우리는 다음과 같은 예제를 살펴보았습니다.

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)

우리는 isort 함수를 통해 어떤 타입이든 given Ord[T]를 구현하면 sorting 기능을 제공할 수 있습니다. 마치 sortable supertype을 가지고 있는 subtype 그룹처럼 보입니다. 다시 말하면, 서로 다른 타입에 대해서 Ord[T]를 통해 동일한 기능을 하는 연산자 또는 함수를 구현할 수 있고, 이를 통해 동일한 기능을 수행할 수 있는 타입들의 집합을 만들 수 있고 이것이 바로 타입클래스입니다.

스칼라 스탠다드 라이브러리에서는 equality를 정의하거나 sorting 시에 요소들의 순서를 결정하는 등 다양한 목적을 위한 타입클래스가 제공됩니다. Ord typeclass는 Scala.math.Ordering typeclass를 부분적으로 다시 구현한 것입니다. 스칼라 라이브러리는 Int와 String과 같은 일반 타입에 대해 Ordering typeclass 객체를 정의합니다.

Context Parameter에 Parameter 이름을 생략하고 isort 예제를 아래와 같이 다시 작성할 수 있습니다.

def isort[T](xs: List[T])(using Ordering[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: Ordering[T]): List[T] =
  if xs.isEmpty || ord.lteq(x, xs.head) then x :: xs
  else xs.head :: insert(x, xs.tail)

이 예제에서는 isort 함수의 Context parameter에 파라미터 이름을 사용하지 않고 바로 파라미터 타입만 표시되어 있습니다. 이러한 형식의 파라미터를 익명 파라미터 (anonymous parameter)라고 부르며, isort 함수에서 insert에 암묵적으로 파라미터를 전달하고 있기 때문에 이름이 필요하지 않습니다. 그러나 insert 함수에서는 ord.lteq를 호출하기 때문에 ord로 이름이 지정되어 있습니다.

다른 예를 하나 더 들어 보겠습니다.
orderedMergeSort 메소드가 다음과 같이 구현되어 있습니다.

def orderedMergeSort[T <: Ordered[T]](xs: List[T]): List[T] =
  def merge(xs: List[T], ys: List[T]): List[T] =
    (xs, ys) match
      case (Nil, _) => ys
      case (_, Nil) => xs
      case (x :: xs1, y :: ys1) =>
        if x < y then x :: merge(xs1, ys)
        else y :: merge(xs, ys1)

  val n = xs.length / 2
  if n == 0 then xs
  else
    val (ys, zs) = xs.splitAt(n)
    merge(orderedMergeSort(ys), orderedMergeSort(zs))

이 예제에서 orderedMergeSort 메소드는 Ordered[T]의 Subtype인 T 타입의 List에 대해서 Sorting이 가능합니다. 이와 같은 형태의 다형성을 서브타이핑 다형성 (subtyping polymorphism)이라고 부르며, 본 예제에서 Ordered[T]의 upper bound를 사용해서 구현하였습니다. 이와 같은 서브타이핑 다형성은 작성자가 구현한 상위 타입을 상속한 객체에 한해서 정상적으로 동작하지만, 반대로 스칼라의 기본 객체들은 이 상위 타입을 상속 받을 수 없기 때문에 이 메소드를 사용할 수 가 없습니다. 따라서 Int, String과 같은 스칼라 기본 타입에 대해서는 애드혹 다형성을 구현하여 given을 통해 마치 서브타이핑 다형성과 같은 효과를 얻을 수 있습니다. 다시 말하면, Int 타입을 Ordered[Int] 타입으로 확장할 수는 없지만 given Ordering[Int]를 제공함으로 기존의 T 타입의 Hierachy와 구분된 새로운 Hierachy를 임시로 만들 수 있습니다. 이는 다음 예제와 같이 구현이 가능합니다.

def msort[T](xs: List[T])(using ord: Ordering[T]): List[T] = 
  def merge(xs: List[T], ys: List[T]): List[T] =
    (xs, ys) match
      case (Nil, _) => ys
      case (_, Nil) => xs
      case (x :: xs1, y :: ys1) =>
        if ord.lt(x, y) then x :: merge(xs1, ys)
        else y :: merge(xs, ys1)

  val n = xs.length / 2
  if n == 0 then xs
  else
    val (ys, zs) = xs.splitAt(n)
    merge(msort(ys), msort(zs))

애드혹 다형성을 구현한 isort와 msort 함수 모두 앞에서 정의한 파라미터에 대해서 추가적인 정보를 제공하기 위해 컨텍스트 파라미터 (context parameter)를 사용하고 있습니다. 즉, Ordering[T] 타입의 컨텍스트 파라미터 ord는 T 타입의 원소들을 정렬하는 방법을 제공합니다. 그리고 List[T]에서의 타입 T는 앞서 정의한 파라미터인 xs의 타입이고 xs는 항상 명시적으로 제공될 것이기 때문에 컴파일러는 컴파일 타임에서 xs를 통해 T의 타입을 알 수 있고 given으로 정의된 Ordering[T]가 가용한지를 판단합니다. 만약 가용하다면 이후 부터는 암묵적으로 컨텍스트 파라미터를 제공하게 됩니다.

마무리

Scala 3의 Given에 대해 3회에 걸쳐 알아보았습니다.

첫 번째 글 Scala3 Given 첫번째 - 기본 사용법에서 given/using 키워드의 도입 배경과 간단한 사용법에 대해서 알아 보았고,
두 번째 글 Scala 3 Given 두번째 - 컨텍스트 파라미터와 Given을 활용한 스칼라의 다형성 구현에서 예제를 통해 given 키워드를 사용해서 컨텍스트 파라미터를 작성하는 과정을 설명하였으며,
세 번째이자 마지막인 이번 글에서 객체지향 언어로서의 스칼라의 다형성에 대한 소개와 애드혹 다형성을 given 객체를 통해 구현하는 방법을 소개하였습니다.

특히 이 번 글에서 스칼라의 다형성은 크게 파라메트릭 다형성(제네릭), 서브타이핑 다형성(상속과 트레이트), 그리고 애드혹 다형성(오버로딩, 타입클래스)으로 구분되며, given을 사용해서 특정 타입이 공통으로 수행해야 할 연산(정렬, 동일성 비교 등)을 외부에 정의하여 타입클래스를 구현할 수 있으며, 타입클래스를 활용하여 애드혹 다형성을 구현할 수 있다는 것을 설명하였습니다. 또한 필요한 경우에 using 을 사용한 컨텍스트 파라미터를 통해 이를 함수에 "주입"해서 암묵적으로 활용할 수 있으며, 스칼라 기본 타입에서 서브타이핑 다형성을 대체할 수 있는 효과를 얻을 수 있습니다.

comments 0