ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Java 프로그래머를 위한 Scala 따라해 보기 #3
    기술 관련/Scala 2021. 1. 24. 01:47

    지난 번 따라해보기Java 프로그래머를 위한 Scala 따라해 보기 #2에 이어서 계속 따라가 보도록 하겠다.


    docs.scala-lang.org/ko/tutorials/scala-for-java-programmers.html

     

    자바 프로그래머를 위한 스칼라 튜토리얼

    Michel Schinz, Philipp Haller 지음. 이희종 (heejong@gmail.com) 옮김. 시작하면서 이 문서는 Scala 언어와 그 컴파일러에 대해 간단히 소개한다. 어느 정도의 프로그래밍 경험이 있으며 Scala를 통해 무엇을 할

    docs.scala-lang.org

    Scala 가 Functional Programming 형식을 지원하긴 해도 Java와 마찬가지로 Scala는 OOP 언어다. 따라서, OOP 에서 많이 사용하는 Class의 개념을 가지고 있으며, 상속도 할 수 있다.

     

    클래스 생성자(Class Constrcutor)

    Scala 클래스와 Java 클래스의 Class constructor에 대한 개념적인 차이가 좀 있다. 자세한 부분은 Scala Class 문서에 잘 나와 있지만, 튜토리얼을 통해 간략하게 알아 볼 수 있다.

     

    우선, Java 클래스를 정의하는 코드를 보자

    public class MyClass {
      public MyClass() {
        //
      }
      
      public MyClass(int x) {
        //
      }
    }

    Java에서 클래스에 대한 정의는 class 라는 키워드로 정의하고 그 안에 Constructor를 통해 parameter 정의한다. 필요에 따라 서로 다른 종류 parameter를 입력 받는 constructor를 구성할 수 있다.

     

    Scala에서도 Java 처럼 class 라는 키워드로 클래스를 정의하고 있지만, Java와 달리 클래스를 정의하면서 바로 parameter를 지정하며 constructor는 class body에 바로 작성하도록 되어 있다.

    class MyTest(x: Int) {
      println("the constructor begins")
    }

     

    혹시, Java 클래스처럼 constructor를 여러 개를 만드는 방법이 있을까 해서 찾아 보았지만, Scala에서는 그런 방식은 지원하지 않는 것 같았다. 다만, JavaScript 처럼 정의된 parameter를 생략할 때 이를 인식하고 대응할 수 있도록 AUXILIARY class constructor라는 것을 만들 수 있다.

     

    val DefaultCrustSize = 12
    val DefaultCrustType = "THIN"
    
    // the primary constructor
    class Pizza (var crustSize: Int, var crustType: String) {
    
        // one-arg auxiliary constructor
        def this(crustSize: Int) = {
            this(crustSize, DefaultCrustType)
        }
    
        // one-arg auxiliary constructor
        def this(crustType: String) = {
            this(DefaultCrustSize, crustType)
        }
    
        // zero-arg auxiliary constructor
        def this() = {
            this(DefaultCrustSize, DefaultCrustType)
        }
    
        override def toString = s"A $crustSize inch pizza with a $crustType crust"
    
    }

     

    다시 튜토리얼 예제를 보자. 이 예제는 복소수(Complex) = 실수(Real) + 허수(Imaginary)를 클래스로 표현한 것이다. 

    class Complex(real: Double, imaginary: Double) {
      def re() = real
      def im() = imaginary
    }

    object와 달리 class는 parameter를 받는 constructor를 가지게 되며 parameter 다음에 중괄호로 되어 있는 코드 블럭이 constructor로서 실행된다. re와, im 는 class에 정의된 method로서 object의 method 정의 방법과 같은 방법으로 정의한다.

     

    예제에서는 return 이라는 키워드도 없고, 또 return 타입도 정의되어 있지 않는데, Scala에서는 Double이라는 명확한 타입을 가진 값(real, imaginary)이 method의 리턴 값으로 주어지는 경우 자동으로 Double 타입으로 선언된 것으로 처리한다.

     

    즉, 위의 줄여진 코드를 더 늘리면 다음과 동일한 의미로 인식된다.

    class Complex(real: Double, imaginary: Double) {
      def re(): Double = {
        return real
      }
      def im(): Double = {
        return imaginary
      }
    }

    또한, method를 호출 할 때 parameter가 없는 경우라면 다음과 같이 괄호 생략도 가능하다.

    class Complex(real: Double, imaginary: Double) {
      def re = real
      def im = imaginary
    }

     

     

    클래스 상속 (Class Inheritance)

    Scala에서 상속은 Java에서처럼 extends라는 키워드를 이용한다. Scala에서는 모든 class는 자신의 superclass가 있으며 별도 super class가 없다면 모든 스칼라 클래스의 부모인 scala.AnyRef 가 super class가 된다.

     

    다음 예제는 Point 클래스를 부모 class로 확장한 ColorPoint 클래스에 대한 예시이다.

    class Point(xc: Int, yc: Int) {
      val x: Int = xc
      val y: Int = yc
      def move(dx: Int, dy: Int): Point =
        new Point(x + dx, y + dy)
    }
    
    class ColorPoint(u: Int, v: Int, c: String) extends Point(u, v) {
      val color: String = c
      def compareWith(pt: ColorPoint): Boolean =
        (pt.x == x) && (pt.y == y) && (pt.color == color)
      override def move(dx: Int, dy: Int): ColorPoint =
        new ColorPoint(x + dy, y + dy, color)
    }

    Point는 xc, yc라는 두 개의 parameter를 가지지만 ColorPoint는 c라는 parameter가 추가되었다. 그리고 Point에 정의된 move라는 method를 대신하여 ColorPoint를 return하도록 override 라는 키워드를 붙여 수정되었다.

     

    부모 class를 상속하게되면 뭔가 그 부모 클래스에서 초기화 코드가 있기 때문에 super constructor 를 호출하는 코드가 있을 것 같았는데, 예제에서는 눈에 띄지 않는다. 그 대신 다음과 같은 모습을 볼 수 있다.

    class ColorPoint(u: Int, v: Int, c: String) extends Point(u, v)

    ColorPoint로 넘겨 받는 parameter u, v, c 중 u, v가 Point에 argment로 전달되는 것이다.

     

    u, v가 아닌 고정 값을 원한다면 이런 형식도 가능하다.

    class ColorPoint(u: Int, v: Int, c: String) extends Point(100, v)

     

    아.. 얼마나 간결한지.. Scala를 사용하다가 Pure Java 코드를 쓰려고 할 때 그 불편함이 말로 할 수 없을 듯 하다. 개인적으로 IDE에서 자동으로 제공해 주긴 하지만 Java의 System.out.println과 세미콜론은 세상 불편하게 느끼는 한 사람으로서 Scala의 등장이 너무나 반갑기까지 하다. (물론 장단점이 있겠지만 ㅋ)

     

    케이스 클래스(Case Class)

    Scala 클래스의 특징 중 하나로 case  class라는 개념이 있다. Constructor로 넘겨진 정보를 보관하는 형태의 클래스 개념으로 constructor parameter를 immutable(수정 불가능)한 field 값으로 갖게 된다.

    case class SimpleCase(x: Int, text: String)
    
    object Main {
      def main(args: Array[String]): Unit = {
        println("Hello world!")
        val s = SimpleCase(2, "simple")
        println(s.x + " and " + s.text)
      }
    }

    println 모양이 좀 구식이라 string interpolation 기능을 이용하여 이렇게 써 볼 수도 있다.

    case class SimpleCase(x: Int, text: String)
    
    object Main {
      def main(args: Array[String]): Unit = {
        println("Hello world!")
        val s = SimpleCase(2, "simple")
        println(s"${s.x} and ${s.text}")
      }
    }

    case class로서 작서된 SimpleCase 클래스는 x와 text라는 immutable field를 갖는다. 그 대신 여느 클래스 처럼 new 로 생성되지 않는다. case class는 그 parameter와 함께 SimpleCase() 형식으로 생성이 되며, 그 결과 x와 text는 초기화 된 값을 가지게 된다. 보통 class에서 instance화 되는 경우 instance의 reference 값으로 동일성을 비교하게 되는데 case class는 구조적인 동등성을 놓고 비교하도록 되어 있다. 즉, 같은 값으로 초기화된 case class를 비교 하면 그 결과가 같다고 인식힌다.

    var s1 =SimpleCase(1, "simple")
    var s2 =SimpleCase(2, "simple")
    println(s==s1) // false
    println(s==s2) // true

     

    패턴 매칭 (Pattern Matching)

    이 예제에서는 Sum, Var, Const 세 가지 종류의 클래스가 각각 트리 노드를 대표한다. 개념적으로 최상위 클래스로서 Tree 클래스를 정의했으며, abtract 키워드로 이 클래스는 추상 클래스 형태가 되었다. 단, abstract 클래스의 method가 지정되어 있지 않으므로, 각각의 상속받는 클래스에서는 별도로 구현하는 method가 없다. 보다 자세한 정보는 추상 클래스 문서를 참고하자.

    abstract class Tree
    case class Sum(l: Tree, r: Tree) extends Tree
    case class Var(n: String) extends Tree
    case class Const(v: Int) extends Tree

     

    Sum, Var, Const 클래스를 정의할 때 case class로 정의했으므로, 값을 직접 비교하고 구분하는 pattern matching을 이용할 수 있다. 패턴 매칭이란 match, case 키워드가 짝을 이루어 구성되는 것으로 match 앞에 대한 object와 case 뒤에 오는 object를 Scala 내장 로직으로 비교 후 case에 대응하는 함수를 실행 시키는 것을 말한다. case class의 case와는 쓰임이 다른 키워드이므로 혼동하지 않도록 주의 한다.

     

    이제 패턴 매칭에 대해 좀 더 자세히 알아보자.

    object MatchTest1 extends App {
      def matchTest(x: Int): String = x match {
        case 1 => "one"
        case 2 => "two"
        case _ => "many"
      }
      println(matchTest(3))
    }

    matchTest라는 method는 정수 값 x를 parameter로 갖고 그 결과로 문자열을 리턴한다. 이를 구성하기 위한 코드 블럭을 제공하는데 match/ case로 구성되어 있다. x와 case 뒤의 값을 비교하여 어떤 것에 같은지 비교 후 그 결과를 얻게 된다. 예시에서는 x값이 1 또는 2 인 경우와 나머지를 wildcard 문자로 '_' (underscore) 를 사용하고 있다.

     

    아래와 같이 어떤 형태의 object라도 다 받을 수 있는 타입으로 Any를 사용할 수도 있다.

    object MatchTest2 extends App {
      def matchTest(x: Any): Any = x match {
        case 1 => "one"
        case "two" => 2
        case y: Int => "scala.Int"
      }
      println(matchTest("two"))
    }

    위의 예제는 method 자체에 대한 코드 블럭에서 사용된 것이지만 코드 블럭 내부에서도 match/case 구조를 이용할 수 있는 것을 볼 수 있다.

    def matchTest(x: Any): Any = {
      x match {
        case 1 => {
          println("one")
        }
        case "two" => {
          println(2)
        }
        case y: Int => {
          println("scala.Int")
        }
      }
    }
      
    matchTest("two") // 2

     

    다시 튜토리얼로 돌아가서, 여기서는 두 개의 Tree를 합치는 Sum, 문자열 정보를 가진 Var, 정수형 정보를 가진 Const 세 가지 종류의 노드가 있다고 가정하고 이를 case class로 표현한 것이다.

    abstract class Tree
    case class Sum(l: Tree, r: Tree) extends Tree
    case class Var(n: String) extends Tree
    case class Const(v: Int) extends Tree

    그리고, Tree의 대한 정보를 계산하는 eval 함수를 다음과 같이 재귀 함수형태로 구성한다.

    def eval(t: Tree, env: Environment): Int = t match {
      case Sum(l, r) => eval(l, env) + eval(r, env)
      case Var(n)    => env(n)
      case Const(v)  => v
    }

     

    여기서 눈여겨 보아야 할 것은 env 라는 parameter가 Environment 타입이고, t가 Var(n)인 경우 env(n)을 호출하는 형태로 보아 env()은  뭔가 호출되는 형태인 것을 볼 수 있다.

     

    이와 관련하여 type 이라는 키워드를 이용하게 된다.

    type Environment = String => Int

    이렇게 type 을 정의하면 문자열을 받아 정수형으로 변환하는 함수형을 Environment라는 이름으로 정의한다는 것을 말한다. 일종의 Alias이다. type을 쓰지 않는 다면 eval은 다음과 같이 사용한다.

    def eval(t: Tree, env: String => Int): Int = t match {
      case Sum(l, r) => eval(l, env) + eval(r, env)
      case Var(n)    => env(n)
      case Const(v)  => v
    }

     

    아, 튜토리얼에 나온 eval method에 대한 설명이 잘 이해가 되지 않는다. *-_-*  Tree로 그림을 그려 보면 좀 나을까?

     

    이 Tree의 최상단 노드인 Sum1을 eval하는 것으로 시작하며 Sum1의 경우 l을 Sum2, r에 Sum5로 놓고 다시 eval을 호출한다. 그렇게 타고 내려가다 Leaf Node인 Var에 도착하면 String으로 되어 있는 정보를 Int로 변환하는 Environment 타입의 Callback을 실행하여 정수 값을 리턴하고, Const의 경우는 원래 정수 형이었기 때문에 가지고 있는 값을 그대로 return한다. 그러면 Sum3의 left, right node의 eval() 실행 결과를 얻게 되므로 두 값을 합친 후 return하여 상위 노드로 되돌아가게 된다.

     

    그래서 마지막에 main method에 다음과 같은 Environment로 전달할 Callback을 미리 구성하고 eval을 호출한다.

    def main(args: Array[String]): Unit = {
      val exp: Tree = Sum(Sum(Var("x"),Var("x")),Sum(Const(7),Var("y")))
      val env: Environment = { case "x" => 5 case "y" => 7 }
      println("Expression: " + exp)
      println("Evaluation with x=5, y=7: " + eval(exp, env))
    }

    exp가 자료구조로서 Tree 구조를 만들어 표현한 것이고 이를 eval()에 넣어 실행한다.

     

    env로 정의된 Enviroment callback은 x값이면 숫자 5로, y 값이면 숫자 7로 변환하는 callback 함수로 정의하고 있다.

    val env: Environment = { case "x" => 5 case "y" => 7 }

    따라서 최종 실행 결과는 5+5+7+7 의 결과인 24를 얻게 된다.

     

    다음으로 trait에 대한 부분을 설명하는데 이건 다음 글에서 정리해야겠다.

    댓글

Designed by Tistory.