이번 강좌에서는 다음을 다룬다.

뷰 바운드(“타입 클래스”)

때로 두 타입이 서로 같거나 상하위 관계가 있는지를 명시할 필요가 없고, 변환으로 이를 흉내만 내면 좋겠다는 생각이 들 때가 있다. 뷰 바운드는 어떤 타입을 다른 타입으로 “볼 수 있는지”를 지정한다. 이는 어떤 객체를 "읽기"는 하지만 변경할 필요는 없는 연산일 때 유용하다.

묵시적(implicit) 함수는 자동 변환을 제공한다. 정확히 말하면, 그 묵시적 함수를 호출해야만 타입 추론이 이루어질 수 있는 경우에만 이 함수가 호출된다. 다음은 한 예이다.

scala> implicit def strToInt(x: String) = x.toInt
strToInt: (x: String)Int

scala> "123"
res0: java.lang.String = 123

scala> val y: Int = "123"
y: Int = 123

scala> math.max("123", 111)
res1: Int = 123

뷰 바운드는 타입 바인드처럼 어떤 타입에 대해 그런 타입 변환 함수가 존재해야 함을 요청하기 위해 사용한다. 뷰 바운드는 아래 예에서와 같이 <%로 표시한다.

scala> class Container[A <% Int] { def addIt(x: A) = 123 + x }
defined class Container

“A <% Int”의 뜻은 AInt 로 “볼 수 있어야만” 한다는 뜻이다. 이를 시험해 보자.

scala> (new Container[String]).addIt("123")
res11: Int = 246

scala> (new Container[Int]).addIt(123) 
res12: Int = 246

scala> (new Container[Float]).addIt(123.2F)
<console>:8: error: could not find implicit value for evidence parameter of type (Float) => Int
       (new Container[Float]).addIt(123.2)
        ^

다른 타입 바운드들

메소드에는 묵시적 매개변수를 통해 더 복잡한 타입 바운드를 걸 수 있다. 예를 들어 List는 원소가 수인 경우 sum을 지원하지만 수가 아니면 지원하지 않는다. 하지만 불행한 점은 스칼라에서 모든 수가 공유하는 공통의 상위타입이 없다는 것이다. 따라서 T <: Number과 같은 바운드를 사용할 수는 없다. 대신 이를 위해 스칼라의 수학 라이브러리에서는 지원하는 타입 T에 대해 묵시적인 Numeric[T]를 정의해 두었다. List의 정의에서는 다음과 같이 그 사실을 활용하면 된다.

sum[B >: A](implicit num: Numeric[B]): B

List(1,2).sum을 호출하는 경우 num 매개변수를 직접 전달할 필요는 없다(implicit로 정의되어 있다는 점에 유의하라). 하지만, List("whoop").sum를 호출한다면, 컴파일러는 num을 설정할 수 없다고 오류를 출력할 것이다.

(역주: 원문에는 List(1,2).sum()이라고 되어 있었다. 하지만 실행해 보면 컴파일 오류가 난다. 이유는 ()로 매개변수가 있다는 것을 명시하는 경우에는 반드시 디폴트값이 지정되지 않은 인자를 넘겨야 하기 때문이다. implicit는 디폴트가 아니다. 따라서 컴파일러는 여기서 num을 넘겨줘야 한다고 비명을 지르게 된다. 이를 해결하려면 implicitly를 사용해야 한다. 어째 배가 산으로 간다. 구글에서 List.sum implicitly를 한번 찾아보라.)

Numeric 처럼 이상한 객체를 설정할 필요 없이, 메소드에서 타입에 특정 "증거"를 요청할 수도 있을 것이다. 대신, 아래와 같은 타입 관련 연산자를 활용할 수도 있다.

A =:= B A는 B와 같아야 함
A <:< B A는 B의 하위 타입이어야 함
A <%< B A는 B로 볼 수 있어야 함
scala> class Container[A](value: A) { def addIt(implicit evidence: A =:= Int) = 123 + value }
defined class Container

scala> (new Container(123)).addIt
res11: Int = 246

scala> (new Container("123")).addIt
<console>:10: error: could not find implicit value for parameter evidence: =:=[java.lang.String,Int]

implicit를 활용하면, 이 제약 사항을 “볼 수 있음”으로 바꿀 수 있다.

scala> class Container[A](value: A) { def addIt(implicit evidence: A <%< Int) = 123 + value }
defined class Container

scala> (new Container("123")).addIt
res15: Int = 246

