Skip to content
Seo Inhyuk edited this page Sep 5, 2016 · 25 revisions

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

  • 암시를 과도하게 사용할 경우 코드를 읽는 사람이 이해하기 어려울 수 있음 -> 암시를 필요할때만 현명하게 사용하는 것이 필요
  • 암시를 사용하는 이유

(1) 준비를 위한 코드를 없앰 (ex. context 정보)

(2) 매개변수화한 타입을 받는 메서드에 사용 -> 버그를 줄이거나 허용되는 타입을 제한하기 위한 제약사항

5.2.5 타입 소거 우회하기

  • 암시적 증거(implicit evidence) : 허용할 타입을 제한하면서, 각 타입이 공통 슈퍼타입을 따를 필요가 없음
  • 타입 소거(type erasure)로 인한 제약을 우회하는 기법 : 암시적 객체가 증거 역할만을 함
// 1
scala> object M {
     | def m(seq: Seq[Int]): Unit = println(s"Seq[Int]: $seq")
     | def m(seq: Seq[Double]): Unit = println(s"Seq[Double]: $seq")
     | }
<console>:12: error: double definition:
def m(seq: Seq[Int]): Unit at line 11 and
def m(seq: Seq[Double]): Unit at line 12
have same type after erasure: (seq: Seq)Unit
       def m(seq: Seq[Double]): Unit = println(s"Seq[Double]: $seq")
// 2
scala> def m(seq: Seq[Int]): Unit = println(s"Seq[Int]: $seq")
m: (seq: Seq[Int])Unit

scala> def m(seq: Seq[Double]): Unit = println(s"Seq[Double]: $seq")
m: (seq: Seq[Double])Unit
  • JVM은 매개변수화한 타입의 타입인자를 '망각' (타입 소거, type erasure) -> 1과 같이 타입정보를 제거하여 오버로딩한 두 메서드가 같은 메서드라고 판단
  • 2와 같이 REPL에서는 편의상 타입이나 값, 메서드를 오버라이딩 할 수 있도록 허용하기 때문에 오류가 발생하지 않음
  • 스칼라 컴파일러는 바이트 코드 상에서 두 메서드가 동일하다고 판단하기 때문에, 이러한 모호한 정의를 금지함
object M {
  implicit object IntMarker
  implicit object StringMarker

  def m(seq: Seq[Int])(implicit i: IntMarker.type ): Unit =
    println(s"Seq[Int]: $seq")
  def m(seq: Seq[String])(implicit s: StringMarker.type ): Unit =
    println(s"Seq[String]: $seq")
}

import M._
m(List(1,2,3))
m(List("one", "two", "three"))
Seq[Int]: List(1, 2, 3)
res0: Unit = ()
Seq[String]: List(one, two, three)
res1: Unit = ()
  • 스칼라 컴파일러가 수행하는 타입 소거로 인한 메서드의 모호성을 제거하기 위해 암시적 인자를 활용
  • 모호성을 제거해 메서드를 구분하기 위해 사용할 암시적 객체인 IntMarker, StringMarker
  • .type : 싱글턴 객체의 타입을 참조
  • [궁금한 사항] IntMarker.type, StringMarker.type으로 각 매개변수화한 타입의 타입인자를 구분하는 것 같은데, IntMarker,StringMarker가 아닌 다른 이름으로 해도 결과에 변함이 없습니다. 암시적 객체 선언시 타입을 부여한 것도 아니고, IntMarker, StringMarker가 정의되어있는 키워드나 객체도 아닌데 각각 Int와 String으로 참조를 어떻게 하는지 궁금합니다.
  • 암시적 인자에 Int나 String 같이 직접적으로 일반적인 타입을 사용하지 않는 이유
  • 첫번째 모듈 : 현재 범위 안에 String 타입의 암시적 '기본'값을 정의해두고, String 타입의 암시적 인자를 사용
  • 두번째 모듈 : 첫번째 모듈의 범위 안에서 자신만의 암시적 String 인자를 정의

