6장 functional objects - codeport/scala GitHub Wiki

  • 이번 장에서는 어떤 상태도 갖지 않는 펑셔널 객체를 정의하는 클래스를 설명한다.

6.1 A specification for class Rational

  • Rarional 클래스에 대한 스펙 설명
  • 분수 얘기임.
  • 최종적으로 다음과 같이 되어야 함.
scala> val oneHalf = new rational(1, 2)
oneHalf: Rational = 1/2
scala> val twoThirds = new rational(2, 3)
towThirds: Rational = 2/3
scala> (oneHalf / 7) + (1 - twoThirds)
res0: Rational = 17/42

6.2 Constructing a Rational

class Rational(n: Int, d: Int)
  • class 선언 시 클래스명 뒤에 () 로 class parameter를 명시할 수 있음.
  • 스칼라 컴파일러가 이 parameter를 받는 primary constructor를 만든다.
  • class 내부의 코드 중 field나 method 선언에 해당하지 않는 모든 코드는 primary constructor에 속함.

Immutable object trade-offs

  • 장점
    • 뮤터블보다 쉽다.(변경되는 복잡한 상태가 없다.)
    • 변결될 걱정없이 전달할 수 있다.
    • 상태를 변경할 수 없으므로 동시성 이슈가 없다.
    • 안전한 해쉬테이블 키를 만들 수 있다.
  • 단점
    • 객체를 수정해야 하는 경우 큰 객체를 복사하는 비용이 든다.

6.3 Reimplementing the toString method

  • 보기 편하게 toString을 오버라이드한다.
class Rational(n: Int, d: Int) {
  override def toString = n + "/" + d
}
scala> val x = new Rational(1, 3)
x: Rational = 1/3

6.4 Checking preconditions

  • 분모는 0이 될 수 없지만 현재는 가능하다.
scala> new Rational(5, 0)
res1: Rational = 5/0
  • 메서드나 생성자에 전달한 값의 제약사항을 검사해야 하는데 한자기 방법이 require이다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  override def toString = n + "/" + d
}
  • require 메서드는 불리언 파라미터를 받는다. 파라미터가 true이면 넘어가고 false이면 IllegalArgumentException을 던진다.

6.5 Adding fields

  • Rational의 덧셈을 위해서 add함수가 필요하다. immutable이므로 add함수는 새로운 Rational객체를 반환한다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  override def toString = n + "/" + d
  def add(that: Rational): Rational = new Rational(n * that.d + that.n * d, d * that.d)
}
  • 이 코드는 val d is not a member of Rational이라는 오류가 발생한다.
  • class parameter는 객체 내에서만 사용할 수 있으므로 thatnd에 접근하려면 필드로 만들어 주어야 한다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  val numer: Int = n
  val denom: Int = d
  override def toString = numer + "/" + denom
  def add(that: Rational): Rational = new Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
  )
}
scala> val oneHalf = new rational(1, 2)
oneHalf: Rational = 1/2
scala> val twoThirds = new rational(2, 3)
towThirds: Rational = 2/3
scala> oneHalf add twoThirds
res3: Rational = 7/6
  • 10.6절에 소개될 parametric fields를 이용하면 귀찮게 생성자와 별도로 val/var를 선언할 필요 없음. class Rational(n: Int, d: Int) -> class Rational(val n: Int, val d: Int)

6.6 Self references

  • this로 자기 자신을 참조할 수 있음.
def lessThan(that: Rational) =
  this.numer * that.denom < that.numer * this.denom

6.7 Auxiliary constructors

  • primary constructor가 아닌 생성자는 auxiliary constructor라고 부른다.
  • 예를 들어 Rational을 5/1로 작성하는 대신 5로 작성할 수 있어야 한다.(ex: new Rational(5))
  • auxiliary constructor는 def this(...)로 시작한다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  val numer: Int = n
  val denom: Int = d
  def this(n: Int) = this(n, 1) // auxiliary constructor
  override def toString = numer + "/" + denom
  def add(that: Rational): Rational = new Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
  )
}
scala> val y = new Rational(3)
y: Rational = 3/1
  • auxiliary constructor의 시작은 항상 this(...)형태로 다른 생성자를 호출해야 하고 최종적으로는 primary constructor를 호출해야 한다.
  • 혹시나 primary constructor를 호출하지 않는 경우가 있을까 해서 잠깐 실험을 해 보니 compile 오류가 발생함.
class A( a: Int ) {
  def this( a: Double) = this(a,1)
  def this( a: Double, n: Int ) = this(a)
}

<console>:6: error: called constructor's definition must precede calling constructor's definition
    def this(n: Double) = this(n,1)
  • 즉, 생성자가 호출할 다른 생성자는 자신보다 코드 상에서 앞에 선언되어야 함. 이를 보면 반드시 primary constructor가 실행되리라고 유출할 수 있음.
  • 오직 primary 생성자만 superclass 생성자를 호출할 수 있다.