뷰를 사용한 일반적 프로그래밍

스칼라 표준 라이브러리에서 주로 뷰가 사용되는 곳은 컬렉션에 대한 일반적인 함수를 정의하는 경우이다. 예를 들면 Seq[] 에 대한 “min” 함수가 이런식이다.

def min[B >: A](implicit cmp: Ordering[B]): A = {
  if (isEmpty)
    throw new UnsupportedOperationException("empty.min")

  reduceLeft((x, y) => if (cmp.lteq(x, y)) x else y)
}

이렇게 하면 다음과 같은 장점이 있다.

scala> List(1,2,3,4).min
res0: Int = 1

scala> List(1,2,3,4).min(new Ordering[Int] { def compare(a: Int, b: Int) = b compare a })
res3: Int = 4

한가지 덧붙이자면,표준 라이브러리에는 OrderedOrdering 을 상호 변환하는 뷰가 있다.
(역주: Ordered와 Ordering의 차이는 자바 Comparable과 Comparator의 차이와 유사하다. 둘다 순서관계(partial ordering)를 지정하는 함수를 제공하는 역할을 하지만, Ordered는 주로 클래스에서 상속해 사용해 기본 대소비교를 담당하는 역할에 사용되고, Ordering은 명시적으로 두 값의 대소를 비교하는 연산을 정의해서 넘겨야 하는 경우 사용된다. 예를 들어 scala.util.Sorting에 있는 정렬 함수들에 컬렉션 객체만을 넘기면 컬렉션 원소의 Ordered 트레잇에 의해 정렬이 수행되고, 직접 Ordering을 넘기면 그 순서에 의해 정렬이 수행된다.)

trait LowPriorityOrderingImplicits {
  implicit def ordered[A <: Ordered[A]]: Ordering[A] = new Ordering[A] {
    def compare(x: A, y: A) = x.compare(y)
  }
}

implicitly[]와 컨텍스트 바운드

스칼라 2.8부터 묵시적 인자를 억세스하고 꿰는 간략한 문법이 도입되었다.

scala> def foo[A](implicit x: Ordered[A]) {}
foo: [A](implicit x: Ordered[A])Unit

scala> def foo[A : Ordered] {}                        
foo: [A](implicit evidence$1: Ordered[A])Unit

원한다면 implicitly 를 사용해 묵시적 값을 억세스할 수 있다.

scala> implicitly[Ordering[Int]]
res37: Ordering[Int] = scala.math.Ordering$Int$@3a9291cf

이 두 기능을 사용하면 뷰를 엮어 써야 하는 경우 코드가 간략해 지는 경우가 자주 있다.

상류 타입(Higher-kinded type)과 임의 다형성(ad-hoc polymorphism)

(*역주: Higher-Kinded는 한글로 번역하기가 어렵다. 고차(high-order)에서 이미 ’차’라는 말은 사용하고 있고, 상위라는 말은 subtype관계-클래스의 상속관계도 subtype관계이다-에서 이미 사용하고 있다. type은 형이라고 번역한다면, kind는 형보다 한 단계 더 상위의 형이기 때문에 류라고 붙이는게 적절할 것 같고, 그러고 나면 고류/상류 등이 남는다. 그냥 상위 카인드라고 하는 것도 이상한 것은 마찬가지이다. 어색하지만 그냥 상류를 쓰자.)

스칼라에서는 “상류” 타입에 대한 추상화가 가능하다. 예를 들어 어떤 데이터 타입을 보관하는 여러 타입의 컨테이너를 필요로 하는 경우를 생각해 보자. Container를 정의하고, 이를 여러 컨테이너들(즉 Option, List 등)이 상속/구현 하도록 할 수도 있다. 여기서 각 컨테이너에 들어가는 값의 타입을 고정하지 않고도 사용할 수 있는 인터페이스를 만들고 싶다.

이는 함수 커링과 유사하다. 예를 들어 “단항 타입”은 List[A]과 같은 식으로 생성이 가능할 것이다. 이 타입의 의미는 구체적 타입(List[Int]등)을 만들어내기 위해 타입 변수를 한 "단계"만 만족시키면 된다는 의미이다(마치 커링되지 않은 함수에 인자 목록을 한번만 넣어 호출하면 되는 것과 유사하다). 더 상류의 타입이라면 적용을 여러번 해야 할 것이다.

scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }

scala> val container = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }
container: java.lang.Object with Container[List] = $anon$1@7c8e3f75

