Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Scala 2.9+ 의 새로운 기능들

지난 5월 12일 Scala 2.9.0이 공개되었고 8월 31에 2.9.1이 공개되었습니다.  2.9가 나온지는 좀 되었지만 따로 살펴보지는 않고 있다가 어쩌다 보니 2.9 변동사항에 대한 정리를 맡게(?) 되어서 이렇게 블로그에 올립니다.
 


개선된 REPL
REPL의 입력핸들러인 jline이 개선되었고 커서핸들링은 더 신뢰할 수 있게 수정되었으며 bash 스타일, ctrl-R히스토리 검색, :imports, :implicits, :keybindings등의 새로운 명령어가 추가되었습니다.


scala> :keybindings
Reading jline properties for default key bindings.
Accuracy not guaranteed: treat this as a guideline only.

  1 CTRL-A: move to the beginning of the line
  2 CTRL-B: move to the previous character
  4 CTRL-D: close out the input stream
  5 CTRL-E: move the cursor to the end of the line
  6 CTRL-F: move to the next character
  7 CTRL-G: abort
  8 BACKSPACE, CTRL-H: delete the previous character 8 is the ASCII code for backspace and therefor deleting the previous character
  9 TAB, CTRL-I: signal that console completion should be attempted
 10 CTRL-J, CTRL-M: newline
 11 CTRL-K: erase the current line
 12 CTRL-L: clear screen
 13 ENTER: newline
 14 CTRL-N: scroll to the next element in the history buffer
 15 CTRL-O: move to the previous word
 16 CTRL-P: scroll to the previous element in the history buffer
 18 CTRL-R: search history
 20 CTRL-T: move to next word
 21 CTRL-U: delete all the characters before the cursor position
 22 CTRL-V: paste the contents of the clipboard (useful for Windows terminal)
 23 CTRL-W: delete the word directly before the cursor
 24 CTRL-X: delete the word directly after the cursor
127 DELETE, CTRL-?: delete the next character 127 is the ASCII code for delete

:keybindings를 입력하면 REPL내에서 사용할 수 있는 단축키를 확인할 수 있습니다.


scala> :type
:type <expression>
scala> :type 1
Int
scala> :type "test"
java.lang.String
scala> :type 1 :: 2 :: Nil
List[Int]

:type를 사용하면 평가없이도 타입을 확인해 볼 수 있습니다.


