ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라] 함수형 객체
    스칼라 2022. 1. 30. 15:46
    class ComplexNumber(real: Double, imaginary: Double) {
      require(imaginary != 0)
      override def toString = s"$real + ${imaginary}i"
    }

    Programming in scala 4th edition 을 읽고 정리한 글입니다.

     

    1. 함수형 객체: 변경 가능한 상태를 전혀 갖지 않는 객체

    책에서는 분수를 가지고 설명을 하므로 복소수를 가지고 구현해보도록 한다.

     

    2. ComplexNumber 클래스 생성

    class ComplexNumber(real: Double, imaginary: Double)

    스칼라는 클래스에서 사용할 파라미터를 클래스 선언과 함께 바로 정의할 수 있다. 이것을 클래스 파라미터라고 하고 스칼라 내부적으로 클래스 파라미터와 같은 인자를 받는 주 생성자(primary constructor)를 만든다.

    스칼라는 필드나 메서드에 속하지 않은 코드들을 주 생성자로 밀어넣는다. 아래 예시에서 확인할 수 있다.

    // ComplexNumber.sala
    class ComplexNumber(real: Double, imaginary: Double) {
      println(s"This is Complex Number. real: $real, imaginary: $imaginary")
    }
    
    // ComplexNumber 클래스로 객체를 만드는 프로그램
    object ComplexExample extends App {
      val complexNumber = new ComplexNumber(10, 10)
    }
    // 위 코드를 실행하면 아래의 출력결과를 얻는다
    // 출력결과: This is Complex Number. real: 10.0, imaginary: 10.0

     

    3. ComplexNumber의 toString 구현

    스칼라의 toString은 java.lang.Object의 toString구현을 물려받는데 toString은 기본적으로 객체의 클래스이름과@id를 출력해준다. toString을 오버라이드하여 복소수 표현을 출력하도록 변경한다. 스칼라에서 override는 함수 선언 앞에 override 키워드를 붙여주면 된다.

    // ComplexNumber.scala
    class ComplexNumber(real: Double, imaginary: Double) {
      override def toString = s"$real + ${imaginary}i"
    }
    
    // ComplexExample.scala
    object ComplexExample extends App {
      val complexNumber = new ComplexNumber(1, 2)
      print(complexNumber)
    }
    
    // ComplexExample을 실행시키면 출력 결과: 1.0 + 2.0i

     

    4. 선결 조건 확인

    예시를 좀 더 간단하게 만들기 위해서 허수부가 존재해야만한다고 가정하자. 주 생성자 내부에 선결조건(precondition)을 정의할 수 있다. 선결조건을 정의하면 메서드나 생성자가 전달받은 값을 제한할 수 있다. 여기에서는 require문을 사용해서 선결조건을 지정한다.

    class ComplexNumber(real: Double, imaginary: Double) {
      require(imaginary != 0)
      override def toString = s"$real + ${imaginary}i"
    }

    이제 ComplexNumber로 객체럴 생성할때 imaginary 가 0이면 IllegalArgumentException이 발생하게 된다. 

     

    5. 필드 추가

    두 복소수를 더하는 add 함수를 작성해본다.

    class ComplexNumber(real: Double, imaginary: Double) {
      require(imaginary != 0)
      override def toString = s"$real + ${imaginary}i"
    
      def add(other: ComplexNumber): ComplexNumber = {
        new ComplexNumber(real + other.real, imaginary + other.imaginary)
      }
    }

    위와 같이 작성하고 실행하면 add 함수 안에서 other 의 real, imaginary를 바로 호출할 수 없어 에러가 발생한다. 필드를 추가해서 클래스파라미터를 할당해 주는 방법으로 구현해본다.

    class ComplexNumber(r: Double, i: Double) {
      require(i != 0)
      val real = r
      val imaginary = i
      override def toString = s"$real + ${imaginary}i"
    
      def add(other: ComplexNumber): ComplexNumber = {
        new ComplexNumber(real + other.real, imaginary + other.imaginary)
      }
    }

     

    6. 자기참조

    메서드가 자신의 객체를 참조할때는 this 키워드를 사용한다. 예를들어 위의 add함수에서 자신의 객체의 real을 참조하고 싶으면 this.real로 사용할 수 있다. (위와 같은 경우는 생략 가능하다.) 변수명이 중복되는 등의 경우에는 this를 명시해서 메서드의 호출 대상 인스턴스참조임을 표현해야한다.

     

    7. 보조 생성자

    주 생성자외에 여러가지 생성자가 필요한 경우 보조 생성자 (auxiliary constructor)를 만들 수 있다. 예를 들어서 허수부만 존재하는 복소수를 위한 생성자를 아래와 같이 만들어둘 수 있다. 

    class ComplexNumber(r: Double, i: Double) {
      require(i != 0)
      val real = r
      val imaginary = i
      override def toString = s"$real + ${imaginary}i"
    
      def this(i: Double) = this(0, i)
    
      def add(other: ComplexNumber): ComplexNumber = {
        new ComplexNumber(real + other.real, imaginary + other.imaginary)
      }
    }

    스칼라에서 모든 보조생성자는 반드시 같은 클래스 내의 다른 생성자를 호출하는 코드로 시작해야한다. 꼭 주 생성자를 호출할 필요는 없지만 그보다 앞에 있는 생성자를 반드시 호출해야되기 때문에 결국 주 생성자가 호출된다.

     

    8. 연산자 정의

    이제 복소수의 덧셈과 곱셈을 정의해본다.

    //ComplexNumber.scala
    class ComplexNumber(r: Double, i: Double) {
      require(i != 0)
      val real = r
      val imaginary = i
      override def toString = s"$real + ${imaginary}i"
    
      def this(i: Double) = this(0, i)
    
      def add(other: ComplexNumber): ComplexNumber = {
        new ComplexNumber(real + other.real, imaginary + other.imaginary)
      }
    
      def +(other: ComplexNumber): ComplexNumber = {
        add(other)
      }
    
      def *(other:ComplexNumber): ComplexNumber = {
        val real = this.real * other.real - this.imaginary * other.imaginary
        val imaginary = this.real * other.imaginary + this.imaginary * other.real
        new ComplexNumber(real, imaginary)
      }
    }
    //ComplexExample.scala
    object ComplexExample extends App {
      val complexNumberA = new ComplexNumber(1, 2)
      val complexNumberB = new ComplexNumber(3 ,4)
      println(complexNumberA + complexNumberB)
      println(complexNumberA * complexNumberB)
    }
    
    // 실행 결과
    4.0 + 6.0i
    -5.0 + 10.0i

     

    9. 오버로드

    곱셈 구현에서 단순 배수를 하는 구현을 추가한다. 이렇게 이름은 같지만 파라미터가 다른 메서드를 정의하는 것을 메서드 오버로드라고 한다. 

    class ComplexNumber(r: Double, i: Double) {
      require(i != 0)
      val real = r
      val imaginary = i
      override def toString = s"$real + ${imaginary}i"
    
      def this(i: Double) = this(0, i)
    
      def add(other: ComplexNumber): ComplexNumber = {
        new ComplexNumber(real + other.real, imaginary + other.imaginary)
      }
    
      def + (other: ComplexNumber): ComplexNumber = {
        add(other)
      }
    
      def * (other:ComplexNumber): ComplexNumber = {
        val real = this.real * other.real - this.imaginary * other.imaginary
        val imaginary = this.real * other.imaginary + this.imaginary * other.real
        new ComplexNumber(real, imaginary)
      }
    
      def * (r: Int): ComplexNumber = {
        new ComplexNumber(real * r, imaginary * r)
      }
    }

     

    10. 암시적 타입 변환

    이제 아래의 코드들을 모두 동작시킬수 있다.

    object ComplexExample extends App {
      val complexNumberA = new ComplexNumber(1, 2)
      val complexNumberB = new ComplexNumber(3 ,4)
      println(complexNumberA + complexNumberB)
      println(complexNumberA * complexNumberB)
      println(complexNumberB * 2)
    }
    
    // 결과
    4.0 + 6.0i
    -5.0 + 10.0i
    6.0 + 8.0i

    만약 complexNumberB * 2 대신에 2 * complexNumberB라고 표현하면 어떨까? 앞서 말했다시피 2.*(complexNumberB)로 변환되는데 Int 의 * 메서드에는 ComplexNumber를 파라미터로 받는 오버로드가 없기 때문에 에러가난다. 이를 해결하기 위해서는 암시적 타입변환을 위한 메서드를 생성해주면 된다.

    //ComplexNumber.scala
    class ComplexNumber(r: Double, i: Double) {
      // require(i != 0) 정수를 변환할 수 있도록 하기위해 이 제약조건은 제거하였다.
      val real = r
      val imaginary = i
      override def toString = s"$real + ${imaginary}i"
    
      def this(i: Double) = this(0, i)
    
      def add(other: ComplexNumber): ComplexNumber = {
        new ComplexNumber(real + other.real, imaginary + other.imaginary)
      }
    
      def + (other: ComplexNumber): ComplexNumber = {
        add(other)
      }
    
      def * (other:ComplexNumber): ComplexNumber = {
        val real = this.real * other.real - this.imaginary * other.imaginary
        val imaginary = this.real * other.imaginary + this.imaginary * other.real
        new ComplexNumber(real, imaginary)
      }
    
      def * (r: Int): ComplexNumber = {
        new ComplexNumber(real * r, imaginary * r)
      }
    }
    object ComplexExample extends App {
      val complexNumberB = new ComplexNumber(3 ,4)
    
      implicit def intToComplexNumber(x: Int) = new ComplexNumber(2, 0)
    
      println(2 * complexNumberB)
    }
    // 출력 결과: 6.0 + 8.0i

     

    마무리

    이번 장에서는 클래스 파라미터, 생성자, 연산자를 메서드로 정의하는법, 오버라이딩, 오버로딩에 대해서 알아보았다.

    댓글

Designed by Tistory.