Post

[PL] 프로그래밍 언어(1강) - Basic Introduction of Scala

함수형 프로그래밍을 이해하고, Scala 언어의 기본 문법을 정리합니다.

[PL] 프로그래밍 언어(1강) - Basic Introduction of Scala
  • 수식이 제대로 보이지 않는다면, 새로고침(F5)을 해주시기 바랍니다.
  • 고려대학교 박지혁 교수님의 ‘프로그래밍언어(COSE212)’를 기반으로 했습니다.

Functional Programming(FP)

Scala 언어는 함수형 프로그래밍(Functional Programming, FP)을 실현하기에 매우 유용한 언어입니다. 함수형 프로그래밍이란 대입문을 사용하지 않고, 문제를 해결하기 위한 코드를 함수로 작성하는 프로그래밍 방식을 말합니다. 이러한 방식을 사용하면 예기치 못한 문제(부수 효과, Side Effects)를 줄이고 가독성 높은 코드를 작성하는 데 도움이 됩니다.

Side Effects에는 변수의 값 변경, 예외 및 오류 발생으로 인한 실행 중단, 객체의 필드값 변경 등이 있으며, 이를 제거한 함수를 Pure Function이라고 합니다.

이후 작성할 Scala 코드는 모두 FP 스타일을 기반으로 할 예정입니다.
이제부터 Scala의 기본 자료형을 살펴보겠습니다.


Basic Data Types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scala> 42  
val res0: Int = 42

scala> 3.7  
val res1: Double = 3.7

scala> true  
val res2: Boolean = true

scala> 'c'  
val res3: Char = c

scala> "abc"  
val res4: String = abc


### Operations
scala> "abc".length  
val res5: Int = 3

scala> println(println("HI"))  
HI  
()  //Unit

Immutable Variables

val (variable name): (variable type) = initial value의 형태로 사용합니다. 값을 변경할 수 없습니다. (immutable)

1
2
val x: Int = 1
x + 2
1
2
val y = 1
val c = true

타입을 직접 지정하지 않아도 Type Inference(타입 추론)이 적용되어 자료형이 자동으로 결정됩니다.

Methods

def add(x: Int, y: Int) : Int = x + y의 형태로 사용할 수 있습니다.
add 부분은 method name이 들어가는 곳입니다. x, y 부분은 parameter name, 괄호 안 Int 부분은 parameter type 부분이다. 콜론 뒤에 있는 Int 부분에는 함수의 return type이 들어갑니다. 마지막으로 method body가 = 뒤에 들어갑니다.

Examples

1
2
def add(x: Int, y: Int): Int = x + y
add(1, 2)

Conditionals

if (x < 0) -x else x의 형태로 사용할 수 있습니다. abs 함수를 예시로 만들어보겠습니다.

1
2
3
4
def abs(x: Int): Int = if (x < 0) -x else x

abs(12)
abs(-5)

Recursions

Scala에서는 함수형 프로그래밍을 지향하며, 재귀적 프로그래밍이 매우 유용하게 사용됩니다.

1
2
3
4
5
6
7
// A program that calculates sum of integers from 1 to n
def sum(n: Int): Int =
  if (n < 1) 0
  else sum(n - 1) + n

sum(10)
sum(100)

case class

case class는 product type을 선언하는 방식으로, tuple과 유사하게 사용할 수 있습니다. case class Point(x: Int, y: Int, color: String)의 형식으로 선언할 수 있습니다.

scala> case class Point(x: Int, y: Int, color: String)
// defined case class Point

scala> val p: Point = Point(3, 4, “RED”)
val p: Point = Point(3,4,RED)

enums

Algebraic Data Types(ADTs)의 하나로, enum을 통해 자료형을 정의할 수 있습니다.

scala> enum Tree {
| case Leaf(value: Int)
| case Branch(left: Tree, value: Int, right: Tree)}
// defined class Tree

scala> import Tree.* //이 부분이 있어야 Tree 내부의 constructor들을 불러와 직접 사용 가능합니다.

