-
[스칼라] 추상 멤버스칼라 2022. 2. 25. 00:36
Programming in scala 4th edition 20장
1. 추상 멤버
추상멤버: 완전히 정의되지 않은 클래스나 트레이트의 멤버
type, def, val, var 로 정의되는 타입, 함수, 변수들은 추상 멤버로 정의할 수 있다.
trait Abstract { type T def transform(x: T): T val initial: T var current: T } class AbstractImpl extends Abstract { type T = String override def transform(x: T): String = x + x val initial = "hi" var current = "abcd" }
2. 타입 멤버
추상 타입: 클래스나 트레이트 내부에 정의 없이 선언만 된 형태의 타입. (추상 클래스나 트레이트를 추상 타입이라고 하지는 않는다.)
타입 멤버는 어떤 타입에 대해서 새로운 이름을 붙인다. 타입 이름이 너무 길거나 의미를 명확하게 할 때 사용한다.
3. 추상 val
추상 val 은 변수 명과 타입을 정의한다. 추상 val은 추상 def로 선언한 변수와 비슷해 보이지만 추상 def로 선언한 변수는 이후 구현시 메소드로 오버라이드 해버릴 수도 있다. 추상 val은 val로 정의되어있으므로 구현시 반드시 변수임을 보장할 수 있다.
4. 추상 var
추상 var 또한 변수명과 타입을 정의한다. var를 선언하면 기본적으로 필드가 할당되고 getter, setter가 생성되는데 추상 var는 getter와 setter가 생성되고 필드는 생성되지 않는다.
trait Time { var hour: Int var minute: Int } // 위의 Time은 AbstractTime과 정확히 같다. trait AbstractTime { def hour(): Int def hour_=(h: Int) def minute(): Int def minute_=(m: Int) }
5. 추상 val 초기화
추상 val은 슈퍼클래스 파라미터처럼 사용할 수 있다. 예를들어 트레이트에서 추상 val을 정의해두고 서브클래스에서 추상 val을 초기화 시키는 방식으로 사용할 수 있다.
trait RationalTrait { val numerArg: Int val denomArg: Int } new RationalTrait { override val numerArg: Int = 1 override val denomArg: Int = 2 }
RationalTrait에는 numerArg, denomArg 두 가지의 추상 val이 있고 아래의 new RationalTrait에서는 익명 클래스를 만들었다. 이 때 numerArg, denomArg를 구체화했다.
클래스 파라미터를 쓰는 방식과 추상 val을 이용한 방식의 차이점: 초기화 순서에 차이가 있다.
클래스 파라미터는 인스턴스를 만들기 전에 계산이 끝난 형태로 할당되지만 추상 val은 인스턴스를 만든 후에야 계산이 이루어진다.
trait RationalTrait { val numerArg: Int val denomArg: Int require(denomArg != 0) private val g = gcd(numerArg, denomArg) } new RationalTrait { override val numerArg: Int = 1 override val denomArg: Int = 2 }
위의 예제에서 new RationalTrait으로 익명 클래스 인스턴스를 만들면 IlligalArgumentException이 발생한다. require을 통과하지 못하는 것인데 추상 val 을 초기화 하는 시점이 익명 클래스 인스턴스를 생성하는 시점 이후이기 때문에 denomArg 가 0인 상태로 require문을 실행하고 여기서 에러가 발생하게 된다. 이를 해결하기위한 방법이 두가지 있다.
필드를 미리 초기화하기
스칼라에서는 필드 정의를 생성자 호출 앞에 두는 방법으로 필드를 미리 초기화 할 수 있다.
new { val numerArg: Int = 1 val denomArg: Int = 2 } with RationalTrait
class A extends { val numerArg = 10 val denomArg = 20 } with RationalTrait
익명 클래스 인스턴스를 생성할 때나 클래스를 정의할 때 위와 같이 추상 val을 미리 초기화해서 사용할 수 있다.
지연계산 val 변수
스칼라에서는 lazy val 을 정의할 수 있다. lazy val은 처음 이 변수가 사용될 때 비로소 초기화 표현식을 계산한다.
object NoLazyVal { val value = {println("initialization"); 0} } NoLazyVal // initialization 출력됨 object LazyVal { lazy val value = {println("initialization"); 0} } LazyVal // initialization 출력되지않음 LazyVal.value // initialization 출력됨
numer, denom에 지연 초기화를 활용하면 아래와 같다.
trait LazyRationalTrait { val numerArg: Int val denomArg: Int lazy val numer = numerArg / g lazy val denom = denomArg / g override def toString = s"$numer / $denom" private lazy val g = { require(denomArg != 0) gcd(numerArg, denomArg) } private def gcd(a: Int, b: Int): Int = { if(b == 0) a else gcd(b, a % b) } } val x = 2 new LazyRationalTrait { override val numerArg: Int = 1 + x override val denomArg: Int = 2 + x }
6. 추상 타입
추상 타입은 타입을 할당할 수 있는 변수를 서브 클래스에서 정의하도록 선언해둔 것이다.
class Food abstract class Animal { def eat(food: Food) } class Grass extends Food class Cow extends Animal { def eat(food: Grass): Unit = { print("cow eat grass.") } }
위와 같은 경우에 Cow의 eat은 컴파일에러를 발생시킨다. Animal은 Food를 인자로 받는 eat을 정의해 두었는데 Cow는 Grass를 인자로 받도록 하는 eat을 정의하려고 하기 때문이다. 이럴 경우에 추상 타입을 하나 정의해서 이런 문제를 해결할 수 있다.
class Food abstract class Animal { type SuitableFood <: Food def eat(food: SuitableFood) } class Grass extends Food class Cow extends Animal { type SuitableFood = Grass def eat(food: SuitableFood): Unit = { print("cow eat grass.") } } class Fish extends Food val c = new Cow() c.eat(new Grass) c.eat(new Fish)
Food를 상속해서 만들어야만 하는 타입 SuitableFood를 정의해 이 추상 타입을 서브클래스에서 정의하도록 하고, 그 타입을 사용하도록 하면 Cow와 Grass의 관계를 정의하면서 문법적으로도 오류를 일으키지 않는다.
7. 경로에 의존하는 타입
c.eat(new Fish)를 실행하면 아래와 같은 에러 메시지가 나온다.
type mismatch; found : Fish required: c.SuitableFood
c.SuitableFood 같은 타입을 경로에 의존하는 타입이라고 한다.
8. 세분화한 타입
클래스 A와 클래스 B의 상속 관계를 정의하려면 extends 키워드로 정의하곤한다. 이것을 이름에 의한 서브타입이라고 한다. 스칼라는 구조적인 서브타입도 지원한다.
- 이름에 의한 서브타입: 명시적인 상속 선언
- 구조적인 서브타입: 두 타입의 멤버가 호환될 때 생기는 관계
class Food abstract class Animal { type SuitableFood <: Food def eat(food: SuitableFood) {} } class Grass extends Food class Cow extends Animal { type SuitableFood = Grass override def eat(food: SuitableFood): Unit = { print("cow eat grass.") } } class Pig extends Animal { type SuitableFood = Grass override def eat(food: SuitableFood) = { print("pig eat grass.") } } var grassEaters: List[Animal { type SuitableFood = Grass }] = List() val p = new Pig() val c = new Cow() grassEaters = c :: p :: grassEaters grassEaters // val res6: List[Animal{type SuitableFood = Grass}] = List(Cow@2978ba59, Pig@2f587e4f)
새로 타입을 선언하는 대신에 주식을 Grass로 하는 동물들에 대한 리스트를 작성하려면 위와 같이 구조적인 서브 타입을 활용할 수도 있다.
9. 열거형
스칼라는 Enum 타입에 대해서 미리 Enumeration 클래스를 정의해뒀다. Enum을 쓰고싶으면 Enumeration을 상속받아 활용하면 된다.
object Color extends Enumeration { val red, green, blue = Value } print(Color.red, Color.green, Color.blue) // (red,green,blue)
Value는 Enumeration 클래스 안에 정의된 내부 클래스이고 Value라는 파라미터 없는 메서드를 정의해서 Value의 인스턴스를 리턴하도록 했다. Color.red, Color.green의 타입은 Color.Value이다.
'스칼라' 카테고리의 다른 글
[스칼라] 리스트 구현 (0) 2022.02.27 [스칼라] 암시적 변환과 암시적 파라미터 (implicit) (0) 2022.02.26 [스칼라] getter setter (0) 2022.02.23 [스칼라] 컬렉션 (0) 2022.02.23 [스칼라] 리스트 (0) 2022.02.22