[입 개발] Scala의 거시기 : _(underscore) 의 용법 정리

Scala 를 사용하면 만나게 되는 여러 문법 중에, 처음 접하는 이들의 머리를 깨는 것이 있으니, 그게 바로 Scala에서 거시기로 통하는 _(underscore) 입니다. 이게 뭐지 하고 고민하는 중에 팀분이(멀린 사랑해요.) 아래 자료를 알려주셨습니다. 그리고 이 블로그는 아래의 문서를 풀이하는 것입니다. 사실 아래의 문서만 봐도 거의 모든 것이 이해되지만, 제 부족한 머리를 위해서 정리해둡니다. 참고로 dreaded 는 “무서운” 이런뜻입니다.

첫 슬라이드는 다음과 같습니다. 뭔가 어려워보이죠? 각종 _ 의 용법은 모두 들어가 있습니다.
아주 간단하게 설명하면 아래의 offset 에 대입되는 커링 함수 sum2(count) 에서 count는 매번 실행시의 count 값이 바인딩됩니다.(악 벌써 여기부터 어려워!!!)

class Underscores {
	import collection.{ Map => _ , _ }

	var count : Int = _

	def sum = (_:Int) + (_:Int)
	def sum2(a:Int)(b:Int) = a+b
	def offset = sum2(count) _

	def sizeOf(l:Traversable[_]) : Unit = l match {
		case it:Iterable[Int] => count = (0/:it)(_ + _)
		case s:Seq[_] => s.foreach( _ => count = count + 1)
		case _ => println(offset(l.size))
	}
}

두번째 슬라이드는 각각 어떤 용법으로 이루어졌나를 보여줍니다. 총 6가지 용법을 색상까지 이쁘게 보여주네요.

세번째 슬라이드 부터 각각의 용법에 대해서 알려줍니다.

1번은 “모두”를 의미합니다. 아래의 예에서 첫번째 Map => _ 는 일단 무시하시고(5번이니깐요.)
그 뒤의 _ 가 자바에서의 import * 와 같은 용법입니다.(왜 *가 아닌지는…)

	import collection.{ Map => _ , _ }

실제 예는 다음과 같습니다.

import java.util._
val date = new Date()

import scala.util.control.Breaks._
breakable {
	for (i <- 0 to 10) { if (i == 5) break }
}

웬지 breakable은 Exception을 잡아서 나가는 것 같은 느낌이 진하게 납니다.

2번은 디폴트 값 지정입니다. 숫자면 0 문자면 null로 지정됩니다.

	var count : Int = _

다만 이렇게 지정하는 건 생성자에서만 되고 함수안에서는 되지 않습니다.

class Foo {
	var i:Int = _ // i = 0
	var s:String = _ // s = null

	def f {
	// var i:String = _//error: local variables must be initialized
	}
}

3번째는 unused variables 입니다. 아래와 같이 _로 받은걸 쓰지 않게 되는 겁니다.

		case _ => println(offset(l.size))

예를 보면 다음과 같습니다. 아래의 두 예는 같은 예입니다. x를 파라매터로 받지만… 쓰지 않는 겁니다.

(1 to 5) foreach { (x:Int) => println("one more")}
(1 to 5) foreach { _ => println("one more")}

다음 예제들도 동일합니다.

def inPatternMatching1(s:String) {
	s match {
		case "foo" => println("foo !")
		case x => println("not foo")
	}
}

def inPatternMatching2(s:String) {
	s match {
		case "foo" => println("foo !")
		case _ => println("not foo")
	}
}

4번째는 이름 없는 파라매터입니다. 아주 명확하게 들어갈 변수 대신에 지정되게 됩니다.(3번하고는 반대입죠.)
아래의 예는 1…10 까지가 x로 바인딩되는데, 이걸 _로 대체하는 경우입니다. 명시적으로 사용하기 위해서이죠.

(1 to 10) map { x => x + 1}
(1 to 10) map { _ + 1}