(1) 두번째 모듈에서 암시적'기본'값을 정의하지 않고, 사용자가 애플리케이션에 따라 적절한 기본값을 정의하도록 하는 경우 -> 사용자가 기본값을 정의하지 않는 경우, 다른 모듈의 암시적 String을 사용하게 되고, 프로그램이 예상치 못한 동작을 할 수 있음

(2) 사용자가 두번째 암시적 값을 정의하는 경우 범위안에 타입이 같은 두 암시적 값이 존재하여 모호성이 생김 -> 컴파일 오류 발생

  • [궁금한 사항] 바로 위에 설명한 암시적 인자에 일반적인 타입을 사용하지 않는 이유에 대해 자세히 알고싶습니다. 예제코드를 찾아보려고 했는데 쉽지가 않네요..

5.2.6 오류 메시지 개선하기

  • CanBuildFrom : 시작할 때 사용했던 컬렉션과 동일한 타입의 컬렉션을 반환하고 싶은 경우 전달하는 빌더
  • CanBuildFrom 정의가 존재하지 않는 타입에 map을 사용
scala> case class ListWrapper(list: List[Int])
defined class ListWrapper

scala> List(1,2,3).map[Int, ListWrapper](_ * 2)
<console>:14: error: Cannot construct a collection of type ListWrapper with elements of type Int based on a collection of type List[Int].
       List(1,2,3).map[Int, ListWrapper](_ * 2)
  • 위 메시지는 컴파일러가 암시적 인자에 대한 암시적 값을 찾을 수 없을때 일반적으로 내놓는 오류 메시지와 다름!
  • scala.annotation.implicitNotFound 애노테이션을 암시적 인자를 만족시키기 위해 암시적 값으로 사용할 의도가 있는 타입에 붙여야 함

5.2.7 유령 타입

  • 유령 타입(Phantom Type) : 타입은 존재하지만 인스턴스 정의는 없는 타입
  • 암시와는 관계가 없지만, 어떠한 일을 순차적으로 혹은 특정 순서로 진행해야 할 경우 작업흐름을 정의할 때 유용함
  • 유령타입은 타입이 존재하는지 존재여부만 확인함 -> '표식'
// 기본적인 Phantom Type 사용
package progscala2.implicits.payroll

// 봉인된 트레이트 사용 => 다른 파일에서 구현할 수 없는 '표식'의 역할
sealed trait PreTaxDeductions
sealed trait PostTaxDeductions
sealed trait Final

case class Employee(
                   name: String,
                   annualSalary: Float,
                   taxRate: Float,
                   insurancePremiumsPerPayPeriod: Float,
                   _401kDeductionRate: Float,
                   postTaxDeductions: Float)

case class Pay[Step](employee: Employee, netPay: Float)

// Payroll 객체에서 선언된 순서대로 메서드를 호출해야 함!
object Payroll {
  // employee -> Pay[PreTaxDeductions]
  def start(employee: Employee): Pay[PreTaxDeductions] =
    Pay[PreTaxDeductions](employee, employee.annualSalary / 26.0F)

  // Pay[PreTaxDeductions] -> Pay[PreTaxDeductions]
  def minusInsurance(pay: Pay[PreTaxDeductions]): Pay[PreTaxDeductions] = {
    val newNet = pay.netPay - pay.employee.insurancePremiumsPerPayPeriod
    pay copy (netPay = newNet)
  } 
  // Pay[PreTaxDeductions] -> Pay[PreTaxDeductions]
  def minus401k(pay: Pay[PreTaxDeductions]): Pay[PreTaxDeductions] = {
    val newNet = pay.netPay - (pay.employee._401kDeductionRate * pay.netPay)
    pay copy (netPay = newNet)
  }

  // Pay[PreTaxDeductions] -> Pay[PostTaxDeductions]
  def minusTax(pay: Pay[PreTaxDeductions]): Pay[PostTaxDeductions] = {
    val newNet = pay.netPay - (pay.employee.taxRate * pay.netPay)
    pay copy (netPay = newNet)
  }

