The Absolute Scala Basics

Scala에서의 변수 선언 및 주요 기본 문법에 대해 정리합니다.

1. Values, Variables and Types


Scala에는 두 종류의 변수 val과 var이 있음. val의 자바의 final 변수와 비슷하다. 일단 초기화하고 나면 val 변수에 대해 값을 다시 할당 할 수 없다. 반면 var은 자바의 final이 아닌 일반 변수와 비슷하다. var 변수는 없어질 때까지 계속 값을 재할당 할 수 있다.

val x: Int = 2 // immutable -> can't be reassigned
// x = 2 (x)

var y: Int = 1 // mutable   -> can be reassigned
y = 1
y += 1

Scala에서는 var 변수보단 val 변수를 선호한다. 그 이유는 함수형 프로그래밍에서 side effect를 최소화 하기 위해서 임.

Takeaway


  • vars 보단 vals 선호
  • 모든 val과 var은 타입을 가짐
  • 타입을 생략 할 경우 컴파일러가 자동으로 타입 추론

2. Expressions


Basic Expressions: operators

val x = 3 + 5 // EXPRESSION
val xIsEven = x % 2 == 0
val xIs Odd = !xIsEven

If Expression

val cond: Boolean = ...
val i = if (cond) 42 else 0

스칼라에서 코드 블럭은 expression 이다.

val x = {
    val cond: Boolean = ...
    if (cond) 42 else 0
}
  • 블록의 값은 마지막 expression의 값이다.

Expressions vs. instructions

  • instructions are executed(think Java)
  • expressions are evaluated(Scala)

3. Functions


scala에서는 함수를 다음과 같이 정의한다

def max(x: Int, y: Int): Int = {
    if (x > y)
        x
    else
        y
}

함수 정의는 ‘def’로 시작한다. 그 다음으로 함수 이름이 오고, 그 뒤에 괄호 안에 콤마(,)로 구분한 파라메터 목록이 온다. 컴파일러가 파라미터 타입을 추론하지 않기 때문에 모든 파라미터에는 타입 지정을 반드시 덧붙여야 한다. 다음으로 파라미터 목록을 닫는 괄호 뒤에는 리턴 타입을 정의한다. 그 뒤로 중괄호 안에는 함수의 본문이 들어간다.

위 max() 경우 결과 타입을 생략해도 컴파일러가 타입을 추론하지만, 때로 컴파일러가 함수의 결과 타입을 지정하도록 요구하는 경우가 있는데 함수가 재귀적이라면 함수의 결과 타입을 반드시 명시해야 한다.

Recursion Function - Fibonacci function

def fibonacci(n: Int): Int = {
    if (n <= 2) 1
    else fibonacci(n-1) + fibonacci(n-2)
}

스칼라는 함수를 정의하고 그 내부에 함수를 정의 할 수 있다. 아래 코드는 중첩된 함수를 정의한다.

Nested Function - Prime check function

def isPrime(n: Int): Boolean = {
    def isPrimeUntil(t: Int): Boolean = {
        if (t <= 1) true
        else n % t != 0 && isPrimeUntil(t-1)
    }

    isPrimeUntil(n / 2)
}

4. Type inference


스칼라는 프로그래머가 특정 타입을 생략할 수 있도록 해주는 빌트인 타입 추론 기능을 갖추고 있다. 컴파일러가 변수의 초기화 표현식으로 부터 타입을 추론 할 수 있기 때문에 변수의 타입을 지정할 필요가 없을 때가 많다. 또한 메소드의 리턴 타입은 본문의 타입과 일치하기 때문에 리턴 타입 또한 컴파일러가 추론 할 수 있다.

val message = "Hello, world!" // 초기화 값이 String이기 때문에 message 변수는 String 타입
val x = 2       // 초기화 값이 2이기 때문에 변수 x는 Int 타입
val y = x + "items"     // Int + String = String. 변수 y는 String 타입

def succ(x: Int) = x + 1 // 함수 본문의 Expression은 Int 타입이기 때문에 succ 함수 리턴 타입은 Int 타입
// 동일한 함수 정의
def succ(x: Int): Int = x + 1