(1 to 10).foldLeft(0) { (x,y) => x+y }
(1 to 10).foldLeft(0) { _+_ }

이제 partial function 에서 보면 더더욱 눈이 휘둥굴해 집니다.

def f(i:Int) : String = i.toString

def g = (x:Int) => f(x)
def g2 = f _
def f2 = (_:String).toString

def u(i:Int)(d:Double) : String = i.toString + d.toString

def v = u _

def w1 = u(4) _

def w2 = u(_:Int)2.0)

5번은 아까 Map 관련 헤더들을 import 하지 말라는 뜻입니다.
6번은 c++의 Template 처럼 특정 타입을 지칭하는 것입니다. 위에서 넘어온 타입을 그대로 사용하겠다라고 하면 이해가 쉬울까요.

다시 한번 말씀드리지만, 슬라이드가 잘 되어있으니, 실제로 보시면 꽤 도움이 되실껍니다. 뭐, 저도 공부하는 중이라…

[입 개발] Scala 의 App Trait는 어떻게 동작하는가?

요새 스칼라 스터디를 하고 있는데…(스칼라 어려워요 흑흑흑, 전 스맹 T.T) 아주 여러가지 기능들이 있습니다. 그런데 첫 부분에 나오는 예제부터 머리속을 땡땡 때리는 경우가 있습니다. 간단한 예를 들자면, tuple의 파라매터가 22개 밖에 안되는 것은 실제로 tuple1 ~ tuple22 까지의 클래스가 있어서 처리된다는 것(tuple 은 다시 product 이라는 것을 상속받는…)

보통 우리가 언어를 처음 배울때 쓰는 첫 예제는… 반가워 세상입니다. 즉 Hello World! 를 출력하는 것이죠.

 object HelloWorld {
    def main(args: Array[String]) {
      println("Hello, world!")
    }
  }

그런데 App 이라는 trait 를 상속받으면 다음과 같은 형태로 똑같이 동작이 됩니다.

object HelloWorld extends App {
  println("Hello, world!")
}

사실 스칼라를 공부하는 사람이야 그냥 당연하게 넘어갈 수 있지만, 두 번째 예의 경우는 println 코드가 있는 부분은 생성자입니다. 그런데 “어떻게 저게 자동으로 실행이 되는거지?” 라는 의문이 생기게 됩니다.(안생기면 500원…), 그리고 args 도 사용할 수 있습니다.

그래서 안을 조금 파보니…

App Trait 는 다시 DelayedInit 라는 Trait를 상속받습니다. 먼저 App Trait 부터 살짝 보도록 하겠습니다.

trait App extends DelayedInit {

  /** The time when the execution of this program started, in milliseconds since 1
    * January 1970 UTC. */
  @deprecatedOverriding("executionStart should not be overridden", "2.11.0")
  val executionStart: Long = currentTime

  /** The command line arguments passed to the application's `main` method.
   */
  @deprecatedOverriding("args should not be overridden", "2.11.0")
  protected def args: Array[String] = _args

  private var _args: Array[String] = _

  private val initCode = new ListBuffer[() => Unit]

  /** The init hook. This saves all initialization code for execution within `main`.
   *  This method is normally never called directly from user code.
   *  Instead it is called as compiler-generated code for those classes and objects
   *  (but not traits) that inherit from the `DelayedInit` trait and that do not
   *  themselves define a `delayedInit` method.
   *  @param body the initialization code to be stored for later execution
   */
  @deprecated("The delayedInit mechanism will disappear.", "2.11.0")
  override def delayedInit(body: => Unit) {
    initCode += (() => body)
  }

    /** The main method.
   *  This stores all arguments so that they can be retrieved with `args`
   *  and then executes all initialization code segments in the order in which
   *  they were passed to `delayedInit`.
   *  @param args the arguments passed to the main method
   */
  @deprecatedOverriding("main should not be overridden", "2.11.0")
  def main(args: Array[String]) = {
    this._args = args
    for (proc <- initCode) proc()
    if (util.Properties.propIsSet("scala.time")) {
      val total = currentTime - executionStart
      Console.println("[total " + total + "ms]")
    }
  }
}

