[Course] 01-2: 프로그램 설계 방법
카이스트 류석영 교수님의 강의를 듣고 정리한 정리본입니다.
✨ List
리스트는 원소가 0 또는 그 이상인 집합을 의미한다.
Scala에서는 리스트 또한 불변이다.
생성 방법은 아래와 같다.
1. List([expression], ..., [expression]):
2. [expression] :: ... :: [expression] :: Nil
리스트는 원소 모두 같은 타입이어야 한다.
또한, ::로 리스트를 묶을 때는 마지막 원소가 리스트여야 하기 때문에 빈 리스트를 의미하는 Nil을 붙여야 한다.
예시는 다음과 같다.
List(1, 2, 3)
1 :: 2 :: 3 :: Nil
둘 모두 같은 리스트가 생성된다.
차이점이라면 List(1, 2, 3)이 만드는 데 더 편하지만, 원소를 덧붙일 때는 두 번째 방식이 더 편하다는 점이다.
val l = List(1, 2, 3)
0 :: l
// 결과는 다음과 같다.
val res14: List[Int] = List(0, 1, 2, 3)
0 :: l에서 Nil이 안 붙어도 되는 까닭은 이미 l이 리스트이기 때문이다.
자바와 비슷하게, 인덱스로 접근 가능하다.
// 예제 1
l.length
val res15: Int = 4 // 결과
// 예제 2
l(0)
val res16: Int = 0 // 결과
Patern matching
리스트를 패턴 매칭과 결합할 수도 있다.
[expression] match {
case Nil => [expression]
case [name] :: [name] => [expression]<br/ }
다음과 같이 사용 가능하다.
def headOrZero(l: List[Int]) = l match {
case Nil => 0
case h :: t => h
}
headOrZero(List(1, 2, 3))
val res17: Int = 1 // 결과
headOrZerl(List())
val res18: Int = 0 // 결과
🫧 match
패턴 매칭 표현식을 표현할 때 match를 사용한다.
[expression] match {
case 패턴1 => 결과1
case 패턴2 => 결과2
…
}
여기서 [expression]에는 어떤 값이 들어가도 상관 없다.
// ex1
x match {
case 0 => "zero"
case 1 => "one"
case _ => "other"
}
// ex2
true match {
case true => 1
case false => 2
}
그러나 다음 두 가지 상황에서 문제가 발생할 수 있다.
💡 주의 1: match 대상과 패턴이 맞지 않는 경우
// 오류!
true match {
case 1 => 3
}
💡 주의 2: 어떤 case에도 매칭되지 않는 경우
// 오류!
3 match {
case 1 => "one"
case 2 => "two"
}
특히 자바와 같은 경우에는 에러가 나지 않지만, Scala의 match는 expression이라서 결과값이 반드시 있어야 한다.
cf) 요즘 자바는 expression으로 나타낼 수 있어서, 만일 자바에서 switch를 expression 형태로 사용한다면 반드시 default 키워드를 넣어줘야 한다.
따라서 이런 경우 _라는 와일드카드 패턴 (그 외 모든 값)을 넣어 오류를 방지한다.
✨ Tuples
([expression], …, [expression])
scala> (1, true) // == (1:Int, true:Boolean)
val res19: (Int, Boolean) = (1, true) // 결과
각 튜플에 접근하기 위해서는 ._i 기호를 사용한다.
scala> (1, true)._1
val res20: Int = 1 // 결과
🫧 튜플 vs 리스트
- 튜플은 각기 다른 타입으로 원소를 구성할 수 있지만, 리스트는 그렇지 않다.
- 리스트는 임의의 인덱스에 접근할 수 있지만 튜플은 그렇지 않다.
두 번째 차이점의 예를 들어보자.
val l = List(10, 20, 30)
// 이 경우에는 다음과 같이 인덱스로 접근이 가능하다.
l(0) //10
l(1) //20
l(2) //30
l의 타입이 List[Int]이기 때문에 어느 인덱스를 골라도 타입이 같다는 걸 알지만, Tuple은 위치마다 타입이 다를 수 있기 때문에 해당 문제가 발생하는 것이다.
val t = (1, true, "hello")
t._1 // Int
t._2 // Boolean
t._3 // "hello"
다음과 같이 위치가 정해져 있는 경우 인덱스로 접근이 가능하다.
그러나 t(i)일 때 i가 실행하며 정해진다면 t(i)의 타입을 컴파일러는 알 수 없다.
추가로, 길이가 0인 tuple는 ()로 나타낼 수 있으며, Scala에서 ()는 바로 Unit의 값이므로 unit으로 고려해야 한다.
cf) List()는 빈 리스트이며, unit(구성요소가 0인 tuple로 생각할 수 있는 unit 값)은 돌려줄 정보 자체가 없다는 뜻이다.
✨ map
Map([expression] -> [expression], …)
scala> val m = Map(1 -> "one", 2 -> "two", 3 -> "three")
val m: Map[Int, String] = Map(1 -> one, 2 -> two, 3 -> three) //결과
원소 접근 시 다음과 같이 표기한다.
scala> m(2)
val res21: String = two //결과
val 대신 var m으로 표기해도 문제는 없다.
기본 Map의 경우 immutable Map이므로 Map 자체는 불변이지만 m이라는 변수를 var로 선언했다고 보면 된다.
💡 원소 추가
var m = Map(1 -> "one", 2 -> "two")
m = m + (3 -> "three")
Map은 불변이므로 m을 복사하여 m + (3 -> “three”)를 수행한다.
이때 새로운 Map이 생성되며 m이 가리키는 곳을 변화시켜주므로 Map은 여전히 불변 상태로 존재하게 된다.
✨ 클래스와 오브젝트
case class name
클래스는 필드 명시가 필수다.
scala> case class Student(name: String, height: Int)
class Student // 결과
scala> val s = Student("John Doe", 173)
val s: Student = Student(John Doe, 173) // 결과
dot 연산자로 필드에 접근 가능하다.
scala> s.name
val res22: String = John Doe // 결과
Object 또한 불변을 기본으로 가진다. 그러나 field를 가변으로 설정할 경우 Object는 가변이 될 수 있다.
case class Student(name: String, var height: Int)
val s = Student("John Doe", 173)
다음과 같이 field를 height로 설정한다면 이후 height를 dot 연산자로 접근하여 바꿀 수 있다.
✨ 질문
Q. null vs nil
- Scala는 null과 nil이 모두 존재한다.
- null은 참조할 객체가 없다는 뜻이고, Nil은 빈 List로, 요소가 0개인 정상적인 리스트 객체로 해석된다.
- null에 접근하면 NPE가 터지게 되며, Nil은 정상적으로 접근 가능하다는 점에서 차이가 있다.
- 참고로, Ruby, Swift, Go 언어에서 nil은 null과 일맥상통한다고 볼 수 있다.
Q. 왜 ::로 리스트 생성 시 마지막에 Nil이 붙어야 하는가?
A. Nil이 빈 리스트를 뜻하기 때문이다.
1 :: 2 :: 3 :: Nil의 경우, 실제로는1 :: (2 :: (3 :: Nil))형태로 묶인다.
3 :: Nil → List(3)
2 :: List(3) → List(2, 3)
1 :: List(2, 3) → List(1, 2, 3)
-
그런데 만약 2 :: 3만 있다면, 2: Int, 3: Int로 해석되어 :: 연산자를 처리할 수 없게 된다.
-
위와 같은 방식으로, 이미
l이라는 리스트가 있다면1 :: l은l이 리스트이므로 오류가 나지 않게 되는 것이다. -
따라서 ::의 오른쪽에는 반드시 리스트가 와야 한다.
Q. 0 :: l로 리스트를 추가하면 불변식이 깨지는 거 아닌가?
A. l을 꼬리로 재사용해 새로운 리스트를 만들기 때문에 불변식은 유지된다.
- Scala의 immutable List는
단일 연결 리스트형태를 취하고 있어서, 0 :: l을 하게 된다면 기존 리스트를 복사해 새 원소를 추가한 새 리스트를 만들기 때문에 불변식이 유지되는 것이다.
Q. Scala에서는 리스트 자료구조를 어떤 식으로 표현하고 있을까?
A. JVM 기반으로 동작하기 때문에 객체와 참조 방면에서는 Java와 비슷하나, 수정하지 않고 새 리스트를 만든다는 점에서 Java의 ArrayList, LinkedList와 사용 철학이 다르다.
Q. 기존 리스트에 원소를 추가하는 것이 가능할까?
A. 순수 함수를 지향한다는 철학에 어긋나긴 하지만… 다음과 같이 프로그래밍하면 된다.
var l = List(1, 2, 3)
l = 0 :: l
- 그러나 list 자체가 mutable해진 것은 아니다.
var l = List(1, 2, 3)을 수행하며 l이라는 리스트를 만든 후,l = 0 :: l을 수행하며 새 리스트 생성 후 l이라는 변수가 새 리스트를 가리키도록 변경된다.- 따라서 List 객체 자체는 불변이고,
l이라는 변수는 mutable해지는 것이다.
Q. l(f())에서 f()가 정해지지 않은 경우 오류가 난다고 했는데, 그렇다면 정해지지 않는다는 말이 무엇일까?
A. f()의 타입을 알 수 없는 경우를 말한다.
-
우선 첫 번째, 만약 l(f())에서 f()가 사용자 입력으로 결정되는 경우, 컴파일 오류가 나지 않는다.
-
값은 상관 없으나, f()의 타입이 Int인지 확인할 수만 있으면 된다.
-
아래와 같은 식은 a의 값이 정해지지 않았으므로 에러가 난다. (리스트의 경우 에러 X)
// 튜플의 경우 (에러 O)
// a가 expression으로 해석되기 때문
val a = 1 + 1
val t = (10, true, false, 20)
val x = t._a
// 리스트의 경우 (에러 X)
val a = 1 + 1
val l: List[Int] = List(10, 20, 30)
val x = l(a)