// 다음과 같이 사용하면 에러 발생
val x: Int = "Hello, world!"    // 변수 x는 Int형으로 명시되어 있기 때문에 우변의 초기값인 String을 입력할 경우 에러

앞서 함수의 결과 타입을 생략해도 컴파일러가 타입을 추론하지만, 때로 컴파일러가 함수의 결과 타입을 지정하도록 요구하는 경우가 있다고 설명하였다. 하지만 함수가 재귀적이라면 함수의 결과 타입을 반드시 명시해야 한다.

5. Stack & Tail Recursion


def factorial(n: Int): Int = {
    if (n <= 1) 1
    else {
        println("Computing factorial of " + n + " - I first need factorial of " + (n-1))
        val result = n * factorial(n-1)
        println("Computed factorial of " + n)

        result
    }
}

factorial(5000) // Stack Overflow Error Occured!!

위 코드는 Factorial을 재귀적으로 계산하는 코드로 작은 수에 대해서는 정상 동작하지만 어느 시점에서는 결국 재귀적 함수 호출로 인해 스택 사용이 중첩되어 스택 오버플로 에러가 발생한다.

이렇게 재귀 함수의 콜 스택이 깊어질수록 메모리 오버헤드가 발생하는 문제를 해결하기 위한 재귀 호출 방식을 Tail Recursion(꼬리 재귀) 라고 한다.

재귀 함수의 실행 결과가 연산에 사용되지 않고 바로 반환되게 함으로써 이전 함수의 상태를 유지 할 필요가 없기 때문에 스택을 사용하지 않으므로 메모리 오버헤드가 발생하지 않는다.

아래는 꼬리 재귀로 구현한 Factorial 함수이다.

def anotherFactorial(n: Int): Int = {
    @tailrec
    def factHelper(x: Int, accumulator: Int): Int = {
        if( x <= 1) accumulator
        else factHelper(x - 1, x * accumulator) // TAIL RECURSION
    }

    factHelper(n, 1)
}
/*
  anotherFactorial(10) = factoHelper(10, 1)
  = factHelper(9, 10 * 1)
  = factHelper(8, 9 * 10 * 1)
  = factHelper(7, 8 * 9 * 10 * 1)
  = ...
  = factHelper(2, 3 $ 4 * ... * 10 * 1)
  = factHelper(1, 1 * 2 * 3 * 4 * ... * 10)
  = 1 * 2 * 3 * 4 * ... * 10
*/

6. Call-by-Name and Call-by-Value


Scala에서는 다음과 같은 두 가지의 함수 호출 방식이 있음.

  • Call By Value(CBV) : 값에 의한 호출
  • Call By Name(CBN) : 이름에 의한 호출
def calledByValue(x: Long): Unit = {
    println("CBV by value: " + x)
    println("CBV by value: " + x)
}

def calledByName(x: Long): Unit = {
    println("CBN by value: " + x)
    println("CBN by value: " + x)
}

calledByValue(System.nanoTime())
calledByName(System.nanoTime())

/*
CBV by value: 2892182467908626
CBV by value: 2892182467908626
CBN by value: 2892201562799589
CBN by value: 2892218927551838
*/

CBV의 경우는 함수가 호출되기 전에 파라미터의 값이 미리 정해지지만, CBN의 경우는 파라미터를 참조할 때마다 전달된 표현식이 평가되고 실행된다는 점이 다름.

아래의 코드는 CBN, CBV 파라메터를 입력받는 함수 호출에 대한 다른 예이다.

def infinite(): Int = 1 + infinite()
def printFirst(x: Int, y: => Int) = println(x)

// printFirst(infinite(), 34) <-- Error
printFirst(34, infinite())

첫 번째의 printFirst 호출의 경우 첫번째 인자가 Int 타입으로 infinite() 함수의 리턴 타입이 Int이어서 컴파일을 성공하지만 CBV 이기 때문에 먼저 infinite 함수의 호출 계산이 이뤄지는데 이때 무한재귀 호출로 에러가 발생한다.

두 번째 printFirst 호출의 경우는 두 번째 인자에 CBN으로 인자를 받고 있다. 하지만 함수 본문에서는 해당 인자의 참조를 하고 있지 않기 때문에 정상적으로 첫번째 인자의 값을 출력하고 정상 종료한다.