젤 아래의 main을 보면, 아 여기서 실행되겠구나 할것입니다. 쉽네하고 보다보면, 다시 이상해집니다. 분명히 main이 보통 entrypoint 일텐데…(실제로는 object이니 이것을 실행하는 부분이 있긴하겠죠.) 뭔가 initCode 라는 것에서 proc를 가져와서 이걸 실행시킵니다.

그 위의 delayedInit 함수를 보니, body가 넘어와서 initCode에 저장됩니다.(여기서 body는 람다라고 보시면 될듯합니다.)

그럼 다시 처음으로 여기서 main이 실행되는 건 알겠는데… App Trait를 상속받은 object의 생성자를 실행을 시켜주는 걸로 봐서 아마도 위의 proc 가 App Trait를 상속받은 object의 생성자일꺼라는 예상을 할 수 있게 됩니다. 그러나, 여전히 delayedInit을 호출해주는 녀석은 보이지 않습니다. 다시 App Trait 가 DelayedInit Trait를 상속받으니, 이걸 살펴보도록 하겠습니다.

trait DelayedInit {
  def delayedInit(x: => Unit): Unit
}

악!!! 살펴볼 내용이 없습니다. 그냥 인터페이스만 정의가 되어있습니다. 그럼 뭔가 언어적으로 뭔가 해주지 않을까 싶습니다. 소스를 까보면 src/reflect/scala/reflect/internal/Definitions.scala 에서 다음 코드를 발견할 수 있습니다.

def delayedInitMethod = getMemberMethod(DelayedInitClass, nme.delayedInit)

해당 클래스에서 delayedInit를 뽑아내는 것 같습니다.

그리고 src/compiler/scala/tools/nsc/transform/Constructors.scala 를 보면 다음 코드가 있습니다.

    private def delayedInitCall(closure: Tree) = localTyper.typedPos(impl.pos) {
      gen.mkMethodCall(This(clazz), delayedInitMethod, Nil, List(New(closure.symbol.tpe, This(clazz))))
    }

그리고 위의 delayedInitCall은 rewriteDelayedInit() 에서 사용하고 있습니다. delayedInitCall을 실제로
호출하게 됩니다. 즉 여기서 아까 delayedInit가 호출되면서 App Trait 의 initCode 쪽에 생성자를 넣어주는 것입니다. 그래서 실제로 App Trait 의 main에서 그걸 호출하게 되는거죠.

    def rewriteDelayedInit() {
      /* XXX This is not corect: remainingConstrStats.nonEmpty excludes too much,
       * but excluding it includes too much.  The constructor sequence being mimicked
       * needs to be reproduced with total fidelity.
       *
       * See test case files/run/bug4680.scala, the output of which is wrong in many
       * particulars.
       */
      val needsDelayedInit = (isDelayedInitSubclass && remainingConstrStats.nonEmpty)

      if (needsDelayedInit) {
        val delayedHook: DefDef = delayedEndpointDef(remainingConstrStats)
        defBuf += delayedHook
        val hookCallerClass = {
          // transform to make the closure-class' default constructor assign the the outer instance to its pa>
          val drillDown = new ConstructorTransformer(unit)
          drillDown transform delayedInitClosure(delayedHook.symbol.asInstanceOf[MethodSymbol])
        }
        defBuf += hookCallerClass
        remainingConstrStats = delayedInitCall(hookCallerClass) :: Nil
      }
    }

마지막으로 Constructors.scala 안에서 다시 rewriteDelayedInit를 실행합니다. 그래서 App Trait를 상속받을 경우 생성자에만 코드를 넣어두면 실행이 되는 것입니다.

뭐, 이게 맞는 플로우인지는 정확하게 보증은 못합니다. 저도 이제 막 스칼라를 공부하는 중이고, 아무리 봐도, 스칼라를 편안하게 쓰지는 못할듯 하네요 T.T 흑흑흑