추상 val 초기화시 문제점
아래와 같이 (추상 val
을 가진) 트레이트를 인스턴스화하여면, 추상 val
의 정의를 구현해야 한다.
trait RationalTrait {
val numerArg: Int
val denomArg: Int
require(denomArg != 0)
}
위 예제(트레이트)는 분수를 표현하는 트레이트이다. (numerArg
: 분자,denomArg
: 분모) 따라서,denomArg
는 0이 될 수 없다.
만약 위 Rational
이 트레이트(Trait
) 가 아닌 클래스였다면, new Rational(expr1, expr2)
와 같이 구현할 수 있다.
new Rational(expr1, expr2) 실행 순서
expr1
,expr2
식을 계산한다.Ratinal()
을 초기화 한다.
하지만, 트레이트의 경우는 초기화 순서가 반대이다.
new RatinalTrait(expr1, expr2) 실행 순서
RatinalTrait()
를 초기화 한다.expr1
,expr2
를 계산한다.
trait RationalTrait {
val numerArg: Int
val denomArg: Int
require(denomArg != 0)
}
val x: Int = 2
new RationalTrait {
override val numerArg: Int = 1 * x
override val denomArg: Int = 2 * x
// RatinalTrait가 먼저 초기화되면서, denomArg는 default 값인 0을 갖는다.
// require() 에서 오류가 발생한다.
}
RatinalTrait
가 먼저 초기화되면서, denomArg
는 default 값으로 0을 갖게된다.
require(denomArg != 0)
에 의해 오류가 발생한다.
scala> new RationalTrait {
| override val numerArg: Int = 1 * x
| override val denomArg: Int = 2 * x
| }
java.lang.IllegalArgumentException: requirement failed
at scala.Predef$.require(Predef.scala:327)
at RationalTrait.$init$(<console>:4)
... 38 elided
추상 val 초기화하는 방법
추상 val
을 초기화하는 방법에는 2가지가 있다.
- 필드를 미리 초기화하기
- 지연(
lazy
) val 을 사용하기
필드를 미리 초기화하기
슈퍼 클래스를 호출하기전에 서브클래스에서 필드를 초기화하면 된다.
스칼라에서는 미리 필드 정의를 통해 초기화하는 방법이 있다. 중괄호를 이용하여 클래스 생성자 호출 앞에 위치시키면 된다.
trait RationalTrait {
val numerArg: Int
val denomArg: Int
require(denomArg != 0)
}
val x: Int = 2
new {
val numerArg = 1 * x
val denomArg = 1 * x
} with RationalTrait // with 키워드를 사용.
익명 클래스를 통해 예제를 소개했지만, 아래와 같이 이름이 있는 서브 클래스에서도 필드를 미리 초기화 할 수 있다.
class RationalClass(n: Int, d: Int) extends {
val numerArg: Int = 1 * x
val denomArg: Int = 1 * x
} with RationalTrait {
// 분수의 합 기능을 구현한 메소드.
def + (that: RationalClass) = new RationalClass(
numer * that.denom + that.numer * denom,
denom * that.denom
)
}
참고로 미리 초기화하는 경우, 슈퍼클래스가 초기화되기전에 먼저 초기화되므로, this
를 사용할 수 없다.
지연(lazy) val 을 사용하기
lazy val
은 사용하는 시점에 초기화가 된다.
scala> object Demo {
| val x = {
| println("initializing x")
| "done"
| }
| }
defined object Demo
scala> Demo
initializing x
res0: Demo.type = Demo$@71d8cfe7
scala> println(Demo.x)
done
Demo를 초기화하면서 "initializing x"
가 같이 실행된다. (x가 같이 초기화 됨)
lazy val
을 사용한 경우...
scala> object Demo2 {
| lazy val x = {
| println("initializing x")
| "done"
| }
| }
defined object Demo2
scala> Demo2
res2: Demo2.type = Demo2$@348137e8
scala> println(Demo2.x)
initializing x
done
Demo2.x
를 호출하는 시점에 초기화되면서, "initializing x"
가 출력된다. (x가 실행하는 시점에 초기화 된다)
실행시점에 초기화 한다고,val
을 결코 두번 계산하지 않는다. 처음lazy val
을 계산한 값을 저장해두고 있기 때문이다.
그래서, 다시 사용하는 경우 저장해두었던 값을 재사용한다. (def
와의 차이점)
지연 사용하는 값들끼리는 순서가 중요하지 않다. 이는 lazy value의 중요한 특징이다. (단, 부수효과가 없어야 함)
부수 효과가 있는(초기화 순서에 문제가 될 수 있다) 명령형 코드의 경우는 lazy value
와는 어울리지 않는다. 반면, 부수효과(side effect
) 가 없는 함수형 코드에는 어울린다.
지연 함수 언어
스칼라는 지연 정의와 함수형 코드를 조화시키려는 노력을 기울인 첫 언어가 아니다.
모든 값과 파라미터를 지연 연산하는 지연 함수형 프로그래밍 언어(lazy functional programming language
) 라는 분야가 있고, 이런 언어 중 가장 잘 알려진 하스켈(Haskell
) 이 있다.
'Language > Scala' 카테고리의 다른 글
Scala의 열거형(Enumeration type) (0) | 2020.02.23 |
---|---|
Scala 추상 타입 (type T) (0) | 2020.02.23 |
Scala 추상 멤버 (타입멤버, 메소드, val, var) (0) | 2020.02.22 |