흐름 제어 추상화

함수 값을 활용해 흐름 제어를 추상화하는 방법에 대해서 알아보겠습니다. 커링(currying)과 이름에 의한 파라미터 호출(by-name parameter)도 살펴보겠습니다.

고차 함수를 사용할 때 장점 중 하나는 자신만의 추상화한 흐름 제어를 작성할 수 있어서 코드의 중복을 줄일 수 있다는 점 입니다.

아래 예에서 볼 수 있듯이, filesMatchingmatcher를 사용해서 필요한 함수값을 전달하면 코드의 중복을 줄이고 흐름을 제어할 수 있습니다.

object FileMatcher {
  private def filesHere = (new java.io.File(".")).listFiles

  private def filesMatching(matcher: String => Boolean) =
    for (file <- filesHere; if matcher(file.getName))
    yield file

  def filesEnding(query: String) =
    filesMatching(_.endsWith(query))

  def filesContaining(query: String) =
    filesMatching(_.contains(query))

  def filesRegex(query: String) =
    filesMatching(_.matches(query))
}

커링

미국의 수학자인 하스켈 B. 커리의 이름에 따온 커링은 다중인자를 받는 함수를 단일 인자 함수열로 만드는 것을 말합니다. 아래 예를 보시면 직관적으로 커링에 대해서 알 수 있습니다.


// 일반 함수
def plainOldSum(x: Int, y: Int) = x + y
plainOldSum(1, 2)

// 커링 함수
def curriedSum(x: Int)(y: Int) = x + y
curriedSum(1)(2)

새로운 제어 구조 작성

함수가 1급 계층인 언어에서는 언어의 문법이 고정되어 있더라도 새로운 제어 구조를 작성할 수 있습니다. 함수를 인자로 받는 메소드만 작성하면 됩니다. 아래 메소드를 사용하는 경우 사용자 코드가 아니라 withPrintWriter가 파일 닫기를 보장하는 장점이 있습니다. 이러한 기법을 빌려주기 패턴(loan pattern)이라고 부릅니다.


import java.io.{File, PrintWriter}

def withPrintWriter(file: File)(op: PrintWriter => Unit) = {
  val writer = new PrintWriter(file)
  try {
    op(writer)
  } finally {
    writer.close()
  }
}

withPrintWriter(new File("date.txt"))(writer => writer.println(new java.util.Date))

이름에 의한 호출

빈 파라미터 목록인 ()을 생략할 수 있는 이름에 의한 호출 파라미터 타입은 파라미터에서만 사용할 수 있습니다.


def byNameAssert(predicate: => Boolean) =
    if (assertionsEnabled && !predicate)
        throw new AssertionError

스칼라의 계층구조

모든 클래스가 Any를 상속하기 때문에 스칼라 프로그램에 있는 모든 객체를 ==, !=, equals를 사용해 비교할 수 있습니다. 단, Any클래스의 ==, !=final이기 때문에 equals을 재정의 해야 합니다.

루트 클래스 Any에는 서브클래스가 둘 있는데 바로 AnyValAnyRef가 있습니다. AnyVal은 값 클래스이며, AnyRef는 참조 클래스이며 자바의 java.lang.Object로 구현했다고 생각하는 것도 이해하는 한 가지 방법입니다.

트레이트

메소드와 필드 정의를 캡슐화하면 트레이트를 조합한 클래스에서 그 메소드나 필드를 재사용할 수 있습니다. 하나의 부모 클래스만 갖는 클래스의 상속과는 달리, 트레이트의 경우 몇 개라도 혼합해 사용(믹스인, mixin)할 수 있습니다.

하나는 간결한 인터페이스를 확장해 풍부한 인터페이스를 만드는 것이고, 다른 하나는 쌓을 수 있는 변경을 정의하는 것 입니다.

트레이트의 동작 원리

트레이트의 정의는 trait 키워드를 사용하고, 트레이트를 믹스인할 때에는 extends 키워드를 사용합니다. extends를 사용하면 트레이트의 슈퍼클래스를 암시적으로 상속합니다.


trait Philosophical {
  def philosophize() = {
    println("I consume memory, therefore I am!")
  }
}

class Frog extends Philosophical {
  override def toString = "green"
}

트레이트를 어떤 슈퍼클래스를 명시적으로 상속받는 클래스에 혼합할 수도 있습니다. 이때는 extends 키워드를 사용해서 슈퍼클래스를 지정하고, with는 사용해서 트레이트를 믹스인 합니다.


class Animal
trait HasLegs

class Frog extends Animal with Philosophical with HasLegs {
  override def toString = "green"
}

Ordered 트레이트

Ordered 트레이트가 비교 연산자 구현을 대신할 수 있기 때문에 순서가 있는 두 객체를 비교할 때마다, 한 번의 메소드 호출만으로 원하는 비교를 정확히 수행할 수 있습니다. 단, Ordered 트레이트는 equals를 정의하지 않음에 유의해야 합니다.

p.s. 해당 교재의 12.7 트레이트냐 아니냐, 이것이 문제로다는 꼭 참고해보세요.


import scala.collection.mutable.ArrayBuffer

trait Doubling extends IntQueue {
  abstract override def put(x: Int) = { super.put(2 * x) }
}

class BasicIntQueue extends IntQueue {
  private val buf = new ArrayBuffer[Int]
  def get() = buf.remove(0)
  def put(x: Int) = { buf += x }
}

val queue = new BasicIntQueue with Doubling
queue.put(10)
queue.get()