Scala

Scala의 암시(implicit)

partner_jun 2017. 3. 16. 13:45

스칼라의 암시는 크게 두 가지의 방법으로 쓰인다.


1. 암시적 파라미터(암시적 인자)

메소드의 파라미터에 implicit 키워드를 사용하는 방법이다.

이미 implicit 키워드를 이용해 선언한 값이 존재할 때 그 값을 쓰게 된다.


implicit val x: Int = 500
def plusValue(paramX: Int)(implicit paramY: Int) = paramX + paramY
println( plusValue(100)(200) ) // 300
println( plusValue(200) ) // 700


이런 사용은 Future를 사용할 때를 생각하면 편하다.

ExecutionContext가 필요한 Future 함수는 두번째 인자로 ExecutionContext를 입력받는다.

*  @param executor  the execution context on which the future is run
* @return the `Future` holding the result of the computation
*/
def apply[T](body: =>T)(implicit @deprecatedName('execctx) executor: ExecutionContext): Future[T] =

Future의 apply



직접 입력하지 않고 scala.concurrent.ExecutionContext.Implicits.global을 import해 인자 없이 사용할 수도 있다. 

import scala.concurrent.ExecutionContext.Implicits.global

val result: Future[String] = Future {
TimeUnit.SECONDS.sleep(2L)
"Hello-world"
}

result.onComplete{
case Success(str) => println(str)
case Failure(e) => e.printStackTrace()
}


import한 Implicits 오브젝트에 이미 선언된 ExecutionContext가 있기 때문이다.

implicit lazy val global: ExecutionContext = impl.ExecutionContextImpl.fromExecutor(null: Executor)


이렇게 사용하는 implicit에는 몇 가지 규칙이 있다.

1) 마지막 파라미터에만 implicit 키워드를 쓸 수 있다.

2) 파라미터 목록 가장 처음에만, 단 한번만 사용 가능하다.

3) 파라미터가 implicit 키워드로 시작하면, 그 파라미터 목록 안의 모든 파라미터는 implicit 키워드의 영향을 받는다. 




1.1 암시적 선언과 Implicity 메소드

리스트에 map 함수를 적용한 후 소팅하는 sortBy를 감싼 메소드의 예를 보자.

def sortBy1[A, B](lst: List[A])(f: A => B)(implicit ord: Ordering[B]): List[A] = {
lst.sortBy(f) // ord 생략됨
}

sortBy1은 타입 B를 Ordering[B]로 캐스팅한 값을 implicit ord로 가진다.

lst.sortBy 함수의 세 번째 파라미터는 직접 입력하지 않았지만 만들어진 ord를 사용한다.


def sortBy2[A, B: Ordering](lst: List[A])(f: A => B): List[A] = {
val ord = implicitly[Ordering[B]] // implicit로 선언된 ord와 같음.
lst.sortBy(f)
}


sortBy2는 첫 번째 함수와 같지만 파라미터 대신 함수 바디에 implicitly 메소드를 이용했다. 

하지만 이 방법을 위해서는 타입 B의 상위 타입을 함수 제네릭에 제시해야만 한다.

implicity 메소드를 사용하면 val ord = ... 와 같이 값으로 직접 선언하지 않아도 implicit 키워드를 붙여 선언한 값처럼 사용할 수 있다.




1.2 암시적 파라미터를 이용한 기능 제한

암시적 파라미터로 사용할 수 있는 기능을 제한하는 함수를 만들 수 있다.

def getMenu(implicit userInfo: UserInfo) = userInfo.getPermission() match {
case "Admin" => "AdminMenu"
case _ => "UserMenu"
}

유저 정보를 매번 파라미터로 주고받을 필요 없이 암시적 파라미터를 통해 이용하는 방법이다.




2. 타입 클래스 패턴

타입 클래스 패턴을 이용하면 클래스에 인터페이스를 구현한 것과 같은 효과를 볼 수 있다.

class OuterClass(paramX: Int) { 
println(s"OuterClass - $this")
def *&*(paramY: Int) = paramY * paramX
}

implicit def imp(paramX: Int): OuterClass = new OuterClass(paramX) // 아우터 클래스는 부분 함수로 만들어 사용해야 한다.


implicit class InnerClass(paramX: Int) { // 이너 클래스에는 implicit 키워드를 바로 사용 가능하다.
println(s"InnerClass - $this")
def ***(paramY: Int) = paramY * paramX
}

println(200 *** 500)
println(200 *** 500)
println(200 *&* 500)
println(200 *&* 500)


/*
InnerClass - default.Implicity$InnerClass@7e0babb1
100000
InnerClass - default.Implicity$InnerClass@5a39699c
100000
OuterClass - default.OuterClass@3cb5cdba
100000
OuterClass - default.OuterClass@56cbfb61
100000
*/

이 기능을 이용하면 이미 구현 되어있는 클래스에 새로운 메소드를 추가한 것처럼 사용이 가능하다.

많은 API나 Framework의 DSL은 타입 클래스 패턴을 사용했다고 보면 될 것이다.


이너 클래스일 때의 사용법과 아우터 클래스일 때의 사용법이 다른 것을 기억해야 할 것이다. 

또 하나 특이한 점은 숫자 사이 infix 메소드가 실행될 때마다 클래스의 객체가 만들어진다는 사실이다. 



2.2 암시적 변환

사실 implicit의 가장 강력한 기능은 이 암시적 변환이 아닐까 싶다.

자바에서의 자동 형변환은 부모 클래스로의 캐스팅이나 오토 박싱/오토 언박싱 등 한정적인 상황에서만 가능하다.

하지만 스칼라에서는 implicit로 선언된 형변환 메소드를 통해 오토 캐스팅이 가능해진다.

case class Paper(content: String)
case class Book(name: String, code: Int)

// book 객체를 paper 객체로 바꾸는 메소드
implicit def bookToPaper(book: Book): Paper = Paper(s"${book.code} - ${book.name}")

val book: Book = Book("Scala", 1234)
val paper: Paper = book // bookToPaper 함수를 이용해 Paper 객체로 변환



많은 클래스의 컴패니언 오브젝트에는 이미 많은 암시적 변환이 선언되어 있다.

implicit def char2int(x: Char): Int = x.toInt
implicit def char2long(x: Char): Long = x.toLong
implicit def char2float(x: Char): Float = x.toFloat
implicit def char2double(x: Char): Double = x.toDouble

(Char Companion object의 암시적 형 변환들)



따라서 기본적인 형 변환은 단지 타입을 명시하는 것만으로 변환이 가능하다. 하지만 새로운 클래스를 선언했고, 잦은 형 변환을 해야한다면 implicit 키워드를 사용한 형 변환 함수를 구현하는 것이 좋을 것이다.




이렇게 implicit 키워드는 많은 곳에 사용 할 수 있다. 하지만 몇 가지 단점이 있다.

첫 번째로 implicit로 선언된 함수나 값은 지역적이지 않다. 

클래스나 패키지같은 제한을 무시한 채 프로젝트에 존재한다면 그냥 사용한다는 것이다.


두 번째는 컴파일이 느려진다는 것이다. 

첫 번째 단점과 이어진 문제인데, implicit으로 선언된 값을 찾기 위한 '검색 시간'이 소요되기 때문이다.

특히 동일한 implicit 값을 여러번 사용한다면 더욱 느려질 수 있다.