다이나믹언어처럼 느껴지는 스태틱언어 : Scala
[원문]
Bruce Eckel
2011. 6. 12
2011. 6. 12
개요
파이썬 진영에서 가져올 수 있는 최고의 보완은 "Pythonic하다"라고 말하는 것입니다. 한번도 정적언어가 그렇게 느껴질 수 있다고 생각해보지 않았는데 스칼라는 그렇게 느껴집니다. 어쩌면 좀 더 좋은지도 모르겠습니다.
언어를 배우기 이전부터 오랫동안 이 순간을 기다려왔습니다. 왜냐하면 그동안 많은 이슈들이 있었기 때문입니다. 사실 언어의 여러가지 버전들은 하위호환성을 깨뜨려서 코드의 재작성을 필요하게 만들었습니다. 몇몇 사람들은 이런 점을 발견했을 때 당황하면서 언어가 "미숙"하고 "엔터프레이즈급에 대한 준비가 되지 않았다"고 생각했습니다. Scala가 약속하고 있는 것들 중 하나가 있는데 나중에 알게 될 수 있는 차선책이나 명백한 실수가 될 수 있는 결정을 조기에 미리 확정되도록 하지 않았다는 것입니다. Java는 오래전에 결정한 내용으로 인해 고정된 것들이 인터넷에서 약속된 기한까지 맞추기 위해서 더욱 나쁘게 만들어졌다는 것을 알 수 있는 완벽한 케이스스터디입니다. C++는 C 프로그래머들을 객체지향프로그래밍의 세계로 데려오면서 C호환성을 유지하기로 결정했을 때는 훌륭했었지만 문제점까지 카피한 것은 프로그래머들에게 더이상 좋은 것이 아니었습니다.
사실 언어의 디자인이 프로그래머들의 시간보다 중요하다는 마인드때문에 피곤했었습니다. 그 때문에 언어가 프로그래머를 위한 것이 아닌 프로그래머가 언어를 위해서 일해야 했습니다. 그래서 완전히 프로그래밍과 함께 자라왔지만 언어의 지난 세대에 지쳤고 이제 다음 세대를 기다리고 있습니다. 특히 진보적인 언어를 기다리고 있습니다.
당신이 이전의 제가 작성한 글을 보았다면 그 자체의 목적(보통 "만약 X가 int라는 것을 알 수 없다면 세계를 파멸될 것이다!"로 이어졌습니다.)을 위해서 정적으로 타입을 체크하는 것에 대해서 별로 좋아하지 않는다는 것을 알 것입니다. 이러한 영향을 받지 않으면서 파이썬으로 충분히 안정적인 코드를 작성했습니다. 더 적은 양의 명확한 파이썬 코드로 무엇을 할수 있는가에 비하면 C++과 자바에서 강제된 것을 하기 위한 비용이 훨씬 작아보입니다.
Scala는 정적 타입체크문제를 해결한 것처럼 보이는 첫번째 언어입니다. 놀랍도록 뒤틀려진 능력들은 정적 타입체킹이 없이도 가능할 거라고 생각합니다. 그리고 이 글에서 보여주려는 것처럼 정적타입체크는 비교적 간섭을 하지 않습니다. 그래서 스칼라로 프로그래밍하는 것은 파이썬 같은 동적 언어에서 프로그래밍하는 것처럼 느껴집니다.
단순히 타이핑양(Finger Typing)에 대한 것이 아닙니다.
파이썬같은 언어와 비교한 자바의 결점을 논의할 때 가장 많이 받은 반박은 "당신은 단순히 타이핑양(Finger Typing)에 대해서 불평하는 것입니다"라는 것입니다.(타입체킹에서 "Typing"과는 다른 개념입니다.)
"타이핑양"을 사소하게 생각할 수 있지만 경험상 실제로 큰 차이를 만들어 냅니다. 아이디어가 생겨서 표현하려고 할 때 아주 간단한 컨셉조차도 자바에서 필요한 코드양과 비교하면 훨씬 적은 코드량으로 표현할 수 있습니다. 진짜 문제는 타이핑 수가 아니라 정신적인 부하이다. 이렇게 강제된 것에 시간을 들이게 되면서 실제로 무엇을 타이핑하려고 했는지 잊어버리게 됩니다. 무언가를 하기 위해 강제된 형식들은 종종 시도조차 좌절시킬 것입니다.
Scala는 최대한 오버헤드(그리고 정신적 부하)를 없앴기 때문에 타이핑속도만큼 빠르게 higher-order 컨셉을 표현할 수 있습니다. 많은 경우에서 스칼라가 파이썬보다 더 간결하다는 것을 발견했을 때 놀랐습니다.
이 모든 결과가 제가 항상 파이썬을 사랑했던 이유입니다: 추상화레벨은 일반적으로 화이트보드에 다이어그램을 그리는 것보다 더 쉽고 명확하게 코드로 아이디어를 표현할 수 있다는 것입니다.
예제를 보겠습니다. 다음처럼 모델을 만들다고 가정해보겠습니다:
class Building
val b = new Building
클래스를 만드는 형식의 양이 절대적으로 적기 때문에 스케치할때 아주 좋습니다. 괄호가 필요없다면 괄호를 쓰지 않습니다. val은 불변(immutable)하면서 동시성코드를 더 쉽게 작성할 수 있기 때문에 스칼라에서는 더 선호됩니다.(변수에는 var도 있습니다.) 그리고 b에 어떤한 타입 정보도 작성하지 않았다는 것을 주목해야합니다. 왜냐하면 스칼라는 타입추론을 가지고 있기 때문에 알아서 타입을 찾아줍니다. 더이상 게으른 언어에 만족하고 있을 필요가 없습니다.
Building이 몇 평방피트인지 알기를 원한다면 명시적인 방법이 있습니다:
class Building(feet: Int) {
val squareFeet = feet
}
val b = new Building(100)
println(b.squareFeet)
타입정보를 제공해야 할 필요가 있을 때는 콜론(:) 다음에 타입정보를 주면 됩니다. println()이 자바의 System.out 범위를 필요로 하지 않는 다는 것을 알아야 합니다. 그리고 클래스 필드는 기본적으로 public 입니다. val로 선언한다면 읽기 전용이기 때문에 큰 문제가 아닙니다. 원한다면 언제든지 private로 만들수 있고 스칼라는 제가 본 어떤 언어보다 잘 만들어진 접근제어를 가지고 있습니다.
만약 하려는 것이 클래스에 아규먼트를 저장하는 것이 전부라면 스칼라에서는 쉽게 할 수 있습니다:
class Building(val feet: Int)
val b = new Building(100)
println(b.feet)
이제 feet는 자동적으로 필드가 되지만 거기서 끝이 아니고 Scala는 더 많은 것을 제공하는 case class를 가지고 있습니다. case클래스에서는 val을 작성할 필요없이 아규먼트는 자동적으로 필드가 됩니다.
case class Building(feet: Int)
val b = Building(100)
println(b) // Result: Building(100)
객체를 생성할때 new를 사용하지 않았고 파이썬에서도 동일합니다. case 클래스가 toString을 재작성해주기 때문에 보기 좋은 아웃풋을 생성합니다.
하지만 여기서 끝이 아닙니다. case 클래스는 자동으로 적절한 해쉬코드와 ==를 제공하기 때문에 Map에서 사용할 수 있습니다.(->는 값과 키를 구분합니다.)
val m = Map(Building(5000) -> "Big", Building(900) -> "Small", Building(2500) -> "Medium")
m(Building(900)) // Result: Small
Map은 어떤 import없이도 "스칼라가 기본적으로 제공하는 세트"의 일부로써 이용이 가능합니다.(추가적으로 List, Vector, Set, println()등이 있습니다.) 다시 말하지만 이것은 파이썬처럼 느껴집니다.
상속도 간단합니다. Building의 서브클래스로 House클래스를 만들어 보겠습니다:
class House(feet: Int) extends Building(feet)
val h = new House(100)
println(h.feet) // Result: 100
extends 키워드가 자바에서 익숙한 키워드이지만 베이스클래스의 생성자가 어떻게 호출되는지 알아야 합니다. 가장 명확한 방법은 직접 보는 것이고 설명하는데 반드시 필요한 것 외에는 어떤 코드도 작성하지 않을 것입니다.
trait를 사용해서 행동(behavior)을 믹스인할 수 있습니다. trait는 클래스가 생성될 때 합쳐질 수 있는 메서드정의를 포함할 수 있다는 점을 빼고는 인터페이스와 아주 유사합니다. 여기 house를 묘사하는 여러 개의 trait가 있습니다:
trait Bathroom
trait Kitchen
trait Bedroom {
def occupants() = { 1 }
}
class House(feet: Int) extends Building(feet) with Bathroom with Kitchen with Bedroom
var h = new House(100)
val o = h.occupants()
val feet = h.feet
occupants()는 일반적인 Scala의 메서드정의입니다: def 키워드뒤에는 메서드명과 아규먼트리스트가 오고 그 다음에 =가 오고 중괄호안에 메서드 바디가 옵니다. 메서드에서 마지막 라인은 리턴값이 됩니다. 여기에서 또다른 타입추론이 일어났습니다; 더 명확하기를 원한다면 메서드의 리턴타입을 명시해 줄 수 있습니다:
def occupants(): Int = { 1 }
occupants() 메서드는 traits의 믹스인(mixin)을 통해서 이제 Howse의 일부가 되었습니다.
이 코드가 얼마나 간단하고 얼마나 집중되어 있는지 보십시오. 자바에서처럼 반드시 해야하지만 의미는 없는 문법적인 요구사항을 설명하는 대신에 무엇을 하는지에 대해서 얘기할 수 있습니다. model을 생성하는 것은 몇줄안되는 간단한 코드일뿐이다. 초급 프로그래머에게 이것을 가르치는 것이 자바보다 쉽지 않습니까?
함수형 프로그래밍
펑셔널 프로그래밍은 종종 동시성을 처리하는 방법으로 가장 잘 알려져 있습니다. 하지만 근본적으로 프로그래밍 문제를 분석하는 방법에 더 유용하다는 것을 발견했습니다. 사실 C++는 내장된 동시성 지원없이도 STL의 형식으로 처음부터 가상적인 함수형 프로그래밍을 가지고 있습니다. 파이썬 또한 중요한 함수형 프로그래밍 라이브러리를 가지고 있지만 이러한 것들은 쓰레드 지원에 독립적입니다.(파이썬이 진짜 병렬프로그래밍을 지원할 수 없기 때문에 코드체계화를 위한 것입니다.)
스칼라는 가장 좋은 2개의 세계를 가지고 있습니다: 실제 멀티프로세서 병렬화와 강력한 함수형 프로그래밍 모델이 그것입니다. 하지만 적절치 않다면 함수형으로 프로그래밍하도록 강제하지는 않습니다.
제 생각에 함수형 프로그래밍 스타일에 접근할 때는 천천히 신사적으로 접근하는 것이 중요합니다. 많이 힘들다면 유저그룹에 참여할 수 있습니다. 사실 함수형 프로그래밍을 배웠을 때 가장 큰 이익의 하나는 문제를 증명할 수 있는 단계로 작게 나누는 훈련이 된다는 것입니다. 그리고 가능할 때마다 이러한 각 단계를 위해 이미 존재하는 코드(그리고 증명된)를 사용합니다. 이것은 비함수형 코드를 더 좋게 만들뿐만 아니라 함수형 프로그래밍이 데이터 변환에 초첨을 맞추기 때문에 작성한 모든 것이 더 테스트하기 좋도록 만듭니다. (그러므로 각 변환후에 테스트할 것을 가지게 됩니다.)
함수형 프로그래밍의 많은 부분은 컬렉션에서 오퍼레이션을 수행하는 것입니다. 예를 들어 Vector 데이터를 가지고 있습니다:
val v = Vector(1.1, 2.2, 3.3, 4.4)
for 루프를 사용해서 이것을 출력할 수 있습니다:
for(n <- v) {
println(n)
}
왼쪽 화살표는 "in"이라고 읽을 수 있습니다. 여기서 n은 v의 각 값이 됩니다. 이 문법은 C++와 Java에서처럼 각 세부사항을 갖는 명확한 단계입니다. (Scala가 n을 위한 모든 생성과 타입추론을 합니다.) 하지만 함수형 프로그래밍에서는 순회 구조를 합쳐서 추출합니다. Scala의 컬렉션과 이터레이션은 이를 위한 많은 오퍼레이션들을 제공합니다. 가장 간단한 방법 중 하나인 foreach는 컬렉션에서 각 엘리먼트상에서 오퍼레이션을 수행합니다. 그래서 위의 코드는 다음과 같이 됩니다:
v.foreach(println)
이는 실제로 여러가지 숏컷을 제공합니다. 그리고 함수형 프로그래밍의 이점을 최대한 취하기 위해서 우선적으로 익명함수에 대해서 이해해야 합니다. 익명함수는 이름이 없는 함수이고 여기 기본적인 형식이 있습니다:
( function parameters ) => function body
=>는 "rocket"이라고 읽으며 "좌측에 있는 파라미터를 우측에 있는 코드에 적용하라"는 의미입니다. 익명함수는 더 커질수도 있는데 여러절로 작성하였다면 바디를 중괄호안에 넣으면 됩니다.
익명함수의 간단한 예제가 있습니다:
(x:Int, y:Double) => x * y
앞의 foreach 호출은 명확하게 다음처럼 될 수 있습니다.
v.foreach((n:Double) => println(n))
보통 아규먼트상에서 타입추론을 하는 것은 Scala를 믿으면 됩니다. 이 경우에 스칼라는 v가 Double를 담고 있다는 것을 알 수 있으므로 n이 Double라는 것을 추론 할 수 있습니다.
v.foreach((n) => println(n))
아규먼트가 하나뿐이라면 괄호를 생략할 수 있습니다.
v.foreach(n => println(n))
하나의 아규먼트를 가지고 있다면 파라미터 리스트를 합칠 수 있고 익명함수 바디에서 언더스코어를 사용할 수 있습니다:
v.foreach(println(_))
마지막으로 함수바디가 파라미터 한개를 가지는 단일함수를 호출하는 것 뿐이라면 파라미터 리스트를 제거할 수 있습니다:
v.foreach(println)
함수형 프로그래밍에서는 이렇게 가능한 옵션과 밀도로 쉽고 명확하게 만들수 있기 때문에 사람들이 너무 복잡해서 언어를 거부하지 않도록 코드를 작성할 수 있습니다.
foreach는 사이드 이펙트때문에 아무것도 리턴하지 않습니다. 더 일반적인 함수형 프로그래밍에서 오퍼레이션을 수행하고(보통 컬렉션에서) 결과를 리턴한 다음 결과상에서 오퍼레이션을 수행하고 다른 것을 리턴합니다. 가장 유용한 함수형 기능 중 하나는 map입니다. 이 이름은 별로 안좋은데 왜냐하면 Map 데이터구조와 혼동하기 쉽기 때문입니다. map은 foreach처럼 순차적으로 각 엘리먼트위에서 오퍼레이션을 수행하지만 결과에서 새로운 순서를 생성해서 리턴합니다. 예를 들어:
v.map(n => n * 2)
v에서 각 엘리먼트가 2배가 되어 만들어진 결과를 리턴합니다:
Vector(2.2, 4.4, 6.6, 8.8)
또다시 숏컷을 사용해서 코드를 줄일 수 있습니다:
v.map(_ * 2)
다음처럼 파라미터 없이 호출될수 있는 간단한 오퍼레이션이 많이 있습니다:
v.reverse
v.sum
v.sorted
v.min
v.max
v.size
v.isEmpty
v.sum
v.sorted
v.min
v.max
v.size
v.isEmpty
reverse와 sorted같은 오퍼레이션은 새로운 Vector을 리턴하고 원래의 Vector는 건드리지 않습니다.
오퍼레이션들이 함께 체인되는 것은 일반적입니다. 예를 들어 순열(permutations)은 v의 다른 모든순열을 선택하는 이터레이터를 만듭니다. 이것들을 표현하기 위해서 foreach에 이터레이터에 전달합니다:
v.permutations.foreach(println)
또다른 유용한 함수는 zip입니다. zip은 지퍼처럼 2개의 시퀀스를 가지고 각 인접한 엘리먼트를 하나로 합칩니다:
Vector(1,2,3).zip(Vector(4,5,6))
위를 실행하면 다음의 결과가 됩니다.
Vector((1,4), (2,5), (3,6))
(파이썬에서처럼 Vector내에서 괄호로 묶인 것은 튜플(tuple)입니다.)
더 간단하게 만들 수 있는데 v의 엘리먼트와 v의 엘리먼트를 2배수한 값을 하나로 합칩니다:
v.zip(v.map(_ * 2))
위 코드는 아래의 결과를 생성합니다:
Vector((1.1,2.2), (2.2,4.4), (3.3,6.6), (4.4,8.8))
익명함수가 편리하고 일반적으로 사용되지만 함수형프로그래밍을 하는 본질적인 이유는 아니라는 것을 알아야 합니다. 익명함수가 코드를 너무 복잡하게 만든다면 언제든지 이름이 있는 함수를 정의해서 전달할 수 있습니다. 예를 들어:
def timesTwo(d: Double) = d * 2
(여기서 또 하나의 Scala 숏컷을 사용했습니다: 함수의 바디를 한줄로 작성할 수 있다면 중괄호는 필요없습니다.) 이것은 익명함수 대신 사용할 될 수 있습니다:
v.zip(v.map(timesTwo))
앞에서 for루프를 사용했던 코드와 같은 효과를 만들어 냅니다. 함수형 프로그래밍의 가장 좋은 점은 일반적인 오류들을 포함하고 있는 다루기 어려운 코드를 다룰 수 있다는 것입니다. 신뢰할 수 있는 함수형 코드블럭을 사용할 수 있고 더 빠르게 안정적인 코드를 작성할 수 있습니다. 함수형 코드는 확실히 쉽게 읽기 어려워질 수 있지만 약간의 노력으로도 명확하게 유지할 수 있습니다.
내가 함수형 프로그래밍을 해서 가장 좋았던 점은 그것이 만들어내는 정신적인 훈련입니다. 문제를 작고 테스트할 수 있으면서도 뚜렷하게 분석할 수 있게 나누는 법을 배우게 되었습니다. 때문에 이것은 시간을 들일만한 연습입니다.
패턴 매칭
프로그래머들이 얼마나 오랫동안 어셈블리 언어구조에서 작업을 했는가를 생각해 보면 놀라운 일입니다. switch문은 훌륭한 예제입니다. 진지하게 완전한 값기반이 되었는가? 실제로 도움이 되는 노력이 얼마나 있는가? 사람들은 switch에서 문자열을 사용하는 것 같은 간단한 것을 원하지만 언어 디자이너들은 보통 "안된다"는 답변을 한다.
Scala는 어떤 것이든 선택할 수 있다는 것을 제외하고는 switch문과 유사해 보입니다. 명확성은 커지고 코드는 훨씬 줄어듭니다:
// PatternMatching.scala (Run as script: scala PatternMatching.scala)
trait Color
case class Red(saturation: Int) extends Color
case class Green(saturation: Int) extends Color
case class Blue(saturation: Int) extends Color
def matcher(arg:Any): String = arg match {
case "Chowder" => "Make with clams"
case x: Int => "An Int with value " + x
case Red(100) => "Red sat 100"
case Green(s) => "Green sat " + s
case c: Color => "Some Color: " + c
case w: Any => "Whatever: " + w
case _ => "Default, but Any captures all"
}
val v = Vector(1, "Chowder", Red(100), Green(50), Blue(0), 3.14)
v.foreach(x => println(matcher(x)))
case클래스는 앞에서 본 것처럼 패턴 매처가 분석할 수 있기 때문에 특히 유용합니다.
Java에서 primitive타입이 될 수 있는 것들을 포함한 모든 오브젝트는 루트 클래스가 될수 있습니다. matcher()는 Any를 받기 때문에 전달하는 어떤 타입도 다룰수 있습니다.
보통 =기호 우측에 전체 함수 바디를 감싸는 중괄호가 있습니다. 이 경우에 함수바디는 하나의 문장이기 때문에 숏컷을 사용해서 중괄호를 생략할 수 있습니다.
패턴매칭문은 매칭하기를 원하는 오브젝트로 시작합니다.(이것은 튜플이 될 수 있습니다.) match 키워드와 순차적인 case문으로 이루어진 바디가 있습니다. 각 case문은 매치할 패턴으로 시작하고 매치되었을 때 실행될 코드들로 되어 있습니다. 각 case문의 마지막 라인은 리턴값을 생성합니다.
매치표현식은 여러가지 형식을 가질 수 있지만 여기서는 몇가지만 보여주었습니다. 첫번째로는 간단한 문자열 매치를 보았습니다; 하지만 scala는 세련된 정규표현식 문법을 가지고 있고 매치표현식에 변수로 선택하는 정규표현식을 사용할 수 있습니다. case x: Int에서처럼 변수로 매치문의 결과를 받을 수 있습니다. case 클래스는 Red(100)에서처럼 정확히 매치를 생성하거나 Green(s)에서처럼 생성자 아규먼트를 선택할 수도 있다. c: Color처럼 trait를 매치할 수도 있다.
그 외 모든 것에 매치하고 싶다면 2가지 선택이 있습니다. case w: Any처럼 변수를 잡기 위해서 Any를 매치할 수 있습니다. 값이 무엇인가에 신경쓰지 않는다면 그냥 case _ 라고 작성할 수 있습니다.
각 case 바디끝에 "break"문은 필요없습니다.
액터를 사용한 동시성
프로그래밍에서 나를 멀어지게 하는 것 중 대부분은 무엇인지는 알아냈지만 다른 것으로 설득력있게 표현할 수 없는 것들입니다. 이러한 것들은 컴퓨터과학자들이 증명해야 하는 것들입니다. 다음처럼:
프로그램 복잡도의 어떤 레벨을 뛰어 넘어 반드시 가비지 컬렉터를 가져야 합니다. 객체가 하나의 컬렉션이상에 소속될 수 있는 어떤 프로그램처럼 간단하게 될 수 있습니다. 하지만 어떤 면에서는 직접 메모리를 관리하는 것이 불가능하게 된다고 믿습니다.(C++0X가 지금은 가비지 컬렉션을 후킹했다고 하더라도 C++ 사람들은 이것을 갖지 않았습니다.)
체크드 익셉션은 실패한 실험입니다. 작은 프로그램에서는 좋은 아이디어처럼 보였지만 확장이 어렵습니다.
공유된 메모리(Shared-memory) 동시성은 옳은 방법을 가지는 것 자체가 불가능합니다. 이론적으로 세계에서 가장 똑똑한 개발자들은 모든 경쟁상태를 충분히 오랫동안 추적하면서 고치는 두더지잡기 게임을 할 수 있지만 당신이 해야하는 모든 것은 프로그램을 프로그램을 약간 고치는 것이고 모든 것은 다시 돌아옵니다. 공유된 메모리는 동시성을 위해서는 잘못된 모델입니다.
이 모든 것은 확장성 이슈라는 것을 알아야 합니다. 프로그램이 더 커지고 복잡해지는 것에서 떨어져서 작게 시작하는 것입니다. 왜냐하면 데모 예제는 작고 명백하기 때문에 이것을 논의하는 것이 왜 어려운가 하는 것입니다.
잘못된 사람들과 논의하는 것을 멈추었습니다. 오히려 옳은 사람들은 그것에 대해서 논의하지 않습니다. 그들은 문제를 해결하고 문제가 동시성이 되었을 때의 정답은 엉망이 되지 않게 하는 것입니다: 당신은 안전한 지역내에 있고 메시지는 안전하게 전달될 것입니다. 어떤 것이 락이 걸리는지(로우레벨에 대한 것은 아닙니다.)에 대해서 생각하지 않아도 됩니다; 자신의 쓰레드에서 실행되는 약간 안전한 지역내에서 유지됩니다.
이것에 대한 객체적인 접근의 대부분은 actor 입니다. 액터는 종종 "mailbox"로 참조되는 들어오는 메시지 큐를 가지고 있는 객체입니다. 당신의 보호된 기능 밖의 누군가가 당신에게 무언가를 하기를 원할때 mailbox에서 안전하게 나타나는 메시지를 보냅니다. 그리고 이 메세지를 어떻게 다룰지를 결정합니다. 메일박스를 통해서 다른 액터에게 메시지를 보낼 수 있습니다. 모든 것을 안전한 지역안에 유지하고 있고 오직 메시지로 통신한다면 당신은 안전합니다.
액터를 생성하려면 Actor클래스를 상속받고 mailbox 메시지를 다루기 위해서 호출되는 act()메서드를 정의합니다. 여기 제가 생각할 수 있는 가장 평범한 예제가 있습니다:
// Bunnies.scala (Run as script: scala Bunnies.scala)
case object Hop
case object Stop
case class Bunny(id: Int) extends scala.actors.Actor {
this ! Hop // Constructor code
start() // ditto
def act() {
loop {
react {
case Hop =>
print(this + " ")
this ! Hop
Thread.sleep(500)
case Stop =>
println("Stopping " + this)
exit()
}
}
}
}
val bunnies = Range(0,10).map(new Bunny(_))
println("Press RETURN to quit")
readLine
bunnies.foreach(_ ! Stop)
act()메서드는 언어에 내장되어있지 않더라도 자동적으로 매치문이 됩니다. 스칼라는 이 방법으로 동작하는 Actor라이브러리를 만들어서 사용합니다. 매치문이기 때문에 case객체는 메시지에서 아주 잘 동작합니다.(어떤 매치문에서라도 가상적으로 어떤 것도 매치할 수 있습니다.) case 객체는 자동적으로 싱글톤 객체를 생성하는 것 외에는 case클래스와 유사합니다.
loop{ react { 구조는 처음에는 약간 이상해 보입니다; 이것은 스칼라 액터가 진화한 결과물입니다. 초기디자인에서는 메일박스 메시지를 위한 매치문을 열기 위해서 loop만을 가졌습니다. 하지만 나중에 쓰레드에 의해서 제공된 동시성이 협력하는 멀티태스킹과 합쳐질수 있도록 결정되었습니다. 그 점에서 제어하는 싱글쓰레드는 주위의(협력적으로) 태스크들에 전달됩니다. 각 태스크는 무언가를 하고 그 다음에 명백하게 제어를 포기해서 다은 태스크에 전달됩니다.
협력적인 멀티태스킹의 이점은 가상적으로 스택공간이나 컨텍스트 스위칭 시간을 요구하지 않는다는 것이고 그래서 종종 100만개의 태스크까지 확장할 수 있습니다. 이렇게 쓰레드의 동시성을 합침으로써 협력적인 태스크의 속도와 확장성이라는 2가지 최상의 것을 가질 수 있습니다: 이것들은 또한 이용가능한 많은 프로세서로 분산됩니다. 이것들은 모두 투명하게 됩니다. loop{ react{ 구조는 기본적인 선택이 될것이고 어떤 비용도 들지 않습니다. 그들이 스케치하면서 액터를 만들었다고 짐작합니다. 이 구조는 아마도 그냥 loop{로 간단화 될 것입니다.
Bunny클래스의 시작부분에 있는 2개의 라인을 보십시오. 스칼라에서 객체 초기화 코드를 특별한 메서드에 넣을 필요가 없습니다. 클래스의 바디 어디에나 둘 수 있습니다. 첫 라인은 메시지를 보내는 Actor 오퍼레이터 !를 사용합니다. 그리고 이 경우에 작업하기 위해서 객체는 메세지를 자기자신에게 보냅니다. 그 다음 start()를 호출해서 액터의 메세지 루프를 시작합니다. 액터가 Hop 메세지를 받았을 때 그 자신을 출력하고 자신을 다른 Hop메시지에 보낸 후 0.5초동안 멈춥니다. Stop 메시지를 받았을때 Actor.exit()를 호출해서 이벤트 루프를 멈춥니다.
Bunny 객체를 생성하기 위해서 Bunny생성자를 호출에 매핑되는 0부터 9까지의 순서를 만들기 위해서 Range()를 사용했습니다. readLine은 캐리지리턴을 사용자가 누르기를 기다리리다가 눌러지면 Stop메시지가 각 Bunny에 전달됩니다.
Scala 2.9는 foreach, map같은 오퍼레이션이 멀티프로세스를 쉽게 사용하는 강력한 방법인 병렬컬렉션을 포함하고 있습니다. 비용이 큰 함수 처리인 toBeProcessed를 호출하는 데이터객체의 컬렉션을 가지고 있다고 가정해 보겠습니다. 자동적으로 병렬로 프로세싱하기 위해서 단지 .par를 더하면 됩니다.
val result = toBeProcessed.par.map(obj => process(obj))
병렬로 프로세싱될 수 있는 객체를 가지고 있다는 것을 알고 있다면 이 구조는 거의 노력이 필요없습니다. 이번 Scala Days 2010 비디오에서 병렬 컬렉션에 대해서 더 찾아볼 수 있습니다.
더 강력한 것은 akka 라이브러리입니다. 이것은 다른 것들 가운데 동시성 시스템을 만들고 투명하게 원격적입니다.
스칼라는 동시성 프로그래밍을 위한 내가 본 가장 좋은 솔루션입니다. 그리고 점점 더 좋아지고 있습니다.
하지만 스칼라는 너무 복잡합니다!
스칼라는 좋은 이유였지만 복잡해져버린 잘못된 아이디어를 경험했습니다. 많은 얼리어댑터들은 그것들이 얼마나 명확한지 보여주기를 원하는 언어 열광자들이었습니다. 이것은 오직 초심자들에게만 혼란스러운 것입니다. 하지만 앞의 코드들에서 본 것처럼 스칼라를 배우는 것은 자바를 배우는 것보다 훨씬 쉬워질 것입니다. 단순히 "Hello, World!" 작성하는데 필수적인 자바의 형식같은 건 없습니다. 스칼라에서는 다음처럼 한줄로 작성할 수 있습니다:
println("Hello, world!")
또는 스칼라의 익터렉티브 인터프리터를 통해서 쉽게 언어를 실행해 볼 수 있습니다.
파일을 열고 내용을 처리한다고 가정해보겠습니다.(자바에서 아주 많은 형식을 가지고 있는 것입니다.):
val fileLines = io.Source.fromFile("Colors.scala").getLines.toList
fileLines.foreach(println)
(이 경우에 "프로세싱"은 단순히 각 라인을 출력하는 것입니다.) 파일을 열고 모든 라인을 읽는데 필요한 코드의 간결함은 언어의 모든 파워가 합쳐진 것입니다. 이것은 스칼라가 스크립팅 문제를 해결하는데 유용하다는 것을 의미합니다.(또한 스칼라는 XML을 위해서 강력한 네이티브 지원을 가지고 있습니다.)
단어갯수 세기 프로그램을 만들기 위해서 약간 수정해 보겠습니다:
for(file <- args) {
print(file + ": ")
val contents = io.Source.fromFile(file).getLines.mkString
println(contents.split(" ").length)
}
args는 모든 프로그램에서 이용가능하며 모든 커맨드라인 아규먼트를 담고 있기 때문에 이 프로그램은 한번에 하나씩 처리한다. 여기서는 단어를 공백으로 분리했지만 스칼라는 정규표현식도 가지고 있습니다.
전문적인 기술적 해설이 필요한 복잡한 코드를 작성하는 것도 가능하지만 전적으로 초심자를 가르치는데는 이런 코드를 작성할 필요가 없습니다. 사실 제대로 배웠다면 스칼라가 다른 언어보다 훨씬 간단하고 일관된 언어라는 생각을 갖게 될 것입니다.
어떻게 배우는가
제가 본 모든 스칼라 튜토리얼은 대상을 자바프로그래머라는 가정을 하고 있었습니다. 앞에서 보여준 것처럼 자바를 배우기 위해서 강제적으로 해야했던 것보다 스칼라가 훨씬 덜 혼란스럽기 때문에 첫 언어로 배울 수 있음에도 자바를 배웠다는 것을 가정하는 것은 유감스러운 일입니다. 하지만 프로그래밍을 할 수 있다고 가정한다면 튜토리얼 작성자에게는 훨씬 쉬운 일입니다.
- 아주 유용한 시작점으로 Daniel Spiewak의 Scala for Java Refugees라는 제목의 블로그글들이 있습니다.
- 다음으로 Martin Odersky, Lex Spoon, Bill Venners의 Programming in Scala 2판을 읽었습니다. odersky는 스칼라의 창시자이기 때문에 이 책은 믿을만 합니다. 독자가 자바프로그래머라고 가정하고 있으며 입문서는 아니지만 스칼라가 무엇을 할수 있는지에 대한 완전한 관점을 주는 책입니다.
- 스칼라 웹사이트는 아주 유용한 무료 튜토리얼들을 가지고 있습니다.
- 또한 다른 책을 읽음으로써 새로운 관점을 보았습니다. 예를 들어 Venkat Subramaniam의 Programming Scala의 예제들이 있습니다. 비록 이 책은 너무 영리하게 작성되었다는 것이 명백하고(첫 챕터에서 너무 이해하기 어려운 예제를 주었고 그 다음에 걱정하지 말라고 말한 다음에 공부하고 이해하라고 말합니다.) 확실히 첫번째 책은 되지 말아야 하지만 도움이 되는 다른 관점을 발견했습니다.
- 이러한 것들을 읽은 후에 5월에 Ann Arbor에서 열린 Bill Venners(Programming in Scala의 공동저자)와 Dick Wall(Java Posse의 리더)의 Stairway to Scala워크샵을 갔다왔습니다. 샌프랜시스코에서 8월 8~12에도 열립니다.; 더 찾아볼수 있고 여기서 등록할 수 있습니다: http://www.artima.com/shop/stairway_to_scala 이것은 입문클래스는 아니고 반드시 프로그래밍 경험이 있어야 합니다. 대부분 자바를 참조하기 때문에 자바를 할 줄 안다면 이상적입니다. 그리고 그전에 Programming in Scala를 읽기를 강력하게 추천하고 가능하다면 다른 책도 읽는다면 좋을 것입니다. 깊게 이해하지 못했다고 하더라도 먼저 당신의 뇌를 열어주어 더 편안하게 개념을 받아들수 있게 될 것입니다. 세미나 전에 공부하는 것은 아주 커다란 차이가 있습니다.
- Prgramming Summer Camp동안에 "Scala Campsite"에서 3개의 부스가 있습니다.
이 글에서 이야기한 것과 전혀 언급하지 않은 언어의 특징들이 있습니다. 여기서 보여준 것은 스칼라를 배우거나 사용하라고 하는 것일 수도 있고 선호하는 언어의로 돌아갈 수도 있습니다.
번역이 날이 갈수록 자연스러워지는 것 같아요~ (체질인가!)
그럴리가요.. 이번건 너무 길어서 더 어색한데요 ㅠㅠ