독서관련/Programming in Scala

Scala CH20. 추상 멤버

ColinKang 2020. 3. 29. 13:21
아래의 글은 마틴 오더스키,렉스 스푼,빌 베너스 공저 / 오현석,이동욱,반영록 공역, 『Programming in Scala 3/e』,에이콘출판사(2017), CH01의 내용을 기반으로 작성하였습니다.

 

추상 멤버 : 클래스나 트레이트의 멤버가 그 클래스 안에 완전한 정의를 갖고 있지 않으면 추상 클래스

추상 멤버는 그 멤버가 정의된 클래스를 상속한 서브클래스에서 구현해야함.

자바에서는 추상 메소드를 정의

총 4가지 종류의 추상 멤버,ㅣ val, var, 메소드, 타입

미리 초기화된 필드 : pre-initialized field

지연 : lazy val

경로 의존적인 타입 : path dependent type

열거형 : enumeration

 

1. 간단한 추상 멤버

추상타입 T, 추상 메소드, 추상 val 추상 var

trait Abstract {

    type T

    def transform(x: T) T

    val initial: T

    var current: T

}

 

 

class Concrete extends Abstract {

    type T = String

    def transform(x: String) = x + x

    val initial = "Hi"

    var current = initial

}

 

2. 타입멤버

추상 타입은 클래스나 트레이트의 멤버로 정의없이 선언된 타입

트레이트는 정의상 추상적이지만, 스칼라에서 추상타입이라고 부르지 않음.

스칼라의 추상타입은 Abstract 트레이트의 T 타입 처럼 항상 어떤 클래스나 트레이트의 멤버이다

alias 로 생각할 수도 있음

 

3. 추상 val

val initial: String

이름과 타입은 추지만 값은 지정하지 않음.

변수에 정확한 값은 모르지만, 그 변수가 클래스의 인스턴스에서 변하지 않으리란 사실은 알고 있을 때 추상 val을 사용한다.

initial의 추상 val이면 클라이언트는 obj.initial을 사용할 때 마다 같은 값을 얻을 수 있음을 확신

intial이 추상 메소드라면, 그런 보장은 없음.

추상 val은 그에 대한 구현에 제약을 가한다. 구현시 val 정의를 사용해야한다는 제약이다.

 

4. 추상 var

trait AbstractTime {

    var hour: Int

    var minute: Int

}

 

 

getter/setter 스칼라가 만들어줌.

5. 추상  val 초기화

추상 val은 때때로 슈퍼 클래스 파라미터와 같은 역할을 한다.

슈퍼 클래스에 빠진 미세한 부분을 서브 클래스에 전달 할 수 있는 수단

트레이트에서는 파라미터를 넘길 생성자가 없기 때문이다

트레이트를 파라미터화 할려면 서브 클래스에서 구현하는 추상 val을 통해서 수행

trait RationalTrait {

    val numerArg: Int // 유리수 분자

    var denomArg: Int  // 분모

}

 

 

new RationalTrait(

    val numerArg = 1

    val denomArg = 2

}

// 익명클래스

 

 

new RationalTrait {

    val numerArg = expr1

    val denomArg = expr2

}

// 익명 클래스 초기화 중에 표현식 expr1, expr2 계산

// 익명 클래스는 RationalTrait 다음에 초기화 됨

numberArg와 demonArg값은 RationalTrait 초기화 전에는 사용가능하지 않기 때문에 0을 돌려 받음.

 

 

 

 

 

// 아래의 버전에서는 문제가 됨

trait RationalTrait {

    val numerArg: Int

    var denumArg: Int

    require(denomArg != 0)

    private val g = gcd(numerArg, denomArg)

    val numer = numerArg / g

    var denom = denumArg / g

    private def gcd(a: Int, b: Int): Int =

        if (b == 0) a else gcd(b, a % b)

    override def toString = numer + "/" + denum

}

 

 

val x = 2

new RationalTrait(

    val numerArg = 1 * x

    var denumArg = 2 * x

)

 

 

requirement failed Exception

 

 

RationalTrait 클래스 초기화 할때 denumArg 값이 0이기 때문

클래스의 파라미터 인자는 클래스 생성자에 전달되기 전에 계산한다

반면 서브클래스의 val 정의를 계산하는 것은 슈퍼 클래스를 초기화 한 다음에 이뤄진다.

이에 대한 해결로 필드를 미리 초기화 하는 것과 지연 val 하는 방법 두 가지가 있음. (방법이 미리 다 해 놓거나 아님 다 초기화 되고 하거나...)

 

 

필드를 미리 초기화

중괄호를 이용하여 슈퍼클래스 호출 전에 서브클래스 필드 초기화

미리 초기화 한 필드는 클래스 생성자의 인자와 비슷하게 동작함.

new {

    val numberArg = 1 * x

    val denumArg = 2 * x

} with RationalTrait

 

 

object twoThird extends {

    val numerArg = 2

    var denumArg = 3

} with RationalTrait

 

 

class RationalClass(n: Int, d: Int) extends {

    val numerArg = n

    val denomArg = d

} with RationalTrait {

    def + (that: RationalClass) = new RationalClass(

        numer * that.denom + that.number * denom,

        denom * that.denom

    )

}

 

지연 계산 val 변수

내가 초기화 하는게 아닌 system이 초기화하게 지연, 

어떤 val 정의를 lazy 하게 만들면 됨.

