Scala 의 Type Class 코드로 살펴보기 1

language | 29 June 2017

Tags | scala typeclass

이번 글에서는 scala 의 Type class 에 대해서 코드와 함께 알아보도록 하겠습니다.

Type class 를 이용하면

기존의 object oriented programming 에서 사용하던

상속이나 객체, 클래스의 개념 없이도 다양한 기능을 구현할 수 있습니다.

(개인적인 소감으로는, Object Oriented Programming 의 소중함을 느끼게 해주는 공부였습니다…)




1. Type Class 의 구성




  1. type class 본체

  2. type class 의 instance (customizing)

  3. type class 의 interface (함수 사용)

Type class 는 크게 위와같은 3가지 구성으로 이루어져 있습니다 .


Type class 본체abstract class 또는 trait로 만듭니다.

자세한 메서드의 구현은 생략하고, 공통된 동작만 기술되어 있습니다.


Type class 의 instance 는 Type class 의 본체에 특정 Type 을 대입하여 구체화시켰다고 생각할 수 있습니다.

Type class 의 인터페이스(interface) 에 특정 Type 이 들어왔을 때 어떤 동작을 할 것인지 구체적으로 Type Class 의 인스턴스로 표현 할 수 있습니다.

implicit def, 또는 implicit val 로 많이 표현합니다. 자세한 예는 아래서 다루도록 하겠습니다.


Type Class 의 interface는 두 가지 파라메터(인자)를 받습니다.

하나는 실제 함수에서 다룰 데이터이고, 하나는 데이터를 처리하는 동작이 기술되어있는 Type class 의 instance 입니다.




2. Type Class 개요




기본적으로 type class 는 type 을 parameter로 받는 class 입니다.

이제 이 아래부터는 실제 스칼라 코드를 통해서 여러가지 Type Class 와 그의 인스턴스, 인터페이스들을 구현해보도록 하겠습니다.




3. 하나의 type parameter 를 받는 Type class




part1 : type class 의 본체


Ord[A] 는 type A 의 무언가를 크기비교하는 기능을 가진 type class 입니다.

Type Class 는 간단히 abstract class 또는 traits를 통해 구현합니다.

메서드는 정의하지만 정확한 동작은 인스턴스에서 구현합니다.

abstract class Ord[A]{
  def cmp(me : A, you : A) : Int  // instnace 에서 구현될 것입니다.

  //A의 type 과 관계 없는 공통된 메서드들은 미리 작성해놓기도 합니다.   
  def ===(me : A, you : A) : Boolean = cmp(me, you) == 0
  def <(me : A, you : A) : Boolean = cmp(me, you) < 0
  def >(me : A, you : A) : Boolean = cmp(me, you) > 0
  def <=(me : A, you : A) : Boolean = cmp(me, you) <= 0
  def >=(me : A, you : A) : Boolean = cmp(me, you) >= 0
}

defined class Ord

part2 : type class 의 instance (customize)


Ord[A] 의 type parameter A 에 다양한 type 을 집어넣음으로써 각각의 instance 를 구현합니다.

type parameter 의 instance 는 implicit val 또는 implicit def 를 통해 구현합니다.

(implicit def는 보통 생성할 instance에 parameter type 을 한번 더 주고 싶을 때 사용합니다. )

잘 감이 안오신다면, 밑에 코드에서 한번 더 언급될 것입니다. 그때 다시 설명하겠습니다.

1. Ord[Int] 인스턴스

Int 라는 타입을 다룰 수 있는 Type class 의 인스턴스입니다.

이 인스턴스를 인자로 받는 interface는 Int 타입의 데이터를 조작할 수 있습니다.

