이번주는 forfun에 대해서 조금 깊게 공부해보았습니다. 직장과 학업을 병행하면서 공부하니 아쉽게도 정리가 미흡합니다. 다음주엔 조금 더 알찬 학습을 기대하며...

내장 제어 구문

스칼라가 제공하는 내장 제어 구문은 몇 가지 없습니다. if, while, for, try, match, function call이 전부입니다. 스칼라에 제어 구문의 수가 적은 이유는 설계 초기부터 함수 리터럴을 포함했기 때문입니다. 스칼라는 기본 문법에서 제어 구문 위에 다른 제어 구문을 하나하나 추가하기보다는 라이브러리에 제어 구문을 추가하는 방법을 선택했기 때문입니다.

스칼라의 제어 구문은 대부분의 결과를 값으로 반환합니다. 따라서 프로그램 전체를 값을 계산하는 관점에서 바라보아야 합니다.

for 표현식

for로 할 수 있는 가장 간단한 일은 컬렉션에 있는 모든 요소를 이터레이션하는 것 입니다.


val filesHere = (new java.io.File(".")).listFiles

for (file <- filesHere)
  println(file)

제너레이터(generator)라고 부르는 file <- filesHere 문법을 이용해 filesHere의 원소를 이터레이션 합니다. for 표현식은 배열뿐 아니라 어떤 종류의 컬렉션에도 동작합니다. 이터레이션 대상 값의 범위에서 최댓값을 제외하고 싶다면 to 대신에 until을 사용하면 됩니다.


for(i <- 1 to 4)
  println("Iteration " + i)

for(i <- 1 until 4)
  println("Iteration " + i)

컬렉션의 모든 원소를 이터레이션하고 싶지 않은 경우, for표현식에 필터(filter)를 추가하면 가능합니다. 당연히 필터를 여러 개 추가할 수 있습니다. if 구문을 추가하기만 하면 됩니다.


val filesHere = (new java.io.File(".")).listFiles

for (
  file <- filesHere
  if file.isFile
  if file.getName.endsWith(".scala")
) println(file)

for구문에 제너레이터를 여러개를 추가하면 중접 반복문을 작성할 수 있습니다. 또한 for 중에 변수를 바인딩 할 수 있습니다.


val filesHere = (new java.io.File(".")).listFiles

def fileLines(file: java.io.File) =
  scala.io.Source.fromFile(file).getLines().toList

def grep(pattern: String) =
  for {
    file <- filesHere // 중첩1
    if file.getName.endsWith(".scala")
    line <- fileLines(file) // 중첩2
    trimmed = line.trim // 할당
    if trimmed.matches(pattern)
  } println(file + ": " + trimmed)

grep(".*gcd.*")

이터레이션의 중간 결과를 저장하기 위해서 yield를 사용하면 됩니다. yield를 사용하면 for 표현식의 본문을 수행할 때마다 값(trimmed.length)을 하나씩 만들어냅니다.


val filesHere = (new java.io.File(".")).listFiles

def fileLines(file: java.io.File) =
  scala.io.Source.fromFile(file).getLines().toList

val forLineLengths =
  for {
    file <- filesHere
    if file.getName.endsWith(".scala")
    line <- fileLines(file)
    trimmed = line.trim
    if trimmed.matches(".*for.*")
  } yield trimmed.length

match 표현식

스칼라의 match 표현식은 여타 언어의 switch 문과 유사하게, 다수의 대안 중 하나를 선택하게 해줍니다. 자바의 switch와 가장 중요한 차이는 match 표현식의 결과가 값이라는 차이가 있습니다.


val firstArg = if (args.length > 0) args(0) else ""

firstArg match {
  case "salt" => println("pepper")
  case "chips" => println("salsa")
  case "eggs" => println("bacon")
  case _ => println("huh?")
}

명령형에서 함수형으로 리팩토링


// 명령형

def printMultiTable() = {
  var i = 1
  while (i <= 10) {
    var j = 1
    while (j <= 10) {
      val prod = (i * j).toString
      var k = prod.length
      while (k < 4) {
        print(" ")
        k += 1
      }
      print(prod)
      j += 1
    }
    println()
    i += 1
  }
}

// 함수형

def makeRowSeq(row: Int) =
  for (col <- 1 to 10) yield {
    val prod = (row * col).toString
    val padding = " " * (4 - prod.length)
    padding + prod
  }

def makeRow(row: Int) = makeRowSeq(row).mkString

def multiTable() = {
  val tableSeq = for (row <- 1 to 10) yield makeRow(row)
  tableSeq.mkString("\n")
}

함수와 클로저