scala> container.put("hey")
res24: List[java.lang.String] = List(hey)

scala> container.put(123)
res25: List[Int] = List(123)

Container 의 다형성은 매개변수가 있는 타입(즉, “컨테이너 타입”)에 대해 정의됨에 주의하라.

이 container를 implicit와 함께 사용하면 “임의” 다형성이 가능하다. 즉, 컨테이너에 대한 일반적 함수를 작성할 수 있다.

scala> trait Container[M[_]] { def put[A](x: A): M[A]; def get[A](m: M[A]): A }

scala> implicit val listContainer = new Container[List] { def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head }

scala> implicit val optionContainer = new Container[Some] { def put[A](x: A) = Some(x); def get[A](m: Some[A]) = m.get }

scala> def tupleize[M[_]: Container, A, B](fst: M[A], snd: M[B]) = {
     | val c = implicitly[Container[M]]                             
     | c.put(c.get(fst), c.get(snd))
     | }
tupleize: [M[_],A,B](fst: M[A],snd: M[B])(implicit evidence$1: Container[M])M[(A, B)]

scala> tupleize(Some(1), Some(2))
res33: Some[(Int, Int)] = Some((1,2))

scala> tupleize(List(1), List(2))
res34: List[(Int, Int)] = List((1,2))

F-바운드 다형성

(일반적인) 트레잇 안에서 구체적 하위 클래스를 억세스해야만 하는 경우가 가끔 있다. 예를 들어 일반적인 어떤 트레잇이 있는데, 그 트레잇의 특정 하위 클래스와만 비교를 할 수 있다고 하자.

trait Container extends Ordered[Container]

하지만, 이를 위해서는 비교 메소드가 있어야 한다.

def compare(that: Container): Int

그러나, 구체적인 하위 타입을 억세스할 수 없기 때문에 다음과 같은 코드는 컴파일에 실패한다.

class MyContainer extends Container {
  def compare(that: MyContainer): Int
}

우리가 명시한 것은 Container 에 대한 순서가 있어야 한다는 것이었지 특정 하위 타입에 대한 것이 아니었기 때문이다.

이를 해결하기 위해 F-바운드 하위타입을 사용한다.

trait Container[A <: Container[A]] extends Ordered[A]

이상한 타입이다! 하지만, 이제는 Ordered가 A 에 대해 매개변수화 되어 있다는 점을 보라. 그런데, A는 사실 Container[A] 이다.
(역주: F-바운드 다형성을 재귀적 다형성이라고도 한다. 위에서 보면 AContainer[A] 로 바운드되어 있는데, 다시 Ordered[A]를 확장하고 있다.)

따라서, 다음과 같이 쓸 수 있다.

class MyContainer extends Container[MyContainer] { 
  def compare(that: MyContainer) = 0 
}

이제 이는 Ordered 트레잇을 만족한다.

scala> List(new MyContainer, new MyContainer, new MyContainer)
res3: List[MyContainer] = List(MyContainer@30f02a6d, MyContainer@67717334, MyContainer@49428ffa)

scala> List(new MyContainer, new MyContainer, new MyContainer).min
res4: MyContainer = MyContainer@33dfeb30

모두가 Container[_] 의 하위 타입이다. 따라서 이제 다른 하위 클래스를 정의하고, 여러 Container[_] 타입들이 함께 들어간 리스트를 만들 수 있다.

scala> class YourContainer extends Container[YourContainer] { def compare(that: YourContainer) = 0 }
defined class YourContainer

scala> List(new MyContainer, new MyContainer, new MyContainer, new YourContainer)                   
res2: List[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]] 
  = List(MyContainer@3be5d207, MyContainer@6d3fe849, MyContainer@7eab48a7, YourContainer@1f2f0ce9)

이제는 결과 타입이 YourContainer with MyContainer 라는 하위 바운드로 제약된다. 이는 타입 유추기(inferencer)가 한 일이다. 재미있는 것은, 이 타입이 말이 되지 않는 타입이라도 상관 없다는 사실이다.
이 타입은 리스트에 들어가는 통합된 타입의 논리적인 공통의 최저 하위 바운드를 제공하는 역할을 할 뿐이다. 이제 Ordered 를 사용하면 어떤 일이 벌어질까?

(new MyContainer, new MyContainer, new MyContainer, new YourContainer).min
<console>:9: error: could not find implicit value for parameter cmp:
  Ordering[Container[_ >: YourContainer with MyContainer <: Container[_ >: YourContainer with MyContainer <: ScalaObject]]]

