jam 블로그

러닝스칼라 10장 - 고급 타입 특징 본문

IT Book Study/러닝 스칼라

러닝스칼라 10장 - 고급 타입 특징

kid1412 2020. 9. 28. 23:43
728x90

스칼라 데이터 타입 특징을 깊게 알아보자.
이 장을 이해함으로써 스칼라 API, 라이브러리 동작 방식을 이해할 수 있다.

튜플과 함숫값 클래스

튜플은 TupleX[Y] 케이스 클래스의 인스턴스로 구현

X: 1 <= X <= 22, 튜플의 입력 매개변수 개수

Y: 타입 매개변수

productArity: 튜플의 매개변수 개수를 반환하는 함수, 튜플이 상속받는 ProductX 트레이트에 정의됨

scala> val x = (10, 20)
x: (Int, Int) = (10,20)

scala> val x: (Int, Int) = Tuple2(10, 20)
x: (Int, Int) = (10,20)

scala> println("Does the arity = 2? " + (x.productArity == 2))
Does the arity = 2? true

함수값은 FunctionX[Y] 트레이트를 확장한 익명 클래스의 인스턴스로 구현된다.

X: 매개변수 개수

Y: 타입 매개변수, 첫 번째는 반환값 타입, 두 번째부터는 매개변수의 타입
스칼라 컴파일러가 함수 리터럴을 FunctionX를 확장하는 새로운 클래스의 apply() 메소드의 body로 전환한다.
이는 모든 함수가 클래스 메소드로 구현되도록 제한하는 JVM과 호환되도록 해준다.

scala> val hello1 = (n: String) => s"Hello, $n"
hello1: String => String = $$Lambda$839/0x000000080104a040@3820cfe

scala> val h1 = hello1("Function Literals")
h1: String = Hello, Function Literals

scala> val hello2 = new Function1[String, String] {
     |   def apply(n: String) = s"Hello, $n"
     | }
hello2: String => String = <function1>

scala> val h2 = hello2("Functional Instances")
h2: String = Hello, Functional Instances

scala> println(s"hello1 = $hello1, hello2 = $hello2")
hello1 = $line6.$read$$iw$$iw$$$Lambda$839/0x000000080104a040@3820cfe, hello2 = <function1>

Function1 트레이트는 FunctionX (X != 1)에는 없는 2개의 메소드를 가지고 있다.

andThen: 두 함수값으로부터 새로운 함수값을 생성, 왼쪽 인스턴스에서 오른쪽에 있는 인스턴스를 실행

compose: 반대 방향으로 동작

scala> val doubler = (i: Int) => i*2
doubler: Int => Int = $$Lambda$970/0x00000008010c3040@22b10124

scala> val plus3 = (i: Int) => i+3
plus3: Int => Int = $$Lambda$971/0x00000008010c3840@79ae3fb1

scala> val append = (doubler andThen plus3)(1)
append: Int = 5

scala> val prepend = (doubler compose plus3)(1)
prepend: Int = 8

묵시적 매개변수 (implicit parameter)

nonimplicit 매개변수와 별개로 implicit한 매개변수 그룹을 정의한다.
implicit 매개변수에 명시적으로 값을 넣어도 동작한다.

scala> object Doubly {
     |   def print(num: Double)(implicit fmt: String) = {
     |     println(fmt format num)
     |   }
     | }
defined object Doubly

scala> case class USD(amount: Double) {
     |   implicit val printFmt = "%.2f"
     |   def print = Doubly.print(amount)
     | }
defined class USD

scala> new USD(81.924).print
81.92

scala> Doubly.print(3.724)("%.1f")
3.7

묵시적 클래스 (implicit class)

묵시적 클래스는 특정 클래스의 자동 전환을 제공

// Int를 Fishes 클래스로 자동 전환
scala> object IntUtils {
     |   implicit class Fishies(val x: Int) {
     |     def fishes = "Fish" * x
     |   }
     | }
defined object IntUtils

scala> import IntUtils._
import IntUtils._

scala> println(3.fishes)
FishFishFish

묵시적 클래스 제약 사항

  1. 다른 객체, 클래스, 트레이트 안에 정의되어야 한다.
  2. 하나의 nonimplicit 클래스 인수를 받아야 한다.
  3. 현재 네임스페이스의 다른 객체, 클래스, 트레이트와 클래스명이 겹치면 안된다.

케이스 클래스는 자동으로 생성된 동반 객체와 이름이 겹치기 때문에 묵시적 클래스로 사용할 수 없다.

객체 내에 구현하는 것이 가장 좋다. 객체 내에 정의할 경우 쉽게 임포트할 수 있다.

여러가지 묵시적 클래스를 가진 scala.Predef 객체는 자동으로 네임스페이스에 추가된다.

예를 들어서 x -> y 연산자는 아래와 같이 정의되어 자동으로 적용된다.

// 간략하게 적음
// 1 -> "a" Tuple2(1,"a")
implicit class ArrowAssoc[A](x: A) {
  def ->[B](y: B) = Tuple2(x, y)
}

타입

클래스, 트레이트 타입은 사용가능하지만 객체는 타입으로 간주하지 않는다.

scala> class A
defined class A

scala> trait B
defined trait B

scala> val a:A = new A
a: A = A@4860627a

scala> val b:B = new B{}
b: B = $anon$1@79486f38

타입 별칭 (type alias)

기존 타입을 새롭게 명명하는 것

타입 별칭은 객체, 클래스, 트레이트 내부에서만 정의가능

객체는 타입 별칭을 생성하는 데 사용할 수 없음

type <식별자>[타입 매개변수] = <타입명>[타입 매개변수]

