Syntactic sugar in Scala

0. 들어가기에 앞서

흔히들 C++과 Java가 유사하다고 말하지만, C++에서 Java로 넘어갔을 때 가장 황당했던 부분은 자바에선 Operator overloading이 되지 않기 때문에 vector[i], list[i]등으로 접근할 수 있었던 STL을 모두다 list.get(i)로 불러야 한다는 점이었다. 그리고 이것은 코드의 가독성도 해치고, 코딩하는 맛도 떨어지고, 거기에 자바엔 거추장스러운게 너무 많다는 느낌을 주기에 충분했다.  응당 프로그래머라면 알아들을 수 있는 수준의 심볼들을 쓰지 않는 것은 낭비니까.

그런 문제는 나만 느낀게 아니였는지, 요즘 공부하고 있는 Scala에서도 꽤 적극적으로 위와 같은 문제를 해결하고자 많은 고민을 했던 흔적이 보이고, 그것에 꽤 매료된 나는 이 포스팅에서 그 중 몇 가지를 언급해보려고 한다. 스칼라에 대한 사전지식 없이도 이해가능한 수준으로 풀어써보려 하니 스칼라를 배워볼까 말까 고민 중인 분들에게도 좋은 자극이 됐으면!

 


1. val, def, (), return

val i = 1
val s1 = "string1"
def s2() = "string2"
def s3 = "string3"

def factorial(n: Int): Int =
  if (n == 1) 1 else n * factorial(n - 1)

스칼라 컴파일러는 타입 추론(type inference)에 특화되어있어서, 마치 C++11의 auto처럼 변수형을 명시적으로 작성하지 않아도 된다는 것이 좋고, s1은 값 "string1"을 갖는 String형 변수이다. (C++, std::string s1 = "string1";)

s2는 인자를 받지 않고, 값 "string2"를 반환하는 함수인데, 이렇게 인자를 받지 않는 함수는 s3처럼 괄호를 생략할 수 있다.

그리고 같이 JVM위에서 돌아가는 자바와는 다르게 명시적으로 return을 쓰지 않는 것이 관례인데, 그 이유는 statement가 거의 없고 거의 모든 문장이 expression이라는 점이다. 그 결과, s3을 그냥 함수에서 변수로 바꾸는 것은, def를 val로만 바꾸면 된다는 장점과 더불어, 보통 우리가 If Statement로 언급하는 if-문도 위의 factorial()예제 처럼 깔끔한 형태를 띌 수 있게 된다.

 


2. class parameter, case class

class NaturalNum {
  private int n = 1;

  public NaturalNum(int number) {
    if (number <= 0) throw new IllegalArgumentException(..);
    this.n = number;
  }
}

위의 코드는 간단한 자연수를 받아 그것을 초기화하는, 필드를 선언하고 생성자에서 예외처리 & 할당해주는 틀에 박힌 자바코드이다. 필드를 선언하고 그것을 초기화 해주는 작업을 지루한 작업이라고 느낀 파이썬에서도 프로퍼티등을 이용해서 해결하려는 시도가 있었는데, 스칼라에서는 아래와 같은 코드로 필드를 선언하고, 그것의 유효성을 검증한다.

class NaturalNum(n: Int) {
  require(n > 0)
}

스칼라 컴파일러는 클래스 내부에 있으면서 필드나 메소드 정의에 들어 있지 않은 코드를 주 생성자에 넣는다. 5 그래서 유효성 검사를 하는 함수 require()를 바로 부를 수 있다는 것도 코드를 간결하게 만들어 주는 요소다.

여기서 한 가지 주의해야할 점은 n은 private 접근 지정자라는 것이다. 그래서 위의 코드에서 public하게 n에 접근하고 싶다면 별도로 val value = n 와 같은 필드를 선언하고 할당한다. 그럼 nvalue 두 개의 중복된 필드가 생긴 것이 아닌가하고 의아해할 수 있겠지만, 클래스 파라미터가 생성자 내부에서만 쓰인 경우, 스칼라 컴파일러는 그들에 해당하는 필드를 생성하지 않는다. 7

결과적으로 <del>자바보다는</del> 깔끔한 코드가 되긴 했지만, 여기서 더 깔끔하게 처리할 수 있는 것이 case class 기능이다. class 앞에 case라는 키워드를 추가하는 것만으로 1. 클래스 이름으로 객체 생성이 가능해지고, 2. 클래스 파라미터가 public 필드로 바뀐다.

val n1 = new NaturalNum(1) // normal class
println(n1.n) // error, n is private

