스칼라 문법 요약(Scala Cheatsheets)
간단하게 스칼라 기본 문법에 대해 알아보고자 합니다. 이 포스팅은 CC-BY-SA 3.0 License를 준수합니다. 원문 링크는 http://docs.scala-lang.org/cheatsheets 입니다. 작성자는 Brendan T. O'Connor씨에요. 이 포스팅 문서는 원문 링크의 내용 번역 및 자의적 분석도 포함합니다. 번역이나 분석의 오류가 발견되면 댓글로 남겨주세요. 그럼 시작합니다. :3
변수(Variables)
var x = 5 val x = 5 // Good, constant x = 6 // Bad var x: Double = 5 // explicit type
처음 알아볼건 역시 변수입니다. 기본적인 변수 선언 방식이 있네요. 그중 특이한 것은 스칼라에서 세미콜론(;)이 안보인다는 것입니다.(안쓴다는게 아니에요.) 그래서 위와 같은 코드로도 변수가 만들어지네요. 그리고 변수를 만들때는 항상 키워드를 붙여줘야 합니다. x = 6 처럼 선언하면 나쁜 것입니다. 그리고 val 키워드는 정적 변수(constant)를 만드는 키워드네요. 또한 해당 변수의 명확한 타입(explicit type) 지정은 콜론(:) 이후 타입명을 써주면 됩니다.
함수(Functions)
스칼라의 함수 선언에 대해 알아봅시다.
함수 선언
def f(x: Int) = { x*x } // Good def f(x: Int) { x*x } // Bad // hidden error: widthout = it's a Unit-returning procedure; causes havoc
함수를 선언하는 부분입니다. 그런데 이 코드를 보면 자바에 익숙해 있던 방식이 스칼라에선 나쁜 방식입니다. 만약에라도 이퀄(=) 기호가 빠진 def 선언이라면 해당 def는 Unit을 반환합니다. Unit은 자바등에서의 void와 유사하다 보면 됩니다.
함수 선언2
def f(x: Any) = println(x) // Good def f(x) = println(x) // Bad
함수를 선언하는 다른 방식입니다. 어떠한 값이라도 관계 없다면 Any라는 타입을 명시해야 합니다.
별칭 선언
type R = Double
타입의 별칭(alias)을 만듭니다.
호출 타입 지정
def f(x: R) // call-by-value def f(x: => R) // call-by-name(lazy parameters)
첫줄에 있는 코드는 x라는 변수를 값에 의한 호출(call-by-value)을 합니다. 두번째 줄은 이름에 의한 호출(call-by-name)을 합니다. 두 방식의 차이는 별도로 포스팅 해야겠네요.
익명 함수
(x:R) => x*x // anonymous function ((x:Int) => println(x*x))(3) // 9
익명 함수를 선언하는 방식입니다. 두번째 줄은 임의로 만들어본 익명 함수고 해당 함수를 실행하면 9가 출력됩니다. :3
익명 함수2
(1 to 5).map(_*2) (1 to 5).reduceLeft( _+_ ) println((1 to 5).map(_*2)) // Vector(2, 4, 6, 8, 10) println((1 to 5).reduceLeft( _+_ )) // 15
익명 함수를 선언하는 다른 방식입니다. 언더바(_)는 위치적으로 매칭된 아규먼트(arg) 값입니다.
익명 함수3
(1 to 5).map( x => x*x ) println((1 to 5).map( x => x*x )) // Vector(1, 4, 9, 16, 25)
익명 함수를 사용해 입력된 값을 두배로 만드는 방식입니다.
익명 함수4
(1 to 5).map(2*) // Good (1 to 5).map(*2) // Bad println((1 to 5).map(2*)) // Vector(2, 4, 6, 8, 10)
입력된 값을 2배로 만드는 익명 함수 입니다. 2* 이라는 수식은 2*_ 가 축약된것입니다. 하지만 반대로 하면 오류가 납니다. *2 는 _*2 가 되지 않습니다.
익명함수 5
(1 to 5).map { val x=_*2; println(x); x } // anonymous function: block style returns last expression.
문법 오류가 나는데, 맞는 것인가요? 일단 원본 문서에는 있는 내용이라 적어두긴 합니다만. 혹시 오류나는거 이유 아시는분 댓글 좀 부탁드립니다. TT
파이프 스타일
(1 to 5) filter {_%2 == 0} map {_*2} // anonymous functions: pipeline style. (or parens too). println((1 to 5) filter {_%2 == 0} map {_*2}) // Vector(4, 8)
리눅스에서 많이 사용하는 파이프 스타일입니다. 결과 셋(set)을 다음으로 넘겨서 계속 처리하는 방식이네요.
multiple blocks
def compose(g:R=>R, h:R=>R) = (x:R) => g(h(x)) val f = compose({_*2}, {_-1}) // anonymous functions: to pass in multiple blocks, need outer parens. // my test type R = Double def compose(g:R=>R, h:R=>R) = (x:R) => g(h(x)) val f = compose({_*2}, {_-1}) println(f(3)) // 4.0
... 여기부터는 머리가 아파옵니다. 어쨌든 하나씩 분석을 해보자면 f(3)을 호출하면 이건 x:R이라는 것으로 인해 x값이 3이 됩니다. 그리고 g(h(3))을 실행하게 되죠. 그런데 h(3)는 _-1 구문에 의해 x의 값이 2.0이 됩니다. 그리고 g(2.0)이 되면서 g의 구문인 _*2로 인해 결과가 4.0이 나오는 것이죠. -_-; 힘들군요.
Currying
val zscore = (mean:R, sd:R) => (x:R) => (x-mean)/sd // my test type R = Double val zscore = (mean:R, sd:R) => (x:R) => (x-mean)/sd println((zscore(3.0,2.0))(2)) // -0.5
결론부터 말하자면 (2 - 3.0)/2.0 입니다. 그래서 -0.5에요. 하하하. (멘붕)
Currying 2
def zscore(mean:R, sd:R) = (x:R) => (x-mean)/sd // my test type R = Double def zscore = (mean:R, sd:R) => (x:R) => (x-mean)/sd println((zscore(3.0,2.0))(2)) // -0.5
val로 선언한 것과 동일한 결과를 보여주네요.
Currying 3
def zscore(mean:R, sd:R)(x:R) = (x-mean)/sd // my test type R = Double def zscore(mean:R, sd:R)(x:R) = (x-mean)/sd println((zscore(3.0,2.0))(2)) // -0.5
이것도 Currying의 한가지 방식입니다.
Currying 3+
def zscore(mean:R, sd:R)(x:R) = (x-mean)/sd // my test type R = Double val normer = zscore(3.0,2.0)_ println(normer(2)) // -0.5
Currying 3에서 보여준 방식으로 코딩했다면 위처럼도 선언이 가능합니다. Currying 및 Currying2 방식은 되지 않아요.
generic type
def mapmake[T](g:T=>T)(seq: List[T]) = seq.map(g) // my test def mapmake[T](g:T=>T)(seq: List[T]) = seq.map(g) println(mapmake(List(5,10,15,20))(List(0,1))) // List(5, 10) println(mapmake(List(5,10,15,20))(List(2))) // List(15)
특정 List에서 인덱스 값을 List로 구성해서 넣어주면 map 함수를 이용해 결과 List를 반환합니다. 자세한건 my test 부분을 참조해 주세요. (멘붕 of 멘붕)
infix sugar
5.+(3); 5 + 3 // 8 (1 to 5) map (_*2) // Vector(2, 4, 6, 8, 10)
5. 은 실수지만 5+3처럼 해석되어 8이라는 결과가 됩니다.
패키지(packages)
패키지를 임포트 하는 방식을 다룹니다.
와일드카드(wildcard)
import scala.collection._
자바에서 import java.util.* 하는것과 유사한 동작을 합니다.
선택적인(selective)
import scala.collection.Vector import scala.collection.{Vector, Sequence}
하나씩 개별로도 import 할수 있고, array처럼 import도 가능합니다.
패키지명 재정의(renaming)
import scala.collection.{Vector => Vec28}
심지어 import 하려는 패키지 이름도 바꿀 수 있군요. ::ㅇㅅㅇ::
자바(java)
import java.util.{Date => _, _}
자바에 있는 패키지도 사용 가능합니다. 해당 구문은 Date 관련 패키지를 제외한 java.util 패키지를 전부 import 합니다.
패키지 선언(declare)
package pkg at start of file package pkg { ... }
패키지를 선언하는 방법입니다.
데이터 구조(data structures)
스칼라도 기본적으로 제공하는 데이터 셋이 있습니다. 하나씩 알아봅시다.
튜플 리터럴(tuple literal)
(1,2,3)
일반적인 튜플 셋입니다.
리터럴 바인딩(destructuring bind)
var (x,y,z) = (1,2,3) // Good , x = 1, y = 2, z = 3 var x,y,z = (1,2,3) // Bad
한 줄로 여러개의 변수를 바인딩 할 수 있습니다. 다만 밑줄처럼 바인딩하는 것은 오류에요.
불변하는 리스트(list - immutable)
var xs = List(1,2,3)
List를 정의합니다.
리스트 요소 선택(paren indexing)
xs(2)
List에서 index번째 요소를 선택합니다.
연결(cons)
1 :: List(2,3) // List(1, 2, 3)
리스트에 요소를 연결합니다.
범위 지정(range)
1 to 5 same as 1 until 6 1 to 10 by 2 // my test println((1 to 5)) // Range(1, 2, 3, 4, 5) println((1 until 6)) // Range(1, 2, 3, 4, 5) println((1 to 10 by 2)) // Range(1, 3, 5, 7, 9)
범위를 지정하는 다양한 방식입니다.
비어있는(empty parens)
() (empty parens) // sole member of the Unit type (like C/Java void).
C/Java에서 void와 유사한 Unit Type의 솔로 멤버입니다.
제어 관련(control constructs)
이제부터 조건문 및 프로그램의 로직을 제어할 수 있는 구문에 대해 알아봅시다.
if문
if (check) happy else sad
일반적인 if 구문입니다.
else 여부
if (check) happy same as if (check) happy else ()
만약 else 구문이 없더라도 이것은 단순히 생략된 것으로 판단합니다.
while문
while (x < 5) { println(x); x += 1}
일반적인 while문입니다.
do while문
do { println(x); x += 1} while (x < 5)
일반적인 do while문입니다. while문과 다르게 한번은 body의 내용을 실행합니다.
break
import scala.util.control.Breaks._ breakable { for (x <- xs) { if (Math.random < 0.1) break } }
만약 랜덤한 값이 0.1보다 미만이면 for문을 빠져 나옵니다. xs 대신 (1 to 100)등을 넣고 테스트 해보면 됩니다.
filter/map
for (x <- xs if x%2 == 0) yield x*10 same as xs.filter(_%2 == 0).map(_*10)
두개 구문은 동일한 효과를 가집니다. 입력 받은 데이터셋에서 개별 요소의 값을 2로 나눈 나머지가 0이면 그 값을 10배 곱해서 데이터셋을 반환합니다.
멀티 바인딩(destructuring bind)
for ((x,y) <- xs zip ys) yield x*y same as (xs zip ys) map { case (x,y) => x*y } // my test println((List(1,2) zip List(3,4)) map { case (x,y) => x*y }) // List(3, 8)
zip은 두 리스트의 요소를 매핑해 한쌍으로 만들어서 반환 합니다. 즉 List(1,2).zip(List(3,4))은 List((1,3), (2,4))을 반환하는 것이죠. 그리고 map 함수를 통해 쌍인 경우 두 값을 곱합니다. 그래서 my test의 결과가 위처럼 나온거에요.
cross product
for (x <- xs; y <- ys) yield x*y same as xs flatMap {x => ys map {y => x*y}} // my test println(List(1,2) flatMap {x => List(3,5) map {y => x*y}}) // List(3, 5, 6, 10)
위의 두 구문은 같은 효과를 가집니다. flatMap은 추후에 포스팅할 필요가 있을 것 같네요.
imperative-ish
for (x <- xs; y <- ys) { println("%d/%d = %.1f".format(x,y, x*y)) }
C에서 printf하고 비슷한 느낌이군요.
순회(iterate including the upper bound)
for (i <- 1 to 5) { println(i) }
차근차근 1부터 5까지 순회합니다.
순회2(iterate including the upper bound)
for (i <- 1 until 5) { println(i) }
이건 4까지만 순회합니다.
패턴 매칭(pattern matching)
이제 패턴 매칭에 대해 알아봅시다.
패턴1
// use case in function args for pattern matching. (xs zip ys) map { case (x,y) => x*y } // GOOD (xs zip ys) map( (x,y) => x*y ) // BAD
case 빼먹지 말라는 얘기에요.
나쁜 패턴
// “v42” is interpreted as a name matching any Int value, and “42” is printed. val v42 = 42 Some(3) match { case Some(v42) => println("42") case _ => println("Not 42") } // BAD
42라는 값을 매칭 시키고 싶었는데 엉뚱하게 Int형이면 OK하는 case문에 들어가네요. 그래서 3을 넣어도 42라는 값이 출력됩니다.
좋은 패턴
// ”<span class="command">v42</span>” with backticks is interpreted as the existing val v42, and “Not 42” is printed. val v42 = 42 Some(3) match { case Some(<span class="command">v42</span>) => println("42") case _ => println("Not 42") } // GOOD
나쁜 패턴을 원했던 의도대로 변경해 봅시다. v42를 표현할때 쿼테이션(')이 아니라 그레이브(`)입니다. 이제 42라는 값을 가진 것만 42라고 출력합니다.
패턴2
val UppercaseVal = 42 Some(3) match { case Some(UppercaseVal) => println("42") case _ => println("Not 42") } // UppercaseVal is treated as an existing val, rather than a new pattern variable, because it starts with an uppercase letter. Thus, the value contained within UppercaseVal is checked against 3, and “Not 42” is printed.
아까 나쁜 패턴에서 봤던 유사한 코드지만 다른 것은 비교할 변수명의 첫 글자가 대문자라는 겁니다. 대문자로 하면 위에서 봤던 좋은 패턴과 같은 방식으로 동작합니다.
객체 지향(object orientation)
스칼라에서도 객체 지향적인 문법을 지원합니다.
생성자 파라미터(private)
class C(x: R) same as class C(private val x: R) var c = new C(4)
기본적으로 private 키워드는 생략이 가능합니다.
생성자 파라미터(public)
class C(val x: R) var c = new C(4) c.x
val 키워드를 넣으면 클래스의 멤버 변수를 직접 액세스 가능합니다.
생성자로 클래스 구현
class C(var x: R) { // constructor is class body assert(x > 0, "positive please") var y = x // declare a public member val readonly = 5 // declare a gettable but not settable member private var secret = 1 // declare a private member def this() = this(42) // alternative constructor }
클래스의 몸체(Body)를 구현하는 방법입니다. public, private 멤버 변수뿐만 아니라 자바의 final과 유사한 val 변수를 이용할 수도 있네요. 마지막줄은 다른 생성자를 정의 합니다. 원문은 def this 였지만 문법 오류가 나서 def this()로 바꿨습니다.
익명 클래스(anonymous class)
new{ ... }
익명 클래스를 만드는 방법입니다.
추상 클래스(abstract class)
abstract class D { ... }
추상 클래스를 선언합니다. 생성하지는 않습니다.(non-createable)
클래스 상속(inherited class)
class C extends D { ... }
클래스를 상속합니다. 자바와 같네요.
상속과 생성자
class D(var x: R) class C(x: R) extends D(x) // inheritance and constructor params. (wishlist: automatically pass-up params by default)
상속 클래스를 만들때 부모 클래스의 생성자를 넘겨줍니다. 자바에서 super(variable...) 메소드를 보는 것 같네요.
싱글튼 객체 선언(define a singleton)
object O extends D { ... }
스칼라에서는 object 키워드를 선언하면 해당 객체는 싱글튼 객체가 됩니다.
트레잇(traits)
trait T { ... } // traits. class C extends T { ... } // interfaces-with-implementation. no constructor params. class C extends D with T { ... }
자바의 interface와 유사하긴 하지만 특성은 다릅니다. 트레잇은 섞어서 사용이 가능하다고 되어 있습니다. 자세한 것은 추후 별도 포스팅으로 다루겠습니다.
다중 트레잇(multiple traits)
trait T1; trait T2 class C extends T1 with T2 class C extends D with T1 with T2
트레잇을 여러개를 섞어서 사용하는 방법입니다.
메소드 오버라이드(method overrides)
class C extends D { override def f = ...}
메소드 오버라이드를 하는 방법입니다.
객체 생성(create object)
new java.io.File("f")
객체 생성을 하는 방법입니다.
etc.
new List[Int] // [BAD] type error: abstract type List(1,2,3) // [GOOD] instead, convention: callable factory shadowing the type
List 초기화 방법인것 같네요. -_-(무책임)
클래스 리터럴(class literal)
classOf[String]
이런게 있어요. (마지막쯤 되니 이젠 막나감...)
타입 체크(type check (runtime))
x.isInstanceOf[String]
실행 중 타입 체크를 하는 방법입니다.
타입 캐스팅(type cast (runtime))
x.asInstanceOf[String]
실행 중 타입을 캐스팅하는 방법입니다.
명확한 타입
x: String // ascription (compile time) // my test var x = 1 : Double // 1.0
컴파일 중 명확하게 타입을 정의합니다.
마무리
생각보다 양이 많아서 ... (그것보단 Copy CV가 더 많음.) 그래도 스칼라의 기본적인 문법에 대해 빠르게 훑어볼 수 있는 계기가 되었습니다. :3