Skip to content

5.2 암시적 인자를 사용하는 시나리오 1~4

LazySoul edited this page Sep 4, 2016 · 12 revisions

암시적 인자를 사용하는 시나리오 1~4

암시를 현명하게 사용하고, 자주 사용하지 않는 것이 중요하다. 암시를 과도하게 사용하면 코드를 읽는 사람이 코드가 실제로 하는 일을 이해하기 어려울 수 있다.

이런 단점이 있음에도 불구하고 왜 암시적 인자를 사용해야 하는걸까?

  • 준비를 위한 코드를 없애는 것이다. 예를들어 맥락 정보를 명시적으로 제공하는 대신 암시적으로 제공 할 수 있다.
  • 매개변수화한 타입을 받는 메서드에 사용해서 버그를 줄이거나 허용되는 타입을 제한하기 위한 제약사항으로 사용하는 것이다.

5.2.1 실행 맥락 제공하기

implict 내용을 포함하는 전역 기본맥락(file)을 import시킬수 있다.

import scala.concurrent.ExecutionContext.Implicits.global

5.2.2 사용 가능한 기능 제어하기

맥락을 넘기는 것 외에, 암시적 인자를 통해서 사용 가능한 기능을 제어 할 수 도 있다.
권한 토큰이 들어 있는 암시적 사용자 세션 인자를 사용해서 특정 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)
}

5.2.3 사용 가능한 인스턴스 제한하기

매개변수화한 타입이 있는 메서드가 있고, 그 메서드의 타입 매개변수에 사용할 수 있는 타입을 제한하고 싶다고 가정하자.

우리가 허용하고 싶은 타입이 특정 공통 슈퍼타입의 모든 서브타입이라면 객체지향적 기법을 사용해서 암시를 피할 수 있다.

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] 메서드는 두 인자 목록을 받는다. 하나는 행에서 가져올 열의 이름이며, 다른 하나는 암시적 함수 인자다. 이 함수는 행에서 가져온 열의 데이터를 뽑아내고, 그 값을 적절한 타입으로 변환한다.

5.2.4 암시적 증거 제공하기

앞서 암시적 객체를 사용해서 공통 슈퍼타입이 없는 경우에도 허용하는 타입을 제한할 수 있음을 봤다.

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)

따라서 '증거'는 오직 타입 제약을 강제하기 위해서만 존재한다. 추가작업을 수행하는 암시적 값을 직접 정의할 필요는 없다.

Clone this wiki locally