scala> val tree1: Tree = Leaf(1)
val tree1: Tree = Leaf(1)

scala> val tree2: Tree = Branch(Leaf(1), 2, Leaf(3))
val tree2: Tree = Branch(Leaf(1),2,Leaf(3))

Pattern Matching

ADTs에 pattern match를 하는 것이 가능합니다.

scala> enum Tree {
| case Leaf(value: Int)
| case Branch(left: Tree, value: Int, right: Tree)
| }
// defined class Tree

scala> import Tree.*

scala> def sum(t: Tree): Int = t match {
| case Leaf(n) => n
| case Branch(l, n, r) => sum(l) + n + sum(r)
| }

1
sum(Branch(Leaf(1), 2, Leaf(3))) // 6

Methods

case class 내부에 메서드를 정의할 수 있으며, this를 이용해 자기 자신을 참조할 수도 있습니다.

1
2
3
4
case class Point(x: Int, y: Int, color: String) {
  def move(dx: Int, dy: Int): Point = Point(x+dy, y+dy, color)
}
Point(3, 4, "RED").move(1, -2)

또한 this 키워드를 사용하여 다음과 같이 작성할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Tree {
  case Leaf(value: Int)
  case Branch(left: Tree, value: Int, right: Tree)


  // This is a method finding integer in Tree.
  def count(x: Int): Int = this match {
    case Leaf(n) if n == x => 1
    case Leaf(_)           => 0
    case Branch(l, n, r) if n == x => l.count(x) + 1 + r.count(x)
    case Branch(l, _, r)           => l.count(x) + r.count(x)
  }
}

import Tree.*
val t = Branch(
  Branch(Leaf(1), 2, Leaf(3)),
  2,
  Branch(Leaf(2), 4, Leaf(2))
)

println(t.count(2))  // 결과: 4

참고로 예시의 트리 모양은 다음과 같습니다.

1
2
3
4
5
    2
   / \
  2   4
 / \ / \
1  3 2  2

First-Class Functions

Scala에서 함수는 1급 객체(first-class citizen)입니다. 즉,

  1. 변수나 데이터에 할당할 수 있고

  2. 객체의 인자로 전달할 수 있으며

  3. 리턴값으로 반환할 수 있습니다.

x에다 1을 더하는 함수는 (x: Int) => x + 1로 쓸 수 있습니다. 이 때, 함수 자체도 변수에 대입이 가능하여 다음처럼 코드를 작성할 수 있습니다.

1
2
3
val inc: Int => Int = (x: Int) => x + 1

inc(3)

즉, inc에 대입하는 내용이 (x: Int) => x + 1이고, inc의 data type은 Int=>Int형입니다. Int를 넣으면 Int를 반환하는 함수를 넣을 수 있는 타입이란 뜻입니다.
해괴망측하지만 ((x: Int) => x + 1)(3)의 결과는 4입니다.
참고로 (x: Int) => x + 1를 더욱 간편하게 쓸 수 있는 방법도 있습니다.

1
2
3
val inc1: Int => Int = (x: Int) => x + 1
val inc2: Int => Int = x => x + 1 // Type Inference
val inc3: Int => Int = _ + 1 // Placeholder Syntax

inc1, inc2, inc3 세 함수 모두 같은 기능을 합니다.

다음은 함수를 argument로 넘길 수 있는 것의 예제입니다.

1
2
3
def twice(f: Int => Int, x: Int): Int = f(f(x))

twice((x: Int) => x + 1, 5)

다음은 함수를 리턴값으로 사용하는 예시입니다.

1
2
3
4
val addN = (n: Int) => (x: Int) => x + n
val add2 = addN(2)
add2(3)
addN(7)(5)

Lists

List는 List[T] 형태로, 불변(immutable) 시퀀스를 의미합니다.