// A = Int
implicit val intOrd : Ord[Int] = new Ord[Int]{  //Ord[A] 의 A 에 Int를 대입하고 new Ord[Int] 로 초기화
  def cmp(me : Int , you : Int) : Int  = me - you
}
    cmd0.sc:1: not found: type Ord
    implicit val intOrd : Ord[Int] = new Ord[Int]{  //Ord[A] 의 A 에 Int를 대입하고 new Ord[Int] 로 초기화
                          ^cmd0.sc:1: not found: type Ord
    implicit val intOrd : Ord[Int] = new Ord[Int]{  //Ord[A] 의 A 에 Int를 대입하고 new Ord[Int] 로 초기화
                                         ^


    Compilation Failed

2. Ord[Double] 인스턴스

마찬가지고 Double 이라는 타입을 다룰 수 있는 Type class 의 인스턴스입니다.

이 인스턴스를 인자로 받는 interface는 Double 타입의 데이터를 조작할 수 있습니다.

// A = Double
implicit val doubleOrd : Ord[Double] = new Ord[Double]{
  def cmp(me : Double, you : Double) : Int = (me - you).toInt
}

doubleOrd: Ord[Double] = $sess.cmd2Wrapper$Helper$$anon$1@3368bc6f

3. Ord[ (X,Y) ] 인스턴스

여기에선 특별하게 implicit instance 안에 인자로 다시한번 implicit parameter를 받았습니다.

(X,Y) 형식의 type 을 다룰 수 있도록 해주는 Type class 의 인스턴스이고,

이 인스턴스를 받는 interface는 모두 (X,Y) 형식의 type 의 데이터를 다룰 수 있습니다.

type class interface + instance 의 혼종이라고 생각하면 좋겠습니다. (아래 interface 부분을 보시고 다시 보시는게 이해에 도움 됩니다. 조금 어렵습니다.)

// A = (X,Y)
implicit def twoOrd[X,Y](implicit ordX : Ord[X], ordY : Ord[Y])  : Ord[(X,Y)] = new Ord[(X,Y)]{
  def cmp(me : (X,Y), you : (X,Y)) : Int = {
    val c1 = ordX.cmp(me._1, you._1)
    if(c1 != 0) c1
    else ordY.cmp(me._2 , you._2)
  }
}
defined function twoOrd

part3 : type class interface (사용)


type class Ord[X] 의 interface 에는 두개의 입구가 있습니다.

  1. type X 인 데이터 parameter 들을 받는 입구
  2. Ord[X] 의 instance 를 implicit parameter 로 받는 입구

**1번 입구에서 받은 data 들을 , 2번 입구에서 받은 type class instance 의 규칙을 이용해 처리합니다. **

여기서 implicit parameter 의 입구는, scala 의 컴파일러에 의해 자동으로 코드 내의 implicit val / implicit def 로 구현된 Cls[X] 의 instance 를 찾아 연결합니다.

** 즉, 1번 입구는 넣어주어야하지만, 2번 입구는 인스턴스들만 만들어 놓았다면 자동으로 채워집니다. **

def max3[A](a : A, b : A, c: A)(implicit ord : Ord[A]) : A =
  if (ord.<=(a,b))  {if (ord.<=(b,c)) c else b}
  else              {if (ord.<=(a,c)) c else a }


defined function max3

result


max3[Int](1,2,3)  // ord = intOrd 인스턴스 자동 사용됨
max3[Double](1.1, 2.2, 3.3) // ord = doubleOrd 인스턴스 자동 사용됨
res5_0: Int = 3
res5_1: Double = 3.3




4. 두개 의 type parameter 를 받는 Type class




같은 방식으로, 이번엔 두개의 type parameter 를 받는 type class 를 보겠습니다.

part1 : type class 의 본체


Iter[I,A] 는 type A 를 저장하고있는 type I 에서 , A를 조회하거나 다음 I 로 넘어가는 기능을 구현합니다.

예를 들자면,

A 가 Int 이면 I 는 이를 저장하는 List[Int]가 될 것이고

A 가 String 이면 I는 이를 저장하는 List[String] 이 될 것입니다.

abstract class Iter[I, A]{    
  def getValue(i : I) : Option[A]  //메서드의 디테일은 인스턴스에서 구현합니다.
  def getNext(i : I) : I
}

defined class Iter


part2 : type class 의 instance (customize)


Iter[I, A] 의 parameter I, A 에 다양한 type 을 집어넣음으로써 각각의 instance 를 구현합니다.

다시한번..
(implicit def는 보통 생성할 instance에 parameter type 을 한번 더 주고 싶을 때 사용합니다. )


1. Iter[Int, Int] 인스턴스

// I = Int, A = Int

// Int 라는 저장 구조(I) 안에서 Int 데이터(A)를 조회할 수 있습니다.
implicit val intIter : Iter[Int, Int] = new Iter[Int, Int]{
  def getValue(i : Int) = if (i>0) Some(i) else None  
  def getNext(i : Int) = if(i-1 > 0 ) i-1 else 0
}
intIter: Iter[Int, Int] = $sess.cmd7Wrapper$Helper$$anon$1@5a79ff31


2. Iter[ List[X], X ] 인스턴스

이 인스턴스의 독특한 점은, 인스턴스의 완성을 interface 로 떠넘겼다는 것입니다.

X는 결국 이 인스턴스가 사용되는 interface 에서 결정됩니다.

즉 interface에서 X를 Int 로 주면 Iter[List[Int], Int] 가 되고

다른 값으로 주면 달라집니다.

// I = List[X], A = X

//List 라는 저장구조 (I) 안에서 X 타입의 데이터(A) 를 조회할 수 있습니다.  
implicit def listIter[X] : Iter[List[X], X] = new Iter[List[X], X]{
  def getValue(i : List[X]) = i.headOption
  def getNext(i  : List[X]) = i.tail
}
defined function listIter


part3 : type class interface (사용)


// listIter 의 X를 인스턴스 파라메터에서 Int로 고정해놓았습니다.
def sumElements[I](xs : I)(implicit proxy : Iter[I, Int]) : Int = proxy.getValue(xs) match{
  case None => 0
  case Some(n) => n + sumElements(proxy.getNext(xs))
}


// listIter 의 X는 xs(I) 의 타입에 따라 결정됩니다. I 가 List[Int] 이면, A 도 Int 가 됩니다.
def printElements[I](xs : I)(implicit proxy : Iter[I, A]) :Unit = proxy.getValue(xs) match{
  case None =>
  case Some(n) => {println(n) ; printElements(proxy.getNext(xs))}
}
defined function sumElements
defined function printElements


result


printElements[List[Int]](List(3,4,5,6))
printElements[Int](10)
3
4
5
6
10
9
8
7
6
5
4
3
2
1




5. 좀 더 복잡한 문제들




재료


//재료
sealed abstract class MyTree[A]
case class Empty[A]() extends MyTree[A]
case class Node[A](value : A, left : MyTree[A], right : MyTree[A]) extends MyTree[A]

defined class MyTree
defined class Empty
defined class Node


part1 : type class 본체


순환적이지 않은 type R 의 무언가를 순환적으로 만들어준다.

iter()는 순환적이지 않은 type R 의 무언가를 순환적인 I 로 바꾸고, I는 A들을 포함한다.

abstract class Iterable[R, I ,A] {
  def iter(a: R) : I
  def iterProxy : Iter[I,A]
}
defined class Iterable


part2 : type class 의 instance (customize)



Iterable[ MyTree[X] , List[X], X ] 인스턴스 (<- X 유동적 )

implicit def treeIterable[X](implicit proxy : Iter[List[X], X]) : Iterable[MyTree[X], List[X], X] = new Iterable[MyTree[X], List[X], X]{
  def iter(a : MyTree[X]) : List[X] = a match{
    case Empty() =>  Nil
    case Node(v, left, right) => v :: (iter(left) ++ iter(right))
  }
  def iterProxy : Iter[List[X], X] =  proxy
}
defined function treeIterable


part3 : type class 의 interface (사용)


def sumElements2[R, I](xs : R)(implicit proxy : Iterable[R, I ,Int]) = sumElements(proxy.iter(xs))(proxy.iterProxy)

def printElements2[R, I](xs : R)(implicit proxy : Iterable[R, I, Int]) = printElements(proxy.iter(xs))(proxy.iterProxy)
defined function sumElements2
defined function printElements2


result

val tree : MyTree[Int] = Node(10, Node(9, Empty(), Empty()), Node(8, Empty(), Empty()))

sumElements2[MyTree[Int], List[Int]](tree)
tree: MyTree[Int] = Node(10,Node(9,Empty(),Empty()),Node(8,Empty(),Empty()))
res15_1: Int = 27




6. Higher-kind Type classes




Higher-kind 는 type 들을 다시 상위의 하나의 개념으로 묶는, 상위 카테고리라고 할 수 있고, 변역은 “상류” 로 한다.

아래 링크를 참고하면 좋다.

twitter 의 scalaschool : scala의 상류 타입

https://twitter.github.io/scala_school/ko/advanced-types.html#higher

재료


import scala.language.higherKinds
import scala.language.higherKinds

higher kind 를 사용하려면 scala 에서 위처럼 higerKinds 패키지를 import 해줘야한다.

part1 : type class 본체

abstract class Iter2[I[_]]{
    def getValue[A](a : I[A]) : Option[A]
    def getNext[A](a : I[A]) : I[A]
}
defined class Iter2

part2 : type class 의 instance (customize)


Iter2[I[_]] 를 일종의 익명함수 처럼 생각하고 , 빈칸안에 들어올 인자타입은 나중에 받는 것으로 보류하고 나머지를 구현한다.

implicit val listIter2 : Iter2[List] = new Iter2[List]{
    def getValue[A](a : List[A]) = a.headOption
    def getNext[A](a : List[A]) = a.tail
}
listIter2: Iter2[List] = $sess.cmd18Wrapper$Helper$$anon$1@1188d429

part3 : type class 의 interface (사용)


// Int 를 담는 higerkind I 에 대해서만 동작한다.
def sumElements3[I[_]](xs : I[Int])(implicit itr2 : Iter2[I]) : Int = {
    itr2.getValue(xs) match {
        case None => 0
        case Some(n) => n + sumElements3(itr2.getNext(xs))
    }
}

// higerkind I 에 담긴 type X에 구애를 받지 않는다.
def printElements3[I[_],X](xs : I[X])(implicit itr2 : Iter2[I]) : Unit = {
    itr2.getValue(xs) match {
        case None =>
        case Some(n) => {println(n) ; printElements3(itr2.getNext(xs))}
    }
}
defined function sumElements3
defined function printElements3

result


val lst = List(4, 5, 10 ,8, 9)

sumElements3(xs = lst)

// 과정1.    lst 가 들어가면서 I = List 타입인자 전달 ;
// 과정2.    itr2 : Iter2[List] = listIter2   자동으로 채워짐 (implicit 끼리의 연동)

printElements3(xs = lst)
4
5
10
8
9





lst: List[Int] = List(4, 5, 10, 8, 9)
res21_1: Int = 36




7. Higher-kind Type classes + 좀 더 어려운 문제들




순환가능하지 않은 타입 R 을 순환 가능한 타입 I 으로 바꿔주는 동작과 (def iter : I),

그 순환가능한 타입 I에 대한 순환동작(type class Iter2[I]) 을 접근할 수 있게 해주는 동작(def iterProxy : Iter2[I])

으로 구성된 새로운 type class Iterable2[R[_], I[_]] 를 만들어보자.

재료


import scala.language.higherKinds
import scala.language.higherKinds

아래는 예제에 사용될 MyTree 라는 데이터타입이다.

//재료
sealed abstract class MyTree[A]
case class Empty[A]() extends MyTree[A]
case class Node[A](value : A, left : MyTree[A], right : MyTree[A]) extends MyTree[A]

import scala.language.higherKinds


defined class MyTree
defined class Empty
defined class Node

</br>

part1 : type class 본체


abstract class Iterable2[R[_], I[_]]{
    def iter[A](a : R[A]) : I[A]
    def iterProxy : Iter2[I]
}
defined class Iterable2


part2 : type class 의 instance (customize)


implicit def treeIterable2(implicit proxy : Iter2[List]) : Iterable2[MyTree, List] = new Iterable2[MyTree, List]{
    def iter[A](a : MyTree[A]) : List[A] = a match {
        case Empty() => Nil
        case Node(v, left, right) => v :: (iter(left) ++ iter(right))
    }
    def iterProxy : Iter2[List]  =  proxy
}
defined function treeIterable2

TIP :: 위의 코드는 아래와같이도 사용할 수 있다.

/**
implicit def treeIterable2: Iterable2[MyTree, List] = new Iterable2[MyTree, List]{
    def iter[A](a : MyTree[A]) : List[A] = a match {
        case Empty() => Nil
        case Node(v, left, right) => v :: (iter(left) ++ iter(right))
    }
    def iterProxy : Iter2[List]  =  implicitly[Iter2[List]] // type 주어진 채로 연동하기  
}
**/


part3 : type class 의 interface (사용)


def sumElements4[R[_],I[_]](xs : R[Int])(implicit proxy : Iterable2[R, I]) = sumElements3(proxy.iter(xs))(proxy.iterProxy)

//1. xs 에 인자가 전달된다. R과 _가 결정된다.
//2. 결정된 R과 _ 를 확인한 implicit 가 proxy 를 결정한다. I가 결정된다.


def printElements4[R[X], I[X], X](xs : R[X])(implicit proxy : Iterable2[R,I]) = printElements3(proxy.iter(xs))(proxy.iterProxy)
defined function sumElements4
defined function printElements4

result


val t2 : MyTree[Int] = Node(3, Node(4, Node(5, Empty(), Empty()), Empty()), Empty())
t2: MyTree[Int] = Node(3,Node(4,Node(5,Empty(),Empty()),Empty()),Empty())
sumElements4(t2)
printElements4(t2)
3
4
5





res28_0: Int = 12




8. Higher-kind Type classes + Functor Specification




재료


import scala.language.higherKinds
import scala.language.higherKinds
//재료
sealed abstract class MyTree[A]
case class Empty[A]() extends MyTree[A]
case class Node[A](value : A, left : MyTree[A], right : MyTree[A]) extends MyTree[A]

defined class MyTree
defined class Empty
defined class Node

</br>

part1 : type class 본체


trait Functor[F[_]] {
    def map[A, B](f : A=>B)(x : F[A]) : F[B]
}
defined trait Functor


part2 : type class 의 instance (customize)


implicit val ListFunctor : Functor[List] = new Functor[List]{
    def map[A,B](f : A=>B)(x : List[A]) : List[B] = x.map(f)
}

implicit val TreeFunctor : Functor[MyTree] = new Functor[MyTree]{
    def map[A,B](f : A=>B)(x : MyTree[A]) : MyTree[B] = x match{
        case Empty() => Empty()
        case Node(v, left, right) => Node(f(v), map(f)(left), map(f)(right))
    }
}
ListFunctor: Functor[List] = $sess.cmd33Wrapper$Helper$$anon$1@3bbdd7c8
TreeFunctor: Functor[MyTree] = $sess.cmd33Wrapper$Helper$$anon$2@6f83087e


part3 : type class 의 interface (사용)


def compose[F[_],X,Y,Z](g : Y=>Z)(f : X=>Y)(xs : F[X])(implicit proxy : Functor[F]) : F[Z] ={
    proxy.map(g)(proxy.map(f)(xs))
}
defined function compose

result



t2: MyTree[Int] = Node(3,Node(4,Node(5,Empty(),Empty()),Empty()),Empty())

3
4
5





res28_0: Int = 12