아쉽지만 통합된 타입에는 Ordered[] 가 존재하지 않는다.

구조적 타입(Structural type)

스칼라는 구조적 타입 을 지원한다. 타입 요구 사항을 구체적 타입 대신 인터페이스 구조 를 사용해 표현할 수 있다.

scala> def foo(x: { def get: Int }) = 123 + x.get
foo: (x: AnyRef{def get: Int})Int

scala> foo(new { def get = 10 })                 
res0: Int = 133

이렇게 하면 유용한 경우가 많이 있다. 하지만, 구현에 리플렉션을 사용하고 있기 때문에, 성능상의 문제를 항상 신경써야만 한다!

추상 타입 멤버(Abstract type member)

트레잇에서 타입 멤버를 추상인 상태로 남겨둘 수 있다.

scala> trait Foo { type A; val x: A; def getX: A = x }
defined trait Foo

scala> (new Foo { type A = Int; val x = 123 }).getX   
res3: Int = 123

scala> (new Foo { type A = String; val x = "hey" }).getX
res4: java.lang.String = hey

DI등을 할때 이런 기법이 유용할 수 있다.

추상 타입 변수를 해시 연산자를 사용해 참조 할 수 있다.

scala> trait Foo[M[_]] { type t[A] = M[A] }
defined trait Foo

scala> val x: Foo[List]#t[Int] = List(1)
x: List[Int] = List(1)

타입 소거와 메니페이스(Type erasure & manifest)

아다시피 타입 정보는 컴파일시 소거 되어 없어진다. 스칼라는 원하는 타입 정보를 복구할 수 있는 메니페스트 기능을 제공한다. 메니페스트는 묵시적 값으로 제공되며, 필요에 따라 컴파일러가 만들어낸다.

scala> class MakeFoo[A](implicit manifest: Manifest[A]) { def make: A = manifest.erasure.newInstance.asInstanceOf[A] }

scala> (new MakeFoo[String]).make
res10: String = ""

사례 분석: 피네이글(Finagle)

(역주: 피네이글은 JVM을 위한 RPC 시스템이다. 홈페이지는 https://twitter.github.io/finagle/을 참조하라.)

다음을 보라: https://github.com/twitter/finagle

trait Service[-Req, +Rep] extends (Req => Future[Rep])

trait Filter[-ReqIn, +RepOut, +ReqOut, -RepIn]
  extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut])
{
  def andThen[Req2, Rep2](next: Filter[ReqOut, RepIn, Req2, Rep2]) =
    new Filter[ReqIn, RepOut, Req2, Rep2] {
      def apply(request: ReqIn, service: Service[Req2, Rep2]) = {
        Filter.this.apply(request, new Service[ReqOut, RepIn] {
          def apply(request: ReqOut): Future[RepIn] = next(request, service)
          override def release() = service.release()
          override def isAvailable = service.isAvailable
        })
      }
    }
    
  def andThen(service: Service[ReqOut, RepIn]) = new Service[ReqIn, RepOut] {
    private[this] val refcounted = new RefcountedService(service)

    def apply(request: ReqIn) = Filter.this.apply(request, refcounted)
    override def release() = refcounted.release()
    override def isAvailable = refcounted.isAvailable
  }    
}

서비스는 필터를 사용해 인증할 수 있다.

trait RequestWithCredentials extends Request {
  def credentials: Credentials
}

class CredentialsFilter(credentialsParser: CredentialsParser)
  extends Filter[Request, Response, RequestWithCredentials, Response]
{
  def apply(request: Request, service: Service[RequestWithCredentials, Response]): Future[Response] = {
    val requestWithCredentials = new RequestWrapper with RequestWithCredentials {
      val underlying = request
      val credentials = credentialsParser(request) getOrElse NullCredentials
    }

    service(requestWithCredentials)
  }
}

이제 아랫단의 서비스가 인증이 된 요청을 요구하며, 이는 정적으로 검증된다. 필터는 따라서 서비스 변환기로 생각할 수 있다.

여러 필터를 함께 합성할 수도 있다.

val upFilter =
  logTransaction     andThen
  handleExceptions   andThen
  extractCredentials andThen
  homeUser           andThen
  authenticate       andThen
  route

이때 타입 안전성이 보장된다!
(역주: 어떻게 타입안전성을 보장하는지 궁금한 독자는 졸저 스칼라 타입체크로 설정을 컴파일시 체크하기를 참고하라.)