2장 first steps in scala - codeport/scala GitHub Wiki

Step 1. Learn to use the Scala interpreter

스칼라에서 제공하는 인터프리터를 이용해 표현식을 써서 evaluate 할 수 있고 결과를 바로 확인할 수 있는 인터렉티브 쉘(shell)을 제공한다. 프롬프트에 scala 명령을 통해 바로 사용할 수 있다. 첫 스텝에서는 단순한 1 + 2와 같은 수식을 통해 인터프리터의 특징을 보자.

scala> 1 + 1 입력하면 인터프리터는 res0: Int = 2 를 출력한다. 인터프리터 출력은 다음 정보를 포함한다.

  • 연산된 결과를 가르키는 이름 : 자동으로 생성되는 resX 이거나 사용자가 직접 작성한 이름이다.
  • 결과 타입 : 콜론과 표현식의 타입이 따라 붙는다.
  • 등호 (=)
  • 표현식을 통해 evaluate된 결과

예제에서 Int는 scala.Int를 의미하고, 자바의 primitive 타입은 scala package의 클래스에 대응된다. 사용자가 작성한 스칼라 코드가 자바 bytecodes로 컴파일할 때 primitive 타입의 성능이점을 얻을 수 있다면 자바의 primitive type를 사용한다. 또한 스칼라 인터프리터가 자동으로 생성된 resX는 인터프리터에서 활용이 가능하다. 예제에서 2를 담고 있는 res0을 이용해 res0 + 100와 같은 표현식을 사용할 수 있다.

Step 2. Define some variables

  • 스칼라는 두가지 종류의 변수를 제공한다.
    • val: 자바의 final 변수와 유사하고 이미 할당된 변수는 재할당 할 수 없다
    • var: 자바의 final과 대조하는 non-final 변수와 유사하고 재할당이 가능하다.

이번 스텝에서는 스칼라의 특징 중 하나인 type inference를 소개하며, 스칼라 인터프리터는 사용자가 표현식 또는 구문을 쓰고 나면 타입을 알아낸다. val msg = "Hello World"와 같이 string literal으로 initialize한 경우, 스칼라는 msg 타입을 String 으로 추론한다. 스칼라 컴파일러 또는 인터프리터가 타입을 추론할 수 있다면 굳이 불필요한 타입을 작성할 필요가 없어진다. 물론, 사용자가 직접 타입을 선언할 수 있는데, 이는 컴파일러에게 의도한 타입으로 추론할 수도 있도록하고 나중에 코드를 볼 때 개발자가 타입을 추론하기에도 편하다.

val msg: String = "Hey you!" 와 같이 타입을 지정할 수 있으며, 변수명에 콜론과 함께 타입을 지정하면 된다. val 이므로 재할당은 되지 않고 할당했을 경우 에러가 발생한다. 재할당을 하고 싶은 경우는 var msg = "hello"와 같이 var를 사용하며, 재할당은 msg = "World"와 같이 할 수 있다. 스칼라 인터프리터는 표현식 작성을 통해서도 결과를 알 수 있지만, println 메서드를 통해서도 결과를 출력할 수 있다. 자바에서 System.out.println 와 유사하다.

Step 3. Define some functions

기본적으로 함수의 선언은 def로 시작하고, 함수명 그리고 파라메터로 구성된다. 파라메터는 괄호로 둘러쌓이며 각 파라메터는 쉼표로 구분된다. 파라메터의 경우는 타입을 추론할 수 없어서 각 파라메터의 타입을 콜론과 함께 지정해야한다. 함수의 body는 일반적으로 0개 또는 하나 이상의 구문을 포함하고 중괄호로 둘러쌓아서 선언한다. 선언한 함수가 결과 타입을 가지는 경우는 등호(=)를 함수의 body와 엮어줄 필요가 있다. 컴파일러 또는 인터프리터는 함수의 파라메터와는 다르게 함수의 결과 타입은 (일부 경우 제외)추론이 가능하다. 사용자는 직접 결과 타입을 def functionName(..): Type = function body 와 같이 파라메터와 함수 body와 엮은 등호(=) 사이에 변수 선언과 같이 타입을 콜론과 함께 지정할 수 있다.

함수 signature

def function name(parameter name: parameter type[, parameter name: parameter type])

함수 body

{

  // assign to val or var / if, for, loop ... / blah blah

}

결과 타입이 없는 경우

signature body

결과 타입이 있는 경우

signature: result type = body

결론
  • 기본적으로 함수 정의는 def로 시작한다.
  • 함수의 파라메터가 있는 경우, 괄호로 둘러쌓아서 선언한다.
  • 함수의 파라메터는 타입추론이 불가능하다.
  • 2개 이상의 파라메터를 가진 경우는 콤마로 구분한다.
  • 재귀(recursive) 함수의 경우는 결과 타입을 반드시 명시해야 한다.
  • Unit타입은 자바의 void 타입과 유사하다.
  • 함수가 한문장이면 중괄호가 제거가능하다.