제어 흐름을 나누기 위해 스칼라는 함수를 사용해서 코드를 분리합니다. 스칼라에 존재하는 여러 종류의 함수가 지닌 특성을 정리하겠습니다.

메소드

객체의 멤버인 함수를 메소드(method)라고 부릅니다. 스칼라의 메소드는 선언하는 형태를 제외하곤 자바와 유사합니다.

지역 함수

프로그래머는 복잡한 일을 처리하기 위해서 유연하게 조립할 수 있는 빌딩 블록(building block)을 제공한다는 장점이 있습니다. 각 빌딩 블록은 개별적으로 이해가 가능하도록 단순해야 합니다.

스칼라는 함수 안에 함수를 정의할 수 있습니다. 지역 변수와 마찬가지로, 함수 안에 정의한 지역 함수도 그 정의를 감싸고 있는 블록 내에서만 접근할 수 있습니다. 아래 에는 스칼라에서 사용하는 대표적인 방법입니다.


import scala.io.Source

object LongLines {

  def processFile(filename: String, width: Int) = {

    def processLine(line: String) = {
      if (line.length > width)
        println(filename + ": " + line.trim)
    }

    val source = Source.fromFile(filename)
    for (line <- source.getLines())
      processLine(line)
  }
}

1급 계층 함수

함수 리터럴은 클래스로 컴파일하는데, 해당 클래스를 실행 시점에 인스턴스화하면 함수값(function value)이 됩니다. 함수 리터럴과 값의 차이는 함수 리터럴은 소스 코드에 존재하는 반면, 함수 값은 실행 시점에 객체로 존재한다는 점에 있습니다. 함수 리터럴의 본문에 둘 이상의 문장이 필요하다면 본문을 중괄호로 감싸서 블록을 만들면 됩니다. 함수의 반환 값은 마지막 줄에 있는 표현식을 평가한 값입니다.


increase = (x: Int) => {
  println("We")
  println("are")
  println("here!")
  x+1
}

함수 리터럴을 좀 더 간결하게 사용하기 위해서, 밑줄을 하나 이상의 파라미터에 대한 위치 표시자로 사용할 수 있습니다. 때로는 밑줄을 인자의 위치 표시자로 사용할 때 컴파일러가 인자의 타입 정보를 찾지 못할 경우도 있습니다. 이럴 경우 콜론을 이용해 타입을 명시적으로 표시하면 됩니다.


someNumbers.filter((x) => x > 0)
someNumbers.filter(x => x > 0)
someNumbers.filter(_ > 0) // 위치 표시자

부분 적용한 함수

파라미터에서 함수 호출과 밑줄을 활용하면 부분 적용한 함수를 사용할 수 있습니다.


def sum(a: Int, b: Int, c: Int) = a + b + c
sum(1,2,3)

val a = sum _
a.apply(1,2,3)
a(1,2,3)

위의 예에서 sum _은 부분 적용 함수이지만, 왜 부분 적용 함수인지 명확하게 와 닿지 않을 것 입니다. 부분 적용이라는 이름은 함수를 적용할 때 인자를 모두 넘기지 않았기 때문입니다. sum _의 경우에는 인자를 전혀 넘기지 않고 모두 빠진 인자로 처리합니다.


val b = sum(1, _: Int, 3)
b(2)

클로저

함수 리터럴에서 의미를 부여한 것이 아니기 때문에 자유 변수(free variable) 이며, 대조적으로 주어진 함수의 문맥에서만 의미가 있으므로 바운드 변수(bound variable) 입니다.

주어진 함수 리터럴에서 실행 시점에 만들어낸 객체인 함수 값을 클로저(closure)라고 부릅니다. 클로저라는 이름은 함수 리터럴의 본문에 있는 모든 자유 변수에 대한 바인딩을 포획(capturing)해서 자유 변수가 없게 닫는(closing) 행위에서 따온 말입니다.


def makeIncreaser(more: Int) = (x: Int) => x + more

val inc1 = makeIncreaser(1)
val inc9999 = makeIncreaser(9999)
inc1(10)
inc9999(10)

꼬리 재귀

마지막에 자신을 재귀 호출하는 경우를 꼬리 재귀(tail recursive)라고 합니다. 스칼라 컴파일러는 꼬리 재귀를 감지해 다음에 사용할 새로운 값과 함께 함수의 첫 부분으로 돌아가는 내용으로 변경합니다. 따라서 문제를 해결할 때 재귀를 사용하는 것을 기피할 필요가 없습니다. 꼬리 재귀를 사용하면 실행 시점에 겪어야 할 성능상의 초과 비용도 발생하지 않습니다.


def bang(x: Int): Int =
  if (x == 0) throw new Exception("bang!")
  else bang(x-1)

bang(5)