object Demo {

    lazy val x = { println("initilized"); "done"}

}

 

 

Demo.x 선택시 println 부분 호출됨.

 

 

trait LazyRationalTrait {

    val numerArg: Int

    val denomArg: Int

    lazy val numer = numberArg: g

    lazy val denom = denomArg: g

    override def toString = numer + "/" + denum

    private lazy g = (

        require(denomArg != 0)

        gcd(numerArg, denomArg)

    )

    private def gcd(a: Int, a: Int): Int =

        if (b == 0) a else gcd(b, a % b)

}

 

6. 추상 타입

추상 타입 선언은 서브 클래스에서 구체적으로 정해야 하는 어떤 대상에 대한 빈공간을 마련해 두는 것

T는 선언 시점에는 어떤 타입인지 알려져 있지 않은 타입을 참조하기 위해 사용함

class Food

abstract class Animal {

    type SuitableFood <: Food

    // SuitableFood의 상위 바운스는 Food

    def eat(food: SuitableFood)

}

 

 

class Grass extends Food

class Cow extends Animal {

    type SuitableFood = Grass

    override def eat(food: Grass) = {}

}

 

 

class Fish extends Food

var bessy: Animal = new Cow

bessy eat (new Fish)

 

 

error: type mismatch

required: bessy.SuitableFood

 

 

//의미 : messy가 참조하는 객체의 멤버인 SuitableType

 

7. 경로에 의존하는 타입

마지막 오류 메세지는 eat 메소드가 요구하는 타입인 bessy.SuitableFood

경로에 의존하는 타입 path-dependent type

경론ㄴ 객체에 대한 참조를 의미, 참조변수 : 싱글톤 객체 이름

문제는 eat 메소드에 넘기는 SuitableFood 객체의 타입인 bessy.SuitableFood는 eat 의 파라메터 타입인 lassie.suitableFood와는 같지 않음.

하지만 두 Dog의 경우는 실제로 동일한 타입임.

경로 의존 타입은 자바의 내부 클래스 타입과 문법이 비슷하지만, 

경로 의존 타입은 외부 객체에 이름을 붙이는 반면

내부 클래스 타입 이름의 외부 클래스에 이름을 붙인다는 점

자바와 마찬가지로 스칼라에서도 내부 클래스 인스턴스에는 그 인스턴스를 둘러 싼 클래스와 종류

외부 클래스 인스턴스를 지정하지 않고 내부 클래스만 인스턴트.

 

다른 방법

o1.Inner 타입은 특정 이후 후 인스턴스를 가져오기

경로 의존 타입은 자바의 클래스 타입과 비슷하지만, 

new o1.Inner

 

8. 세분화한 타입

어떤 클래스 A가 다른 클래스 B를 상속할 때, 전자 (A)가 후자(B)dml 이름에 의한 서브타입 nominal subtype

구조적인 서브 타이핑 지원 : structual subtyping

세분화한 타입을 사용하면 됨 : refinement type

Animal {type SuitableFood = Grass}

 

 

// 목초리

class Pasture {

    var animals: List[Animal {type SuitableFood = Grass}] = Nil

}

 

9. 열거혐

경로 의존적 타입의 재미있는 활용 ; 스칼라의 열거형 : enumeration type

object Color extends Enumeration {

    val Red = Value

    val Grren = Value

    val Blue = Value

}

 

 

object Direction extends Enumeration {

    val North = Value("North")

    val East = Value("East")

    val South = Value("South")

    val West = Value("West")

}

 

 

Direction.Value 과 Color.Value는 다름

 

10. 사례 연구

object Converter {

    var exchangeRate = Map (

        "USD" -> Map("USD" -> 1.0, "EUR" -> 0.7596, "JPY" -> 1.211, "CHF" -> 1.223),

        "EUR" -> Map("USD" -> 1.316, "EUR" -> 1.0, "JPY" -> 1.594, "CHF" -> 1.623),

        "JPY" -> Map("USD" -> 0.8257, "EUR" -> 0.6272, "JPY" -> 1.0, "CHF" -> 1.018),

        "CHF" -> Map("USD" -> 0.8108, "EUR" -> 0.6160, "JPY" -> 0.982, "CHF" -> 1.0)

    )

}

 

 

abstract class CurrentyZone {

    type Currency <: AbstractCurrency

    def make(x: Long): Currency

     

    abstract class AbstractCurrency {

        val amount: Long

        def designation: String

 

 

        def + (this: Currency): Currency = make(this.amount + that.amount)

         

        def * (x: Double): Currency = make(this.amount * x).toLong)

        def - (this: Currency): Currency = make(this.amount - that.amount)

        def / (this: Double) = make((this.amount / that).toLong)

        def / (this: Currency): Currency = make(this.amount / that.amount)

 

 

        def from(other: CurrencyZone#AbstractCurrency): Currency =

            make(math.round(

                other.amount.toDouble * Converter.exchangeRate

                    (other.designation)(this.designation)

        private def decimal(n: Long): int =

            if (n == 1) 0 else 1 + decimals(n / 10)

         

        override def toString =

            ((amount.toDouble / CurrencyUnit.amount.toDouble)

                formatted ("%." + decimals(CurrencyUnit.amount) + "f")

                + "" + designation)

    }

 

 

    var CurrencyUnit: Currency

}

 

 

Japan.Yen from US.Dollar * 100

 

 

Europe.Euro from res16