이번 강좌에서 다루는 내용은 다음과 같다.

이번 강좌에 대해

처음에는 기본적인 문법과 개념을 다룰 것이다. 그 후, 더 많은 연습문제를 통해 점점 범위를 넓힐 것이다.

예제 중 일부는 인터프리터에서 작성된 것을 보일 것이고, 나머지는 소스파일로 된 것을 보여줄것이다.

인터프리터가 있으면 문제가 되는 부분을 빠르게 찾아나갈 수 있을 것이다.

스칼라를 사용해야 하는 이유는?

스칼라를 어떻게 쓸 수 있나?

스칼라 방식으로 생각하자

스칼라는 단지 “더 나은 자바”가 아니다. 전혀 새로운 마음으로 스칼라를 배우기 바란다. 그래야 이 강좌를 넘어서는 많은 것을 배울 수 있다.

인터프리터 시작하기

sbt console로 시작하라.

$ sbt console

[...]

Welcome to Scala version 2.8.0.final (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_20).
Type in expressions to have them evaluated.
Type :help for more information.

scala>

scala> 1 + 1
res0: Int = 2

식을 입력하면 인터프리터가 결과를 출력한다. 이때, res0는 결과 값에 인터프리터가 부여한 이름이다. 이 값의 타입은 Int이고, 값은 2라는 정수이다.

스칼라의 (거의) 모든 문장은 식이다.

식의 결과에 이름을 붙일 수 있다.

scala> val two = 1 + 1
two: Int = 2

val로 어떤 값에 이름을 붙인 경우, 이 관계를 변경할 수 없다.

변수

값과 이름의 관계를 변경할 필요가 있다면, var를 사용해야만 한다.

scala> var name = "steve"
name: java.lang.String = steve

scala> name = "marius"
name: java.lang.String = marius

함수

def를 사용해 함수를 만들 수 있다.

scala> def addOne(m: Int): Int = m + 1
addOne: (m: Int)Int

스칼라에서는 함수 인자의 타입을 명시해야 한다. 인터프리터는 기꺼이 타입 시그니쳐를 다시 표시해줄 것이다.

scala> val three = addOne(2)
three: Int = 3

인자가 없는 함수의 경우 호출시 괄호를 생략할 수도 있다.

scala> def three() = 1 + 2
three: ()Int

scala> three()
res2: Int = 3

scala> three
res3: Int = 3

이름 없는 함수

이름 없는 함수를 만들 수 있다.

scala> (x: Int) => x + 1
res2: (Int) => Int = <function1>

위 함수는 x라는 이름의 Int에 1을 더한다.

scala> res2(1)
res3: Int = 2

이름 없는 함수를 다른 함수나 식에 넘기거나 val에 저장할 수도 있다.

scala> val addOne = (x: Int) => x + 1
addOne: (Int) => Int = <function1>

scala> addOne(1)
res4: Int = 2

함수가 여러 식으로 이루어진 경우, {}를 사용해 이를 위한 공간을 만들 수 있다.

def timesTwo(i: Int): Int = {
  println("hello world")
  i * 2
}

이름 없는 함수의 경우도 마찬가지이다.

scala> { i: Int =>
  println("hello world")
  i * 2
}
res0: (Int) => Int = <function1>

이름 없는 함수를 넘길 때 위와 같은 형태를 자주 보게 될 것이다.

인자의 일부만 사용해 호출하기(부분 적용, Partial application)

함수 호출시 밑줄(_)을 사용해 일부만 적용할 수 있다. 그렇게 하면 새로운 함수를 얻는다. 스칼라에서 밑줄은 문맥에 따라 의미가 다르다. 하지만 보통 이름 없는 마법의 문자로 생각해도 된다. `{ _ + 2 }` 이라는 문맥에서 밑줄은 이름이 없는 매개변수를 가리킨다. 다음과 같이 이를 사용할 수 있다.

scala> def adder(m: Int, n: Int) = m + n
adder: (m: Int,n: Int)Int
scala> val add2 = adder(2, _:Int)
add2: (Int) => Int = <function1>

scala> add2(3)
res50: Int = 5

인자 중에서 원하는 어떤 것이든 부분 적용이 가능하다. 꼭 맨 마지막 위치가 아니라도 아무 곳에서 밑줄을 넣을 수 있다.
(역주: 사실은 _는 부분 적용하는 것에서 제외되는 위치가 혼동되지 않도록 표시해주는 역할을 한다. 혼동하면 안되는 것은 _이 부분적용이 아니고, _를 제외하고 호출하는 코드에 적힌 실제 인자가 부분적용의 인자값이라는 것이다. 다만 이 scala school에서는 _를 일관되게 부분 적용이라고 부르고 있지만, 역자 생각에 이는 틀린 것이다. 다만 의미의 혼동을 피하기 위해서 원저자의 표현을 그대로 사용할 것이다.)