1
2
3
val list: List[Int] = List(3, 1, 2, 5, 4)
val list2 = 1 :: 2 :: 3 :: 4 :: 5 :: Nil //List(1, 2, 3, 4, 5)와 같은 의미
list(1)

중요한 점은 List 역시 immutable합니다.

pattern match in Lists

List에서도 Pattern Matching이 가능합니다. 다음 예제를 보겠습니다.

1
2
3
4
5
6
7
8
val list: List[Int] = 3 :: 1 :: 2 :: 4 :: Nil
def getSecond(list: List[Int]): Int = list match {
  case _ :: x :: _ => x
  case _ => 0
}

getSecond(list) // list의 앞에서부터 2번 째 원소를 꺼내는 함수

Opertions of Lists

List에 적용할 수 있는 여러 operation들의 종류입니다. 아래 예제를 살펴봅시다.

1
2
3
4
5
6
7
8
9
val list: List[Int] = List(3, 1, 2, 4)

list.length // 4
list.map(_ * 2) // List(6, 2, 4, 8)
list.filter(_ % 2 == 1) // List(3, 1)
list.foldLeft(0)(_ + _) // 10, because 0 + 3 + 1 + 2 + 4
list.flatMap(x => List(x, -x)) // List(3, -3, ..., 4, -4)
list.map(x => List(x, -x)).flatten // List(3, -3, ..., 4, -4)
list.sum

.length: 원소 개수 반환
.map(): 각 원소에 함수를 적용해 동일 길이의 새 리스트 반환
.filter(): 조건을 만족하는 함수만 남김
.flatMap(f: A => List[B]): List[B]는 각 원소를 List로 변환하고 결과들을 이어붙여 하나의 리스트로 만든다.
.sum은 리스트 원소의 합을 계산하며, 원소는 반드시 Numeric 타입이어야 한다. (Int, Double, …)

Pairs

pair(T, U)의 형태로 쓸 수 있습니다. c++ STL의 그 pair와 똑같이 생각하면 됩니다.

1
val pair: (Int, String) = (123, "HELLO")

Maps

Map은 type K인 key로부터 type V인 value로 매핑을 하는 type입니다. 다음과 같이 쓸 수 있습니다.

1
2
3
4
val map: Map[String, Int] = Map("a" -> 1, "b" -> 2, "c" -> 3)
map + ("c" -> 3) // Map("a" -> 1, "b" -> 2)
map("a")
map.get("a") // Some(1)

map()으로 값을 얻을 수 있는데, .get()을 사용하면 Option 타입을 얻을 수 있습니다. Option type이란, map 안에 값이 있으면 Option[type]을 반환해주고, 없을 경우에는 None을 반환해줍니다. .get()을 사용하지 않고, map()으로 값을 얻으려고 시도하는데, 값이 없다면 에러가 발생합니다.

Sets

Set[T]은 type T의 서로 다른 원소들이 모여 있는 집합입니다. 집합이기 때문에 같은 원소는 서로 달라야 합니다.

1
2
3
4
5
6
7
8
val set1: Set[Int] = Set(1, 2, 3)
val set2: Set[Int] = Set(2, 3, 5)

set1 + 4
set1 - 2
set1.contains(2) // true
set1 ++ set2 // Set(1, 2, 3 ,5)
set1.intersect(set2) // Set(2, 3)

for comprehension

for comprehension은 nested map, flatMap, filter 등을 위한 간결한 문법입니다. python의 list comprehension을 안다면 이를 이해하기 쉽습니다. scala의 for comprehensio과 같은 간결한 문법을 문법 설탕(syntactic sugar)라고 합니다.

사용 예시는 다음과 같습니다.

1
2
3
4
5
for {
  x <- xlist
  y <- ylist
  if (x+y) % 2 ==0
} yield x * y

Conclusion

지금까지 핵심적인 scala 문법에 대해 알아보았습니다. scala 문법을 새롭게 알게될 때 마다 이곳에 내용을 추가할 예정입니다.

This post is licensed under CC BY 4.0 by the author.