Posted in scala
Scala for the Impatient, Chapter 1, 2, 3
Comment
Chapter 1
(1). 스칼라의 REPL 은 엄밀히 말해서 인터프리터가 아니다. 입력받은 코드를 자바 바이트코드로 컴파일 한 후 자바 가상머신에서 실행시킨뒤 결과를 돌려준다.
(2). 스칼라는 문자열을 위한 추가적인 연산들을 제공하기 위해 java.lang.String
오브젝트를 StringOps
오브젝트로 변환한다. "Hello".intersect("World")
가 그 예다. 따라서 ScalaDoc 을 보려면 SpringOps
클래스를 살펴보는 편이 정신 건강에 좋다.
마찬가지로 Int
, Double
등에도 편의 메소드를 추가한 RichInt
, RichDouble
, RichChar
등을 제공한다. 1.to(10)
에서 1
은 RichInt
로 변환된 뒤에 to
메소드를 적용한다.
참고로 스칼라에서는 숫자간 타입 변환을 위해 캐스팅이 아니라 메소드를 사용한다. 99.44.toInt
, 99.toChar
, "99.44".toDouble
(3). 스칼라에는 ++
연산자가 없다.
(4). 스칼라에는 Static Method 대신 Singleton Object 와 Companion Object 가 있다.
(5). 오브젝트를 수정하지 않는, 인자가 없는 메소드는 괄호를 사용하지 않는다.
"Hello".distinct
(6). ScalaDoc 에서 implicit 로 태그된 메소드는 자동변환이다. 예를들어 BigInt
오브젝트는 필요할때 자동으로 int
와 long
을 BigInt
로 바꾼다.
Chapter 2
(1). 스칼라에서는 구문(Statement) 이 아니라 모든 것을 식(Expression) 으로 취급한다. 그런데, else
가 없는 if
문은 값이 없을 수 있는데, 스칼라에서는 Unit
클래스를 도입해서 해결한다. Unit
은 값 없음 을 뜻하는 ()
을 값으로 가진다.
(2). 스칼라에서 할당(assignment) 은 Unit
타입의 값을 가진다. 따라서 할당을 묶어서 사용하지 않는다.
x = y = z = 1 // no
(3) for
루프에서 인덱스가 필요하면, until
을 사용하면 된다.
val str = "lambda"
var sum = 0
for (index < - 0until str.length)
sum += index
(4). return
이 없는 삶에 익숙해지자, 익명함수를 사용할 경우 리턴값이 쓸모가 없다. break
쯤으로 여기는 것이 마음 편하다.
(5). 재귀 함수는 타입을 반드시 명시해야 한다. ML 이나 하스켈 같은 일부 언어는 힌들리-밀너 알고리즘을 이용하여 재귀함수의 타입을 추론할 수 있지만, 이 알고리즘은 객체지향과는 잘 안맞는다.
(6). 가변인자가 필요하면, *
를 사용하자. 배열을 풀어헤치기 위해 사용하는 _*
는 Splat operator 다
def sum(args: Int*) = {
var sum = 0;
for(arg <- args) sum += arg
sum
}
sum(1 to 5: _*)
def recursiveSum(args: Int*): Int = {
if (args.length == 0) 0
else args.head + recursiveSum(args.tail : _*)
}
참고로, Object
타입의 가변인자를 받으면 42.asInstanceOf[AnyRef]
처럼 직접 변환해야 한다.
(7). = 가 없는 함수는 Unit 리턴 타입을 가지며, 프로시저 라 불린다. 값을 리턴하지 않기 때문에, 사이드 이펙트만를 위해 사용한다. 프로시저에서 Unit
을 직접 명시할수도 있다.
def proc(str: String) {
...
... // side effect
...
}
(8). lazy val
을 이용하면, def
처럼 해당 변수가 사용되기 전까지 평가되지 않는다.
lazy val words = scala.io.Source.fromFile("words.txt").mkString
그런데, lazy
로 선언되면 값을 접근할때 마다 스레드세이프하게 초기화가 되었는지 확인하는 검사가 필요하므로 비용이 든다.
(8). 스칼라에는 체크예외가 없다. 모두 런타임 예외다. 그리고, throw
는 Nothing
을 값으로 가지는데, if
문에서는 Nothing
대신 다른 분기의 타입으로 식의 값이 정해진다.
Chapter 3
(1). 고정길이 배열이 필요하면 Array
로 사용하면 된다. 만약 초기값이 필요없으면 new
를 사용하고, 아니면 new
를 제외한다.
val nums = new Array[Int](10)
val strs = Array[String]("Hello", "World")
고정 배열은 JVM 내부에서 자바의 일반 배열로 처리된다.
(2). 가변빌이 배열이 필요하면 ArrayBuffer
를 사용하면 된다. 자바의 ArrayList
라 보면 된다.
val ab = ArrayBuffer[Int]()
ab += 1
ab += (2, 3)
ab ++= Array(4, 5)
ArrayBuffer
끝에 원소를 삽입하고 삭제하는건 성능이 괜찮지만, 중간에 삽입하고 삭제하면 기존의 원소를 옮겨야 하므로 성능이 떨어진다는 점에 주의하자.
만약 Array
를 ArrayBuffer
로 만들려면 toBuffer
메소드를, ArrayBuffer
를 Array
로 만들려면 toArray
를 호출하면 된다.
(3). until
은 RichInt
클래스에 속한다. 만약 0부터 10까지 2씩 건너뛰고 싶으면
0 to (10, 2)
거꾸로 순회하고 싶으면
(0 to 10).reverse
(4). 만약 ArrayBuffer
에 있는 음수 중, 처음 것만 제외하고 모두 삭제하고 싶을때 Flag 를 사용하면 다음과 같은 코드를 만들 수 있다.
val first = true
val n = arr.length
var i = 0
while(i < n) {
if (arr(i) >= 0) i += 1
else {
if (first) { first = false; i += 1}
else {
arr.remove(i); n -= 1
}
}
}
그런데, 배열 버퍼 중간에 있는 원소를 삭제하는건 비효율적이기 때문에, 차라리 인덱스를 보존하고 한꺼번에 옮겨 자르는 편이 더 낫다.
val indexes = for(i until arr.length if arr(i) >= 0 || first) yield {
if (a < 0) first = false;
i
}
for(j until indexes.length) {
arr(j) == arr(indexes(j))
}
arr.trimEnd(arr.length - indexes.length)
(4). for comprehension 의 guard
와 yield
를 사용하든, collection 의 filter
와 map
을 사용하든 하는일은 같다.
(5). ArrayBuffer
나 Array
는 max
, min
, sum
과 같은 메소드 들을 가지고 있다. 그리고 min
, max
혹은 scala.util.Sorting.quickSort
에 들어갈 컬렉션의 원소타입들은 반드시 비교 연산을 가지고 있어야 하는데, 숫자, 문자열, Ordered Trait 을 가지는 타입이 해당된다.
(6). 컬렉션의 원소를 이쁘게 출력하고 싶으면 mkString
을 이용하자.
Array(1, 2, 3).mkString(" and ")
// "1 and 2 and 3"
Array(1, 2, 3).mkString("<", ", ", ">")
// "<1, 2, 3>"
(7) Array
의 toString
은 ArrayBuffer
와는 달리 쓸모가 없다.
scala> Array(1, 2, 3).toString
// res28: String = [I@412d54b3
(8). ScalaDoc 을 여행하다보면 기기묘묘한 것들을 만날 수 있다.
def appendAll(xs: TraversableOnce[A]): Unit
같은 경우, xs
는 TraversableOnce
트레이트를 구현하는 콜렉션이라 보면 된다.. 스칼라의 모든 컬렉션은 TraversableOnce
또는 흔하게 볼 수 있는 컬렉션의 트레이트로 TraversableIterable
이 있다.
def += (elem: A): ArrayBuffer.this.type
의 경우 체이닝이 가능하도록 자기 자신을 리턴하는 메소드다. 이를테면 b += 4 += 5.
처럼
def copyToArray[B >: A] (xs: Array[B]): Unit
의 경우 ArrayBuffer[A]
의 모든 원소를 Array[B]
로 복사하는데 B
는 A
의 하위 타입이다. ArrayBuffer[Int]
, Array[Any]
처럼
(9). 자바처럼 당연히 컬럼이 고정되지 않은 다차원 배열도 만들 수 있다.
val triangle = new Array[Array[Int]](10)
for(i <- 0 to triangle.length)
triangle(i) = new Array[Int](i + 1)
만약 컬럼이 고정된 다차원 배열을 만든다면 ofDim
메소드를 이용하면 된다. 접근하려면 괄호를 두번 사용한다.
val matrix = Array.ofDim[Double](3, 4)
maxtric(0)(1)
(10). 스칼라 배열은 자바 배열로 구현되므로, 당연히 주고 받을 수 있다. java.util.List
를 받거나 리턴하는 자바 메소드를 호출하면 스칼라 코드에서 ArrayList
를 사용할 수 있지만, 이것 대신 scala.collection.JavaConversions
에 속한 메소드들을 임포트하면, 스칼라 버퍼를 자동으로 자바 리스트로 변환할 수 있다.
아래 예제에서 자바의 java.lang.ProcessBuilder
는 List<String>
을 받는 생성자를 가지고 있는데, JavaConversions.bufferAsJavaList
를 임포트하면 스칼라 버퍼가 java.util.List
인터페이스를 구현한 자바 클래스 오브젝트로 감싸진다.
iport scala.collection.JavaConversions.bufferAsJavaList
import scala.collection.mutable.ArrayBuffer
val command = ArrayBuffer("ls", "-al" "/home/user")
val processBuilder(command)
반대로 자바 메소드가 java.util.List
리턴하면 Buffer
로 자동으로 변환할 수 있다. 아래 예제에서 cmd == command
다. 바로 ArrayBuffer
로 받지 않는다는 점에 주의하자. Buffer
만 보장한다.
import scala.collection.JavaConversions.asScalaBuffer
import scala.collection.mutable.ArrayBuffer
sd
val cmd: Buffer[String] = pb.command()
참고로, 스칼라의 컬렉션은 immutable
이 기본이고, 자바의 컬렉션은 mutable
이 기본이다.