커리 함수(Curried functions)

떄로 함수의 인자중 일부를 적용하고, 나머지는 나중에 적용하게 남겨두는 것이 더 쓸모있는 경우가 있다.

다음은 두 수를 곱하는 곱셈기를 만들 수 있는 함수이다. 첫 호출시 승수를 지정하고, 나중에 피승수를 지정할 수 있다.

scala> def multiply(m: Int)(n: Int): Int = m * n
multiply: (m: Int)(n: Int)Int

물론 두 인자를 한꺼번에 적용할 수도 있다.

scala> multiply(2)(3)
res0: Int = 6

다음과 같이 첫 인자를 채워 넣고 두번째 인자를 부분적용 할 수도 있다.

scala> val timesTwo = multiply(2) _
timesTwo: (Int) => Int = <function1>

scala> timesTwo(3)
res1: Int = 6

인자가 여러개 있는 함수를 가지고 커리할 수 있다. 앞에서 본 adder에 적용해 보자.

scala> (adder _).curried
res1: (Int) => (Int) => Int = <function1>

(연습문제(역자): 어떤 함수 f(x1,x2,….)의 커리화 된 함수를 어떻게 작성할 수 있을까? 이름없는 함수(무명함수 또는 람다식이라고도 한다)를 사용해 이를 구현해 보라.)

가변 길이 인자

동일한 타입의 매개변수가 반복되는 경우를 처리할 수 있는 특별한 문법이 있다. 여러 문자열에 동시에 `capitalize`를 호출하고 싶을 경우 다음과 같이 쓸 수 있다.

def capitalizeAll(args: String*) = {
  args.map { arg =>
    arg.capitalize
  }
}

scala> capitalizeAll("rarity", "applejack")
res2: Seq[String] = ArrayBuffer(Rarity, Applejack)

클래스

scala> class Calculator {
     |   val brand: String = "HP"
     |   def add(m: Int, n: Int): Int = m + n
     | }
defined class Calculator

scala> val calc = new Calculator
calc: Calculator = Calculator@e75a11

scala> calc.add(1, 2)
res1: Int = 3

scala> calc.brand
res2: String = "HP"

위 예와 같이 클래스 안에서 메소드는 def로, 필드는 val로 정의한다. 메소드는 단지 클래스(객체)의 상태를 억세스할 수 있는 함수에 지나지 않는다.

생성자

스칼라에서는 생성자가 특별한 메소드로 따로 존재하지 않는다. 클래스 몸체에서 메소드 정의 부분 밖에 있는 모든 코드가 생성자 코드가 된다. Calculator 예제를 생성자가 인자를 받아 내부 상태를 초기화하도록 변경해 보자.

class Calculator(brand: String) {
  /**
   * 생성자
   */
  val color: String = if (brand == "TI") {
    "blue"
  } else if (brand == "HP") {
    "black"
  } else {
    "white"
  }

  // 인스턴스 메소드
  def add(m: Int, n: Int): Int = m + n
}

주석을 만드는 방법이 두가지 있다는 점에 유의하라.

생성자를 사용해 인스턴스를 만들어낼 수 있다.

scala> val calc = new Calculator("HP")
calc: Calculator = Calculator@1e64cc4d

scala> calc.color
res0: String = black

BasicCalculator 예제를 보면 스칼라가 식 중심의 언어란 점을 잘 알 수 있다. color 값은 if/else 식에 의해 초기화되었다. 스칼라는 대부분의 구성 요소가 문(statement, 반환값이 없는 문장)이 아니고 식(expression, 결과를 반환하는 문장)이라는 점에서 식 중심의 언어이다.

곁다리: 함수 대 메소드

함수와 메소드는 서로 바꿔쓸 수 있는 개념이다. 함수와 메소드는 매우 유사하며, 실제 여러분이 호출 중인 어떤 이 함수인지 메소드인지를 기억하지 못할 수도 있다. 실제 메소드와 함수의 차이와 마주치게 되면 혼동이 올 수도 있다.

scala> class C {
     |   var acc = 0
     |   def minc = { acc += 1 }
     |   val finc = { () => acc += 1 }
     | }
defined class C

scala> val c = new C
c: C = C@1af1bd6

scala> c.minc // c.minc() 호출함

scala> c.finc // 함수 값(반환값이 아니라 함수 자체)을 반환함
res2: () => Unit = <function0>