  // Pay[PostTaxDeductions] -> Pay[Final]
  def minusFinalDeduction(pay: Pay[PostTaxDeductions]): Pay[Final] = {
    val newNet = pay.netPay - pay.employee.postTaxDeductions
    pay copy (netPay = newNet)
  }
}

object CalculatePayroll {
  def main(args: Array[String]) = {
    val e = Employee("inhack", 100000.0F, 0.25F, 200F, 0.10F, 0.05F)
    val pay1 = Payroll start e
    val pay2 = Payroll minus401k pay1
    val pay3 = Payroll minusInsurance pay2
    val pay4 = Payroll minusTax pay3
    val pay = Payroll minusFinalDeduction pay4
    val twoWeekGross = e.annualSalary / 26.0F
    val twoWeekNet = pay.netPay
    val percent = (twoWeekNet / twoWeekGross) * 100
    println(s"For ${e.name}, the gross vs. net pay every 2 weeks is:")
    println(f"$$${twoWeekGross}%.2f vs. $$${twoWeekNet}%.2f of ${percent}%.1f%%")
  }
}
// Pipeline 연산자를 활용
package progscala2.implicits.payroll

object Pipeline {
  implicit class toPIped[V](value: V){
    def |>[R] (f: V => R) = f(value)
  }
}

object CalculatePayroll2{
  def main(args: Array[String]) = {
    import Pipeline._
    import Payroll._

    val e = Employee("inhack", 100000.0F, 0.25F, 200F, 0.10F, 0.05F)
    val pay = start(e)  |>
      minus401k         |>
      minusInsurance    |>
      minusTax          |>
      minusFinalDeduction
    val twoWeekGross = e.annualSalary / 26.0F
    val twoWeekNet = pay.netPay
    val percent = (twoWeekNet / twoWeekGross) * 100
    println(s"For ${e.name}, the gross vs. net pay every 2 weeks is:")
    println(f"$$${twoWeekGross}%.2f vs. $$${twoWeekNet}%.2f of ${percent}%.1f%%")


  }
}
  • 위 코드의 파이프라인( |> )은 단순히 토큰을 재배열하는 역할을 함
  • pay1 |> Payroll.minus401k --> Payroll.minus401k(pay1)

5.2.8 암시적 인자를 처리하기 위한 규칙

  • 암시적 인자 처리 규칙
  • (1) 마지막 인자 목록에만 암시적 인자가 들어갈 수 있음
scala> class Bad1 {
     | def m(i: Int, implicit s: String) = "boo"
<console>:2: error: identifier expected but 'implicit' found.
def m(i: Int, implicit s: String) = "boo"
              ^

scala> class Bad2 {
     | def m(i: Int)(j:Int, implicit s: String) = "boo"
<console>:2: error: identifier expected but 'implicit' found.
def m(i: Int)(j:Int, implicit s: String) = "boo"
  • (2) implicit 키워드는 인자 목록의 맨 처음에 와야하며, 한번만 사용할 수 있음 / 인자 목록 안에서 암시적 인자 다음에 비암시적 인자가 위치할 수 없음
scala> class Bad3 {
     | def m(i: Int)(implicit s1: String)(implicit s2: String) = "boo"
<console>:2: error: '=' expected but '(' found.
def m(i: Int)(implicit s1: String)(implicit s2: String) = "boo"
                                  ^

scala> class Bad4 {
     | def m(i: Int)(implicit s: String)(j: Int) = "boo"
<console>:2: error: '=' expected but '(' found.
def m(i: Int)(implicit s: String)(j: Int) = "boo"
  • (3) 인자 목록이 implicit 키워드로 시작할 경우, 그 인자 목록 안의 모든 인자는 암시적 인자가 됨
scala> class Good1 {
     | def m(i: Int)(implicit s1: String, d: Double, s2: String) = "boo"
     | }
defined class Good1
Clone this wiki locally