6.8 Private fields and methods

  • 현재 Rational은 66/42를 11/7로 바꿔주지 않는다. 그래서 분자와 분모를 최대공약수로 나누어 주어야 한다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  private val g = gcd(n.abs, d.abs)
  val numer: Int = n / g
  val denom: Int = d / g
  def this(n: Int) = this(n, 1)
  override def toString = numer + "/" + denom
  def add(that: Rational): Rational = new Rational(
    numer * that.denom + that.numer * denom,
    denom * that.denom
  )
  private def gcd(a: Int, b: Int): Int = 
    if (b == 0) a else gcd(b, a % b)
}
scala> new Rational(66, 42)
res7: Rational = 11/7

6.9 Defining operators

  • 더 편리하게 사용하도록 x + y 형태나 실수인 경우에는 x.add(y)x add y같은 형태로 작성할 수 있다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  private val g = gcd(n.abs, d.abs)
  val numer: Int = n / g
  val denom: Int = d / g1
  def this(n: Int) = this(n, 1)
  def + (that: Rational): Rational =
    new Rational(
      numer * that.denom + that.numer * denom,
      denom * that.denom
    )
  def * (that: Rational): Rational =
    new Rational(numer * that.nemer, denom * that.denom)
  override def toString = numer + "/" + denom
  private def gcd(a: Int, b: Int): Int = 
    if (b == 0) a else gcd(b, a % b)
}
scala> val x = new Rational(1, 2)
x: Rational = 1/2
scala> val y = new Rational(2, 3)
y: Rational = 2/3
scala> x + y
res8: Rational = 7/6
  • 5.8장에서 설명한 연산자 우선순위에 따라 * 연산자가 +보다 우선한다.
scala> x + x * y
res10: Rational = 5/6
scala> (x + x) * y
res11: Rational = 2/3
scala> x + (x * y)
res12: Rational = 5/6

6.10 Identifiers in Scala

  • scala의 identifier는 alphanumeric과 operator로 나뉨
  • alphanumeric identifier : letter, underscore로 시작함. $도 사용 가능하지만, scala 내부 구현과 혼동을 일으키기 때문에 쓰면 안됨!
  • naming convention은 자바와 동일(camel case). 단, 상수 정의 시 자바에선 X_OFFESET과 같이 표현했으나 scala는 XOffset과 같이 상수에도 camel case를 사용함.
  • 상수는 val과는 다르다. val은 변수다
  • operator identifier: 하나 이상의 opertaor 문자(+, :, ?, ~, #)로 구성됨.
  • mixed identifier: alphanumeric identifier + _ + operator identifier. ex. unary_+, myvar_= (요 형태는 18장의 properties에서 응용한다고 함)
  • literal identifier: back tick(`) 사이에 표기한 string. ex. \x`, `yeild``. java에서 scala reserved word등을 사용할 때.

6.11 Method overloading

  • 현재까지 작선한 Ratioanal에서는 정수와 섞어서 사용할 수 없다. 즉, r * 2처럼 작성할 수 없고 r * new Rational(2)로 작성해야 한다.
  • 이를 위해서 산술연산 메서드는 2가지 버전이 필요하다.(Rational을 받는 메서드와 정수를 받는 메서드) 즉, 오버로드해야 한다.
class Rational(n: Int, d: Int) {
  require(d != 0)
  private val g = gcd(n.abs, d.abs)
  val numer: Int = n / g
  val denom: Int = d / g1
  def this(n: Int) = this(n, 1)
  def + (that: Rational): Rational =
    new Rational(
      numer * that.denom + that.numer * denom,
      denom * that.denom
    )
  def + (i: Int): Rational = new Rational(numer + i * denom, denom)
  def - (that: Rational): Rational =
    new Rational(
      numer * that.denom - that.numer * denom,
      denom * that.denom
    )
  def - (i: Int): Rational = new Rational(numer - i * denom, denom)
  def * (that: Rational): Rational =
    new Rational(numer * that.nemer, denom * that.denom)
  def * (i: Int): Rational = new Rational(numer * i, denom)
  def / (that: Rational): Rational =
    new Rational(numer * that.denom, denom * that.nemer)
  def / (i: Int): Rational = new Rational(numer, denom * i)
  override def toString = numer + "/" + denom
  private def gcd(a: Int, b: Int): Int = 
    if (b == 0) a else gcd(b, a % b)
}

6.12 Implicit conversions

  • 이제 r * 2로 작성할 수 있지만 2 * r로는 작성할 수 없다. 이는 2.*(r)에서 정수에 Rational을 인자로 받는 * 연산자가 없기 때문이다.
  • 이를 위해서 정수를 Rational로 자동변환하는 implicit conversion을 만들 수 있다.
scala> implicit def intToRational(x: Int) = new Rational(x)
scala> val r = new Rational(2, 3)
r: Rational = 2/3
scala> 2 * r
res16: Rational = 4/3

6.13 A word of caution

  • implicit conversion, operator 재정의는 코드를 간결하게 하는 데 유용하긴 하지만, 남들이 코드를 읽을 때 혼동의 여지를 주기도 하므로 주의해서 사용해야 함.

6.14 Conclusion

⚠️ **GitHub.com Fallback** ⚠️