어떤 "함수"를 괄호 없이 호출할 수 있는데, 다른 것은 그렇게 할 수 없다면, 어라? 스칼라 함수가 어떻게 돌아가는지 잘 안다고 생각했는데, 안그런 모양이네. 때로 괄호를 꼭 써야하는 경우도 있나?라고 생각할 지 모르겠다. 아마 함수라고 생각하고 있지만, 메소드를 사용하는 것이었을 수도 있다.(역주. 인자가 없는 메소드는 이름만으로 호출 가능하지만, 인자가 없는 함수는 이름만 사용하면 해당 함수를 의미하지, 그 함수를 적용한 값을 의미하지 않는다. )

실제로는 메소드와 함수의 차이를 잘 모르더라도 스칼라로 훌륭한 일을 해낼 수 있다. 스칼라를 처음 접하는 독자가 그 둘의 차이에 대한 설명을 보면, 내용을 따라가기 어려울 수도 있다. 하지만 이는 스칼라 언어를 상당히 깊이 파고 들어야 그 둘의 차이를 구분할 수 있을 만큼 함수와 메소드의 차이가 미묘하다는 사실을 보여주는 것 뿐이다. 위 c.finc 부분의 결과를 보면 이를 알 수 있다.)

상속

class ScientificCalculator(brand: String) extends Calculator(brand) {
  def log(m: Double, base: Double) = math.log(m) / math.log(base)
}

See Also “효율적인 스칼라(effective scala)”에서는 하위클래스가 상위클래스와 실제 다르지 않을 경우 타입 별명extends(확장)보다 더 낫다고 말한다. 스칼라 여행(Tour of Scala)에서는 상속하기(Subclassing)에 대해 다루고 있다.

메소드 중복정의(Overloading)

class EvenMoreScientificCalculator(brand: String) extends ScientificCalculator(brand) {
  def log(m: Int): Double = log(m, math.exp(1))
}

추상 클래스

추상 클래스(abstract class)는 메소드 정의는 있지만 구현은 없는 클래스이다. 대신 이를 상속한 하위클래스에서 메소드를 구현하게 된다. 추상 클래스의 인스턴스를 만들 수는 없다.

scala> abstract class Shape {
     |   def getArea():Int    // 하위클래스에서 이 메소드를 정의해야만 한다
     | }
defined class Shape

scala> class Circle(r: Int) extends Shape {
     |   def getArea():Int = { r * r * 3 }
     | }
defined class Circle

scala> val s = new Shape
<console>:8: error: class Shape is abstract; cannot be instantiated
       val s = new Shape
               ^

scala> val c = new Circle(2)
c: Circle = Circle@65c0035b

트레잇(Traits, 특성이라는 뜻)

트레잇(trait)은 다른 클래스가 확장(즉, 상속)하거나 섞어 넣을 수 있는(이를 믹스인Mix in 이라 한다) 필드와 동작의 모음이다.

trait Car {
  val brand: String
}

trait Shiny {
  val shineRefraction: Int
}
class BMW extends Car {
  val brand = "BMW"
}

클래스는 여러 트레잇를 with 키워드를 사용해 확장할 수 있다.

class BMW extends Car with Shiny {
  val brand = "BMW"
  val shineRefraction = 12
}

See Also 효율적인 스칼라도 트레잇에대해 다루고 있다.
.

추상클래스 대신 트레잇를 사용해야 하는 경우는 언제인가? 인터페이스 역할을 하는 타입을 설계할 때 트레잇과 추상클래스 중 어떤 것을 골라야할까? 두 가지 다 어떤 동작을 하는 타입을 만들 수 있으며, 확장하는 쪽에서 일부를 구현하도록 요청한다. 중요한 규칙은 다음과 같다.

당신이 이런 질문을 한 첫번째 사람은 아니다. 스택 오버플로우에서 트레잇과 추상 클래스의 비교, 추상 클래스와 트레잇의 차이,
또는 스칼라 프로그래밍: 트레잇냐 아니냐 그것이 문제로다? 등을 참조하라.

타입

앞에서 숫자 타입중 하나인 Int를 인자로 받는 함수를 보았다. 모든 타입의 값을 처리할 수 있는 일반적(generic)인 함수를 만들 수도 있다. 일반적 함수를 만들 때는 각괄호([])안에 타입 매개변수를 추가한다. 아래는 키와 값을 가지는 일반적인 캐시를 보여준다.

trait Cache[K, V] {
  def get(key: K): V
  def put(key: K, value: V)
  def delete(key: K)
}

메소드에도 타입 매개변수를 추가할 수 있다.

def remove[K](key: K)