Step 4. Write some Scala scripts

스칼라는 일련의 문장(statements)을 나열하여 스크립트 파일형태로 구성할 수 있고 이를 순차적으로 실행할 수 있다. scala filename.scala와 같은 형태로 실행할 수 있으며 커맨드라인 인자를 받을 수 있다.

println("Hello World")

형태로 작성할 수 있고, arguments를 활용하고 싶을 때

println("Hello, " + args(0))

와 같이 작성할 수 있다. 자바와 조금 다른 점은 배열을 접근할 때 대괄호(square brackets)를 사용하지 않는다는 점이다.

스크립트 또한 스칼라에서 제공하는 주석을 포함할 수 있고, ///* */를 사용할 수 있다.

Linux/Unix 셀 스크립트처럼 사용하는 방법

#!/bin/sh
exec scala "$0" "$@"
!#
println("Hello, " + args(0))

와 같이 사용할 수 있다. 여기서 $0은 파일명을 의미하고, $@ 커맨드라인 인자 전체를 의미한다. 잊지말고 실행권한을 주도록한다.

윈도우

::#!
@echo off
call scala %0 %*
goto :eof
::!#
println("Hello, " + args(0))

와 같이 사용하면 된다.

Step 5. Loop with while; decide with if

자바에서 제공하는 것 처럼 스칼라 또한 while, if 등을 제공한다.

var i = 0
while (i < 10) {
  println("*" * i)
  i += 1
}

자바와 별 차이를 느끼지 못할 정도로 유사하다. 스칼라에서 추구하는 스타일(?)은 아니지만 단순 예제를 표현합니다.

자바와 다른 점은

  • i++ 또는 ++i 형태는 사용할 수 없다. i+=1 또는 i=i+1 등으로 대체해서 사용해야한다.
  • 세미콜론은 옵션이다.
var i = 0
while (i < 10) {
  if (i % 2 == 1)
    println("*" * i)
  i += 1
}

와 같이 자바와 유사하게 if 문을 사용할 수 있습니다.

특징으로는

  • 자바와 같이 boolean 표현식을 ()로 둘러싼다.
  • 괄호를 생략할 수는 없다.
  • 한 문장으로 작성할 수 있다면 {}를 생략할 수 있다.

Step 6. Iterate with foreach and for

imperative programming을 설명하기엔 좀 버겁지만, 보통 자바, C++C에서 보통 사용하는 스타일이고, declative/functional programming과는 대조적인 방법이다. 스칼라 또한 imperative style하게 프로그램을 작성할 수 있지만 스칼라를 배우는 목표와는 멀어진다. imperative programming에대해서는 아는 것도 시간도 부족해서 생략합니다.

스칼라에서는 매우 간단하게 인자로 high-order function으로 넘길 수 있도록 anonymous function(function literal)을 제공한다. (물론 그 용도만 있는 것은 아니다.) 자바에서는 없는 개념이고 anonymous class는 있다. 8버전 부터 anonymous function(lambda)를 제공한다. (스칼라의 anonymous function은 자바의 anonymous class를 이용한 방법이다.)

function literal는 (parameter name: parameter type[, parameter name: parameter type]) => function body 으로 선언할 수 있다. 물론 인자가 없는 경우는 () => function body 으로 선언도 가능하다.

args.foreach(arg => println(arg))

와 같이 타입 추론이 가능한 경우에는 타입을 생략할 수 있다. 하나의 파라메터를 가지는 경우엔 괄호를 생략할 수 있지만 두개 이상의 파라메터를 가진 function literal을 원하는 경우에는 괄호를 생략할 수 없다. 예를들어 (x, y) => println(x, y) 와 같이 괄호를 사용해야한다. args.foreach의 경우에 function literal이 한개의 인자를 가진 하나의 문(statement)일 때는 이름과 인자 등을 args.foreach(println)와 같이 생략할 수 있다.

스칼라에서 for를 제공하고, for (arg <- args) { body } 처럼 사용할 수 있다. 이번장에서 설명하는 for는 굉장히 단순한 구조이다. 이 구문을 분석하면,

  • for로 시작하며, 괄호로 둘러쌓인 left <- right 형태로 구성된다.
  • right는 예제에서 설명하는 args 배열과 유사한 것이 들어간다.
  • leftval 변수 이름이 들어간다. (val 변수라고 하기엔 좀 뭐하지만...)

겉보기에는 각 이터레이션이 돌면서 left가 바뀌니 var로 판단될 수는 있겠으나 실제로는 그렇지 않다. 당연히 body에서 leftvar 변수가 아니므로 재할당이 불가능하다. body scope에서 사용될 수 있는 val 변수가 매 이터레이션에서 생성되고 body가 실행되는 구조이다.

질문

  • REPL말고 코드에서도 재정의는 가능한거 아닌가요?

    In the interpreter, however, you can define a new val with a name that was already used before. - p71

drypot님의 한글 번역

현재는 접속되지 않는 링크.