(reverse-i-search)`cla': class A {

Ctrl+R을 입력하면 REPL에서 입력한 히스토리를 검색할 수 있습니다. 위 쉘은 Ctrl+R을 입력하고 'cla'를 입력한 화면이고 입력하는 문자열로 시작하는 명령어를 찾아주고 엔터를 입력하면 해당 명령어를 실행해줍니다.


// Converter.scala
package Test

class Converter(n: Int) {
  def hour = { n + " hours" }
}
object Converter {
}


// ImplicitTest.scala
package Demo

import Test.Converter

object ImplicitTest {
  implicit def converter(n: Int) = new Converter(n)
}


scala> :implicits
No implicits have been imported other than those in Predef.

scala> import Demo.ImplicitTest._
import Demo.ImplicitTest._

scala> :import
 1) import java.lang._             (163 types, 168 terms)
 2) import scala._                 (798 types, 806 terms)
 3) import scala.Predef._          (16 types, 167 terms, 96 are implicit)
 4) import Demo.ImplicitTest._     (25 terms, 1 are implicit)

scala> :implicits
/* 1 implicit members imported from Demo.ImplicitTest */
  /* 1 defined in Demo.ImplicitTest */
  implicit def converter(n: Int): Test.Converter

:implicits를 사용하면 현재 범위내에서 임포트된 implicit 메서드를 보여줍니다. (물론 REPL에서 사용하기 전에 위의 두 파일을 컴파일 해주어야 합니다.) 여기서 나오는 implicit는 임포트된것만 보여주기 때문에 REPL에서 정의한 것은 나오지 않습니다. Predef에 나온 implicits의 리스트를 보고 싶다면 :implicits -v를 사용합니다.


scala> :imports
 1) import java.lang._             (156 types, 161 terms)
 2) import scala._                 (798 types, 806 terms)
 3) import scala.Predef._          (16 types, 167 terms, 96 are implicit)

:imports를 입력하면 현재 import되어 있는 클래스들을 보여줍니다.


scala> class A { private def test {println("test")}}
defined class A

scala> object A { def method { var a = new A; a.test }}
<console>:8: error: method test in class A cannot be accessed in A
       object A { def method { var a = new A; a.test }}
                                                ^

REPL은 라인단위로 실행하고 평가하기 때문에 위와 같이 companion object같은 경우는 정의할 수 없습니다. 이런 경우는 :paste를 사용합니다.


scala> :paste
// Entering paste mode (ctrl-D to finish)

class A { private def test {println("test")}}
object A { def method { var a = new A; a.test }}

// Exiting paste mode, now interpreting.

defined class A
defined module A

scala> A.method
test

:paste를 사용하면 paste mode에 들어가며 여기서 여러라인을 한꺼번에 입력할 수 있고 입력이 완료되면 ctrl+D로 종료하면 한꺼번에 실행되기 때문에 companion object를 정의할 수 있습니다.


scala> :javap A
Compiled from "A.scala"
public class A extends java.lang.Object implements scala.ScalaObject{
    public static final void methodB();
    public void methodA();
    public A();
}

scala> class Test
defined class Test

scala> :javap Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
    public Test();
}

:javap 명령어를 통해서 scala파일이나 클래스를 디컴파일해서 볼 수 있습니다.



SBT의 Process
SBT의 Process 라이브러리가 Scala에 포함되었고 sys.process 팩키지에서 사용할 수 있습니다.


temp $ cat SBTDemo.scala 
import sys.process._

Process("pwd") !
temp $ scala SBTDemo.scala 
/Users/outsider/projects/scala/temp

Process 라이브러리에 대해서는 Wiki문서를 참고하세요.



App Trait
2.9.0에서 추가된 App trait는 deprecated된 Application trait보다 안전하고 강력해 졌습니다. 이제 메인 애플리케이션은 다음과 같이 작성합니다.


object Echo extends App {
     println("Echo" + (args mkString " "))
}

Application trait를 상속받았을 때는 쓰레드 세이프하지 않았고 종종 VM이 최적하지 못했기 때문에 애플케이션의 바디가 오브젝트의 초기화 과정에서 실행되었습니다. App trait를 상속받으면 Scala 2.9의 지연 초기화기능으로 메인메서드의 일부로 바디를 실행합니다. 또한 기존에 불가능했던 커맨트라인 아규먼트를 args를 통해서 접근할 수 있습니다.



DelayedInit Trait
DelayedInit trait는 클래스와 객체의 초기화 순서를 커스터마이징해줍니다. 클래스나 오브젝트가 DelayedInit trait를 상속받았을 때 모든 초기화 코드는 클로저안에 묶이고 delayedInit 메서드에 아규먼트로 전달됩니다. delayedInitDelayedInit trait의 추상메서드로 정의되어 있기 때문에 delayedInit를 맘대로 구현할 수 있습니다. 예를들 어 스칼라의 새로운 App trait는 모든  초기화 순서를 내부 버퍼에 저장하고 오브젝트의 메인메서드가 호출될 때 실행합니다.

초기화 코드는 클래스안에만 담겨지고 오브젝트는 DelayedInit으로 전달됩니다. trait안에 있는 초기화 코드는 영향받지 않습니다.


// DelayTest.scala
class A {
  println("before Child Class")
  println("after Child Class")
}

class B extends A {
  println("initialized B")
}

class C extends A {
  println("initialized C")
}

val childB = new B
println("----")
val childC = new C

A 클래스를 상속받은 B와 C클래스가 있고 B와 C를 생성하면 다음과 같은 결과나 나옵니다.


$ scala DelayTest.scala 
before Child Class
after Child Class
initialized B
----
before Child Class
after Child Class
initialized C

A가 생성자가 실행되고 B와 C의 생성자가 실행됩니다. 자식의 생성자가 실행되는 앞뒤로 어떤 액션을 할 필요가 있다면 다음처럼 작성할 수 있습니다.(간단한 사용테스트 일뿐 정확한 DelayedInit의 용도는 찾아봐야 할듯 합니다.)


// DelayTest2.scala
class A extends DelayedInit {
  println("initialized A")
  override def delayedInit(body: => Unit) {
    println("before Child Class")
    body
    println("after Child Class")
  }
}

class B extends A {
  println("initialized B")
}

class C extends A {
  println("initialized C")
}

val childB = new B
println("----")
val childC = new C

A 클래스에서 DelayedInit을 상속받아서 내부에서 delayedInit을 재정의하였습니다. delayedInit은 파라미터로 생성자의 바디를 받게 되고 함수 내부에서 원하는대로 사용할 수 있습니다. 이 코드를 실행하면 다음과 같이 됩니다.


$ scala DelayTest2.scala 
before Child Class
initialized A
after Child Class
before Child Class
initialized B
after Child Class
----
before Child Class
initialized A
after Child Class
before Child Class
initialized C
after Child Class



Scala Runner
스칼라코드를 다음과 같은 방법으로 실행할 수 있습니다:

  • scala <jarfile> : 메인클래스를 실행하며 java -jar와 비슷합니다.
  • scala <classname> : 클래스의 메인메서드를 실행합니다.
  • scala <sourcefile> : 스칼라 스크립트로 실행합니다
  • scala <sourcefile> : 소스파일이 스크립트가 아니면 최상위 객체의 메인 메서드를 찾아서 실행합니다. scalac를 하고 바로 실행하는 것과 같습니다.
  • scala -save <sourcefile> : 컴파일된 소스로 jar파일을 생성해서 나중에 scala <jarfile>로 실행할 수 있습니다.


Java Interop
@Strictfp 어노테이션을 지원합니다.(자바의 @Strictfp를 지원하도록 추가된 것이라는데  자세히 몰라서 설명을 패스합니다.) JavaConverters와 JavaConversions에서 많은 부분이 수정되어 더욱 자연스럽게 상호작용할 수 있게 되었습니다. 프리미티브타입과 박싱된 타입은 암묵적으로 변환됩니다.


그 외 기능들...
try-catch-finally의 일반화되었습니다.


try body
catch handler
finally cleanup

body와 cleanup에는 임의의 표현식을 사용할 수 있고 handler에는 유효한 예외핸들러(PartialFunction[Throwable, T])는 어떤 것이든 사용할 수 있습니다.

그 외 collectFirst, maxBy, minBy, span, inits, tails, permutations, combinations, subsets가 컬렉션에 추가되었고 AnyRef의 전문화로 타입파라미터에 AnyRef의 서브타입의 사용이 가능해져서(class Foo[@specialize(AnyRef) T](arr: Array[T]) { … }) 배열의 인덱싱과 갱신이 효율적이 되었습니다. 그리고 많은 버그가 수정되고 성능향상이 이루어졌습니다.



덧) 2.9의 대표적인 기능인 Parallel Collection은 내용이 많아서 따로.. ㅎ


[참고 문서]
Scala 2.9.0 final
On Scala 2.9's road...
scala: how to wrap execution of subclass constructor?
2011/09/12 00:59 2011/09/12 00:59