Takeaways


  • Call by value

    • value is computed before call
    • same value used everywhere
  • Call by name

    • expression is passed literally
    • expression is evaluated at every use within

7. Default and Named Arguments


scala에서 파라미터의 디폴트값을 지정할 수 있다. 디폴트 값을 지정한 파라미터가 있다면, 함수 호출시 해당 인자를 생략 할 수 있다. 생략한 인자는 디폴트값으로 채워진다.

  • Before

    def trFact(n: Int, acc: Int): Int = {
        if (n <= 1) acc
        else trFact(n-1, n*acc)
    }
    
    val fact10 = trFact(10, 1) // <- 1 최초 값 설정.
    
  • After

    def trFact(n: Int, acc: Int = 1): Int = { // <- acc 파라메터 디폴트값 1 설정
        if (n <= 1) acc
        else trFact(n-1, n*acc)
    }
    
    val fact10 = trFact(10)
    

디폴트 인자는 이름을 붙인 인자와 조합하면 유용하다.

def savePicture(format: String = "jpg", width: Int = 1920, height: Int = 1080): Unit = println("saving picture")

savePicture() // all parameter value is default 

savePicture(height = 600, format = "bmp")
/*
    1. pass in every leading argument
    2. name the arguments
*/

위 코드를 보면 savePicture()라고 호출하면 세 파라미터가 모두 디폴트값이 된다. 이름 붙인 인자를 이용해 호출하면, 이름 붙이지 않은 인자에 대해 디폴트값으로 유지한 채 어떤 값이라도 명시 할 수 있다.

Takeaways


When 99% of time we call a function with the same params:

def factorial(x: Int, acc: Int = 1): Int = {
    ...
}

val fact10 = factorial(10)

Naming parameters

def greet(name: String = "Superman", age: Int = 10): String = 
s"Hi, I'm $name and I'm $age years old."

greet(age = 5)

8. Smart Operations on Strings


scala에서 문자열 처리 방법과 Scala 2.10에 추가된 String interpolation 기능을 알아본다.

문자열 처리

val str: String = "hello, I am learning Scala"

println(str.charAt(2))          // 1
println(str.substring(7, 11))   // I am
println(str.split(" ").toList)  // List(Hello, I, am, learning, Scala)
println(str.startWith("Hello")) // true
println(str.replace(" ", "-"))  // "hello,-I-am-learning-Scala"
println(str.toLowerCase())      // "hello, i am learning scala"
println(str.length)             // 26

val aNumberString = "2"
val aNumber = aNumberString.toInt
println('a' +: aNumberString :+ 'z')   // a2z
println(str.reverse)                   // alacS gninrael ma I ,olleH
println(str.take(2))                   // He

String interpolation

  • 데이터를 기반으로 문자열을 더 쉽게 만들수 있는 기능
  • 즉, 문자열을 출력하거나 선언할 때 중간에 다른 변수를 끼워넣는 구문
  • String interpolation은 s, f, raw 세가지의 방식을 제공한다.

아래는interpolation방식 3가지의 예 이다.

// Scala-specific: String interpolators.

// S-interpolators
val name = "David"
val age = 12
val greeting = s"Hello, my name is $name and I amd $age years old"
val anotherGreeting = s"Hello, my name is $name and I will be turning ${age + 1} years old."

println(anotherGreeting)    // Hello, my name is David and I will be turning 13 years old.

// F-interpolators
val speed = 1.2f
val myth = f"$name can eat $speed%2.2f burgers per minute"
println(myth)           // David can eat 1.20 burgers per minute

// raw-interpolator
pritln(raw"This is a \n newline") // This is a \n newline
val escaped = "This is a \n newline"
println(raw"$escaped")      // This is a
                            //  newline

String interpoators: F

  • For formatted strings, similar to printf:
    val speed = 1.2f
    val myth = f"$name can eat $speed%2.2f burgers per minute"
    println(myth)           // David can eat 1.20 burgers per minute
    
  • Can check for type correctness
    val x = 1.1f       // value is Float
    val str = f"$x%3d" // format requires Int --> Compiler error!!!