scala> object TypeFun {
     |   type Whole = Int
     |   val x: Whole = 5
     |
     |   type UserInfo = Tuple2[Int, String]
     |   val u: UserInfo = new UserInfo(123, "George")
     |
     |   type T3[A,B,C] = Tuple3[A,B,C]
     |   val things = new T3(1, 'a', true)
     | }
defined object TypeFun

scala> val x = TypeFun.x
x: TypeFun.Whole = 5

scala> val u = TypeFun.u
u: TypeFun.UserInfo = (123,George)

scala> val things = TypeFun.things
things: (Int, Char, Boolean) = (1,a,true)

추상 타입 (abstract type)

추상 클래스, 트레이트를 mixin한 서브클래스가 구현해야만 하는 타입을 선언하기 위해 사용
허용 가능한 타입의 범위를 지정하기 위해 타입 매개변수로 사용된다.
인스턴스를 생성하는데 사용할 수 없다.

scala> class User(val name: String)
defined class User

scala> trait Factory { type A; def create: A }
defined trait Factory

scala> trait UserFactory extends Factory {
     |   type A = User
     |   def create = new User("")
     | }
defined trait UserFactory
//위의 코드와 동일하기 동작하지만 더 간결함
scala> trait Factory[A] { def create: A }
defined trait Factory

scala> trait UserFactory extends Factory[User] { def create = new User("") }
defined trait UserFactory

경계가 있는 타입 (bounded type)

상한 경계

<식별자> <: <상한 경계 타입>

해당 타입 또는 서브타입으로 제한

scala> class BaseUser (val name: String)
defined class BaseUser

scala> class Admin(name: String, val level: String) extends BaseUser(name)
defined class Admin

scala> class Customer(name: String) extends BaseUser(name)
defined class Customer

scala> class PreferredCustomer(name: String) extends Customer(name)
defined class PreferredCustomer
check: [A <: BaseUser](u: A)Unit

scala> check(new Customer("Fred"))

scala> check(new Admin("", "strict"))
Fail!

하한 경계

<식별자> >: <하한 경계 타입>

해당 타입 또는 그 타입이 확장한 타입으로 제한

scala> def recruit[A >: Customer](u: Customer): A = u match {
     |   case p: PreferredCustomer => new PreferredCustomer(u.name)
     |   case u: Customer => new Customer(u.name)
     | }
recruit: [A >: Customer](u: Customer)A

scala> val customer = recruit(new Customer("Fred"))
customer: Customer = Customer@52d7889a

scala> val preferred = recruit(new PreferredCustomer("George"))
preferred: Customer = PreferredCustomer@32dd05af/
scala> abstract class Card {
     |   type UserType <: BaseUser
     |   def verify(u: UserType): Boolean
     | }
defined class Card

scala> class SecurityCard extends Card {
     |   type UserType = Admin
     |   def verify(u: Admin) = true
     | }
defined class SecurityCard

scala> val v1 = new SecurityCard().verify(new Admin("George", "high"))
v1: Boolean = true

타입 가변성 (type variance)

타입 가변성을 사용하면 타입 매개변수를 더 유연하게 사용할 수 있다.

scala> class Car { override def toString = "Car()" }
defined class Car

scala> class Volvo extends Car { override def toString = "Volvo()" }
defined class Volvo
// Volvo 인스턴스가 타입 Car의 값으로 할당될 수 있음
scala> val c: Car = new Volvo()
c: Car = Volvo()

// Item[Volvo] 인스턴스는 Item[Car]의 값으로 할당될 수 없다.
scala> val c: Item[Car] = new Item[Volvo](new Volvo)
                          ^
       error: type mismatch;
        found   : Item[Volvo]
        required: Item[Car]
       Note: Volvo <: Car, but class Item is invariant in type A.
       You may wish to define A as +A instead. (SLS 4.5)

타입 매개변수 앞에 +를 붙이면 공변(covariant)하게 만들 수 있다.

공변성은 타입 매개변수가 그 타입이 확장한 타입으로 변할 수 있는 성질

scala> case class Item[+A](a: A) { def get: A = a }
defined class Item

scala> val c: Item[Car] = new Item[Volvo](new Volvo)
c: Item[Car] = Item(Volvo())

scala> val auto = c.get
auto: Car = Volvo()

타입 매개변수 앞에 -를 붙이면 반공변(contravariant)하게 만들 수 있다.

반공변성은 타입 매개변수가 서브타입으로 변할 수 있는 성질

메소드 매개변수에 사용된 타입 매개변수는 반공변성을 가진다.

scala> class Check[+A] { def check(a: A) = {} }
                                   ^
       error: covariant type A occurs in contravariant position in type A of value a

scala> class Check[-A] { def check(a: A) = {} }
defined class Check
scala> class Car; class Volvo extends Car; class VolvoWagon extends Volvo
defined class Car
defined class Volvo
defined class VolvoWagon

scala> class Item[+A](a: A) { def get: A = a }
defined class Item

scala> class Check[-A] { def check(a: A) = {} }
defined class Check

scala> def item(v: Item[Volvo]) { val c: Car = v.get }
item: (v: Item[Volvo])Unit

scala> def check(v: Check[Volvo]) { v.check(new VolvoWagon()) }
check: (v: Check[Volvo])Unit

모든 타입 매개변수를 불변의 상태로 유지하는 것이 더 안전하다.

패키지 객체 (package object)

패키지 객체는 해당 패키지에 위치한 파일 package.scala에 정의되어 네임스페이스에 자동 임포드된다.

// 위치: com/oreilly/package.scala
package object oreilly {
  type Mappy[A, B] = collection.mutable.HashMap[A, B]
}
Comments