-
Notifications
You must be signed in to change notification settings - Fork 7
5.2 암시적 인자를 사용하는 시나리오 1~4
암시를 현명하게 사용하고, 자주 사용하지 않는 것이 중요하다. 암시를 과도하게 사용하면 코드를 읽는 사람이 코드가 실제로 하는 일을 이해하기 어려울 수 있다.
이런 단점이 있음에도 불구하고 왜 암시적 인자를 사용해야 하는걸까?
- 준비를 위한 코드를 없애는 것이다. 예를들어 맥락 정보를 명시적으로 제공하는 대신 암시적으로 제공 할 수 있다.
- 매개변수화한 타입을 받는 메서드에 사용해서 버그를 줄이거나 허용되는 타입을 제한하기 위한 제약사항으로 사용하는 것이다.
implict 내용을 포함하는 전역 기본맥락(file)을 import시킬수 있다.
import scala.concurrent.ExecutionContext.Implicits.global
맥락을 넘기는 것 외에, 암시적 인자를 통해서 사용 가능한 기능을 제어 할 수 도 있다.
권한 토큰이 들어 있는 암시적 사용자 세션 인자를 사용해서 특정 API 연산을 사용자가 호출할 수 있는지 판단하거나, 데이터의 가시성을 제한할 수 있다.
사용자 인터페이스의 메뉴를 만드는데, 일부 메뉴는 사용자가 로그인한 경우에만 보여야 하며, 일부는 로그인하지 않는 경우에만 보여야 한다고 가정하자. 다음과 같이 암시적 세션을 활용해서 이를 검사 할 수 있다.
def createMenu(implicit session: Session): Menu = {
val defaultItems = List(helpItem, searchItem)
val accountItems =
if (session.loggedin()) List(viewAccountItem, editAccountItem)
else List(loginItem)
Menu(defaultItems ++ accountItems)
}
매개변수화한 타입이 있는 메서드가 있고, 그 메서드의 타입 매개변수에 사용할 수 있는 타입을 제한하고 싶다고 가정하자.
우리가 허용하고 싶은 타입이 특정 공통 슈퍼타입의 모든 서브타입이라면 객체지향적 기법을 사용해서 암시를 피할 수 있다.
object manage {
def apply[R <: { def close():Unit }, T](resource: => R)(f: R => T) = { ... }
...
}
- 타입 매개변수 R은 close():Unit 메서드가 있는 어떤 타입의 서브타입이어야 한다.
- 또는 대상 자원이 모두 Closable 트레이트를 구현하고 있다고 가정할 수도 있다.(트레이트는 자바의 인터페이스를 대신하고 확장)
trait Closable {
def close(): Unit
}
...
object manage {
def apply[R <: Closable, T](resource: => R)(f: R => T ) = { ... }
...
}
이런 기법은 공통 슈퍼클래스가 없으면 사용할 수 없다. 그런 경우 암시적 인자를 사용해서 허용되는 타입을 제한할 수 있다. 스칼라 컬렉션 API는 설계 문제를 해결하기 위해 그런 방법을 사용한다.
trait TraversableLike[+A, +Repr] extends ... {
...
def map[B, That](f: A => B)(
implicit bf: CanBuildFrom[Repr, B, That]): That = { ... }
...
}
- +A 는 공변적임을 의미한다. 즉 B가 A의 서브타입이면
TraversableLike[B]
도TraversableLike[A]
의 서브타입이다. -
CanBuildFrom
은 빌더다. 암시적 빌더 객체가 존재하는 한 원하는 새로운 컬렉션을 이를 통해 생성할 수 있음을 강조하기 위함이다. -
Repr
은 원소를 저장하기 위해 내부적으로 사용하는 실제 컬렉션 타입이다. -
B
는 함수 f가 만들어 내는 원소 타입이다. -
That
은 만들고자 하는 대상 컬렉션의 타입 매개변수다. 즉 B는 A와 같을 수도 있고 다를 수도 있다. - 스칼라 API에는 모든 기본 컬렉션 타입에 대한
CanBuildFrom
정의가 들어있다.
map 연산의 결과 컬렉션으로 허용되는 것은, 현재 범위에 암시적(implicit)으로 선언된 CanBuildFrom
에 대응하는 인스턴스에 따라 결정된다.
따라서 자신에게 필요한 CanBuildFrom
타입을 만들고, 그 타입의 암시적 인스턴스를 컬렉션을 사용하는 쪽에서 임포트하게 만들어야 한다.
// A Java-like Database API, written in Scala for convenience.
package progscala2.implicits {
package database_api {
case class InvalidColumnName(name: String)
extends RuntimeException(s"Invalid column name $name")
trait Row {
def getInt (colName: String): Int
def getDouble(colName: String): Double
def getText (colName: String): String
}
}
package javadb {
import database_api._
case class JRow(representation: Map[String,Any]) extends Row {
private def get(colName: String): Any =
representation.getOrElse(colName, throw InvalidColumnName(colName))
def getInt (colName: String): Int = get(colName).asInstanceOf[Int]
def getDouble(colName: String): Double = get(colName).asInstanceOf[Double]
def getText (colName: String): String = get(colName).asInstanceOf[String]
}
object JRow {
def apply(pairs: (String,Any)*) = new JRow(Map(pairs :_*))
}
}
}
// src/main/scala/progscala2/implicits/scala-database-api.scala
// A Scala wrapper for the Java-like Database API.
package progscala2.implicits {
package scaladb {
object implicits {
import javadb.JRow
implicit class SRow(jrow: JRow) {
def get[T](colName: String)(implicit toT: (JRow,String) => T): T =
toT(jrow, colName)
}
implicit val jrowToInt: (JRow,String) => Int =
(jrow: JRow, colName: String) => jrow.getInt(colName)
implicit val jrowToDouble: (JRow,String) => Double =
(jrow: JRow, colName: String) => jrow.getDouble(colName)
implicit val jrowToString: (JRow,String) => String =
(jrow: JRow, colName: String) => jrow.getText(colName)
}
object DB {
import implicits._
def main(args: Array[String]) = {
val row = javadb.JRow("one" -> 1, "two" -> 2.2, "three" -> "THREE!")
val oneValue1: Int = row.get("one")
val twoValue1: Double = row.get("two")
val threeValue1: String = row.get("three")
// val fourValue1: Byte = row.get("four") // won't compile
println(s"one1 -> $oneValue1")
println(s"two1 -> $twoValue1")
println(s"three1 -> $threeValue1")
val oneValue2 = row.get[Int]("one")
val twoValue2 = row.get[Double]("two")
val threeValue2 = row.get[String]("three")
// val fourValue2 = row.get[Byte]("four") // won't compile
println(s"one2 -> $oneValue2")
println(s"two2 -> $twoValue2")
println(s"three2 -> $threeValue2")
}
}
}
}
implicits
객체 안에서, 자바 JRow를 원하는 get[T] 메서드가 존재하는 타입으로 감싸주는 암시 클래스를 정의한다. 이런 클래스를 암시적 변환(implicit conversion)이라고 부르는데, 그에 대해서는 이번장에서 설명하며 지금은 이럼 암시적 변환을 사용하면 마치 미리 정의해둔 것 처럼 JRow 인스턴스에 대해 get[T]를 호출할 수 있다는 점만 알면 된다.
get[T] 메서드는 두 인자 목록을 받는다. 하나는 행에서 가져올 열의 이름이며, 다른 하나는 암시적 함수 인자다. 이 함수는 행에서 가져온 열의 데이터를 뽑아내고, 그 값을 적절한 타입으로 변환한다.
앞서 암시적 객체를 사용해서 공통 슈퍼타입이 없는 경우에도 허용하는 타입을 제한할 수 있음을 봤다.
trait TraversableOnce[+A] ... {
...
def toMap[T, U](implicit ev: <:<[A, (T, U)]): immutalbe.Map[T, U]
...
}
<:<
라는 타입은 Predef
에 정의된 타입이다. 위 식을 중위 표기법으로 바꿀 수도 있다.
<:<[A, B]
A <:< B
<:<[A, (T, U)]
A <:< (T, U)
scala> val l1 = List(1,2,3)
l1: List[Int] = List(1, 2, 3)
scala> l1.toMap
<console>:12: error: Cannot prove that Int <:< (T, U).
l1.toMap
^
scala> val l2 = List("one" -> 1, "two" -> 2, "three" ->3)
l2: List[(String, Int)] = List((one,1), (two,2), (three,3))
scala> l2.toMap
res1: scala.collection.immutable.Map[String,Int] = Map(one -> 1, two -> 2, three -> 3)
따라서 '증거'는 오직 타입 제약을 강제하기 위해서만 존재한다. 추가작업을 수행하는 암시적 값을 직접 정의할 필요는 없다.