val n2 = NaturalNum(2) // case class
println(n2.n) // profit!

조금 더 정확하게 말하자면 클래스 이름과 같은 identifier를 갖는 static factory method가 추가되는 것이고, 이는 과도한 new operator의 사용을 억제하여 코드의 간결성을 높이는 부수적인 효과도 있다.

 


3. function = operator

def add(NaturalNum that): NaturalNum =
NaturalNum(this.n + that.n)

val n3 = n1.add(n2) // n3 = NaturalNum(3)

연산하거나 기능을 수행한다는 점에서 사실 함수와 연산자는 크게 차이가 없고, 실제로도 C++에서는 위에서 언급했던 연산자 오버로딩을 통해 함수들을 연산자처럼 취급하며 간결한 코드를 작성해왔다. 위의 add 함수는 앞서 보였던 자연수 클래스끼리를 더하는 멤버함수인데, 스칼라는 어떤 메소드라도 연산자 표기법을 사용할 수 있다. 8 다시 말해, val n3 = n1 add n2라고 간략하게 쓸 수 있다!

더 정확하게는, 두 개 이상의 인자를 받는 경우가 아닌 모든 메소드들은 .() 를 생략할 수 있다. 그 결과, add 메소드의 이름을 +로 바꾸기만 하면 우리가 생각하던 연산자 오버로딩을 아주 손쉽게 구현할 수 있다. 9 10

 


4. match-case

match {
  case [CONDITION1] => [EXPRESSION1]
  case [CONDITION2] => [EXPRESSION2]
  case _ => [DEFAULT_EXPRESSION]
}

C-like 언어에서 지원하는 switch-case 문과 아주 유사한 형태를 띄는데, 여기서 CONDITION에 해당하는 부분에는 정수형은 물론이고 직접 값을 비교하거나 심지어 타입과 생성자 패턴, 시퀸스(리스트) 패턴, 튜플 패턴등등 다양한 타입의 패턴 매칭을 지원한다.

def invertWord(word: List[Char]): List[Char] = {
  def invertChar(char: Char) = {
    if (char.isLower) char.toUpper else char.toLower

    word match {
      case Nil => Nil
      case x::xs => invertChar(x) :: invertWord(xs)
    }
  }
}

위의 함수는 시퀀스 패턴매칭을 이용해서 Char의 리스트를 인자로 받은 뒤 소문자인 경우 대문자로, 대문자인 경우 소문자로 invert시켜 반환한다. 예컨데, Hello, World!가 입력으로 들어가면 hELLO, wORLD!가 나오는 식이다.

여기서 건내받은 word를 패턴매칭을 통해 비어있는 경우 Nil을 반환하고, 1개 이상의 원소가 있는 경우 헤드(x)와 테일(xs) 나눠서, 헤드만 invert시켜 준 후에 나머지 테일에 대해서 재귀적으로 invertWord()를 호출한 결과를 붙여(::)주는 것이다.

 


5. EOF

이외에도 스칼라는 클로져, 함수 리터럴, for-each, lambda calculus를 지원하지만, 이것은 스칼라만의 특징적인 syntactic sugar라고 하기엔 약간 부족한 감이 있어서 이런 것들도 있더라는 정도의 소개만.. >_<

 


  1. 자바를 처음 접하던 때를 생각해보면 일일이 class로 감싸주는 것부터 비호감이었다. 나중에 납득하긴 했지만. 
  2. 쓸 수는 있다. 근데 아무도 안 쓴다. 
  3. 하지만 어떤 객체가 생성될 때 멤버변수를 모두 초기화하기 위해서 많은 컴퓨팅 자원을 필요로 하는 경우에는 def를 쓰기보단 lazy키워드로 lazy evaluation을 쓰는 것이 더 좋다고 한다. 
  4. lazy evaluation을 간단하게 설명하면 그 값이 필요하기 직전까지 계산을 미루는 것. 
  5. 출처: Programming in Scala, 2E 한국어판, p.143 
  6. 스칼라에선 private을 따로 붙이지 않는 이상 기본적으로 public 접근이다. 
  7. 출처: Programming in Scala, 2E 한국어판, p.147 
  8. 출처: Programming in Scala, 2E 한국어판, p.126 
  9. 스칼라에서 전위 연산자는 오직 +, -, !, ~ 네 가지 뿐이고, unary_- 메소드를 정의하면 -n1과 같은 표현을 쓸 수 있다. 
  10. 인자를 받지 않는 메소드가 곧 후위 연산자이다. 

p.s. manually imported from previous blog

 

Leave a Reply