코틀린으로 배우는 함수형 프로그래밍 - 2장 코틀린으로 함수형 프로그래밍 시작하기
범위
- 2장 코틀린으로 함수형 프로그래밍 시작하기
요약
- 함수형 프로그래밍의 특징은 다음과 같다.
- 불변성(immutable)
- 참조 투명성(referential transparency)
- 일급 함수(first-class function)
- 게으른 평가(lazy evaluation)
개념 정리
- 익명 함수(anonymous function): 함수 이름을 선언하지 않고, 구현부만 작성하는 함수를 표현하는 방식의 일종.
- 확장 함수(extension function): 이미 작성된 클래스에 상속을 하거나 내부를 수정하지 않고 새롭게 추가한 함수.
- 패턴 매칭(pattern matching): 값, 조건, 타입 등의 패턴에 따라서 매칭되는 동작을 수행하게 하는 기능.
- 객체 분해: 객체를 구성하는 프로퍼티를 분해하여 편리하게 변수에 할당하는 것.
- 제네릭(generic): rorcp 내부에서 사용할 데이터 타입을 외부에서 정하는 기법.
- 변성(variance): 제네릭을 포함한 계층 관계에서 타입의 가변성을 처리하는 방식.
- 무공변(invariant): 타입 S가 T의 하위 타입일 때,
Box<S>
가Box<T>
는 상속 관계가 없다. - 공변(covariant): 타입 S가 T의 하위 타입일 때,
Box<S>
는Box<T>
의 하위 타입이다. - 반공변(contravariant): 타입 S가 T의 하위 타입일 때,
Box<T>
는Box<S>
의 하위 타입이다.
- 무공변(invariant): 타입 S가 T의 하위 타입일 때,
책에서 기억하고 싶은 내용
- 2.1 프로퍼티 선언과 안전한 널 처리
- val: 읽기 전용 프로퍼티를 선언하는 예약어
- var: 선언 이후에 수정이 가능한 가변(mutable) 프로퍼티를 선언하는 예약어
- 타입 뒤에 ?를 붙이면 해당 프로퍼티는 값으로 널을 할당할 수 있다.
- 2.2 함수와 람다
- fun: 함수의 예약어
- 반환 타입을 명시하지 않으면 Unit 타입을 반환한다.
- 2.3 제어 구문
- 코틀린에서 if문은 기본적으로 표현식이다. 표현식은 구문과 달리 결과로서 어떤 값을 반환한다.
- when문도 표현식이다. when문은 java의 swich문이나 스칼라의 패턴 매칭과 유사한 기능을 한다.
- 2.4 인터페이스
- 코틀린에서 제공하는 인터페이스는 다음과 같은 특징이 있다.
- 다중 상속이 가능하다.
- 추상(abstract) 함수를 가질 수 있다.
- 함수의 본문을 구현할 수 있다.
- 여러 인터페이스에서 같은 이름의 함수를 가질 수 있다.
- 추상 프로퍼티를 가질 수 있다.
- 코틀린에서 제공하는 인터페이스는 다음과 같은 특징이 있다.
- 인터페이스에서는 추상 프로퍼티의 값을 직접 초기화할 수 없고, getter를 구현해야 한다.
1
2
3
4
interface Foo {
val bar1:Int = 3 // 컴파일 에러
val bar2: Int get() = 3
}
- 2.5 클래스
- data class
- data class는 기본적으로 게터(getter), 세터(setter) 함수를 생성해주고, hashCode, equals, toString 함수와 같은 자바 Object 클래스에 정의된 함수들을 자동으로 생성한다.
- data class로 선언된 객체는 추가로 copy와 componentN 함수를 제공한다. copy 함수는 객체의 값을 그대로 복사한 새로운 객체를 생성할 때 사용된다. componentN 함수는 객체가 가진 프로퍼티의 개수만큼 호출할 수 있는데, 프로퍼티 이름으로 접근하는 대신에 사용된다. 예를 들어 component1 함수는 객체의 첫 번째 프로퍼티의 값을 반환한다.
- enum class
- enum class는 특정 상수에 이름을 붙여주는 클래스다.
- sealed class
- sealed class는 enum class의 확장 형태로, 클래스를 묶은 클래스이다. 제약 없이 새로운 타입을 확장 할 수 있다.
- data class
- 2.6 패턴 매칭
- when문에 값을 넣지 않으면 조건문에 따른 패턴을 정의할 수 있다.
- 2.7 객체 분해
- 2.8 컬렉션
- 코틀린에서는 불변과 가변(muable) 자료구조를 분리해서 제공하고 있고, List, Set, Map 등의 자료 구조는 기본적으로 불변이다.
- List와 Set은 불변 자료구조이기 때문에 add 함수가 없다. 대신 plus 함수가 제공된다. plus 함수는 원본 리스트를 변경하지 않고 새로운 리스트를 반환한다.
- 2.9 제네릭
- 제네릭을 사용해 클래스를 일반화하면 재사용성이 높아진다. 마찬가지로 제네릭으로 함수의 타입을 일반화하면 재사용성이 높은 함수를 만들 수 있다.
- 2.10 코틀린 표준 라이브러리
- 람다 리시버는 receiver의 타입 T를 block 함수의 입력인 T.()로 전달한다.
- use는 클로즈 작업을 자동으로 해 주는 함수이다.
- let, with, run, apply, also 함수
- 사용 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
fun main() {
generalExample(Person("example", 30))
letExample(Person("example", 30))
withExample(Person("example", 30))
runExample(Person("example", 30))
applyExample(Person("example", 30))
alsoExample(Person("example", 30))
}
data class Person(var name: String, val age: Int)
fun generalExample(input: Person) {
input.name = "generalExample"
val result = input
println("일반적인 객체 변경")
println("result: $result")
println()
}
fun letExample(input: Person) {
val result = input.let {
it.name = "letExample"
it
}
println("let")
println("result: $result")
println()
}
fun withExample(input: Person) {
val result = with(input) {
name = "withExample"
this
}
println("with")
println("result: $result")
println()
}
fun runExample(input: Person) {
val result = input.run {
name = "runExample"
this
}
println("run")
println("result: $result")
println()
}
fun applyExample(input: Person) {
val result = input.apply {
name = "applyExample"
}
println("apply")
println("result: $result")
println()
}
fun alsoExample(input: Person) {
val result = input.also {
it.name = "alsoExample"
}
println("also")
println("result: $result")
println()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
일반적인 객체 변경
result: Person(name=generalExample, age=30)
let
result: Person(name=letExample, age=30)
with
result: Person(name=withExample, age=30)
run
result: Person(name=runExample, age=30)
apply
result: Person(name=applyExample, age=30)
also
result: Person(name=alsoExample, age=30)
1
- 비교
let | with | run | apply | also | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
코드 블록 | 람다식 | 람다 리시버 | 람다 리시버 | 람다 리시버 | 람다식 | ||||||||||
접근 | it | this | this | this | it | ||||||||||
반환값 | 람다식 반환값 | 람다식 반환값 | 람다식 반환값 | 자기 자신 | 자기 자신 | ||||||||||
확장 함수 여부 | O | X | X | O | O | ||||||||||
선언 |
|
|
|
|
|
- 2.11 변성
무공변 | 공변 | 반공변 | |
---|---|---|---|
전제 조건 | 타입 S가 T의 하위 타입(S --▷ T) ex) Kotlin --▷ Language | ||
상속 관계 | 상속 관계가 없다 | Box<S> --▷ Box<T> ex) Box<Kotlin> --▷ Box<Language> | Box<S> ◁-- Box<T> ex) Box<Kotlin> ◁-- Box<Language> |
키워드 | X | out ex) Box<out Language> | in ex) Box<in Kotlin> |
java 키워드 | extends (upper bound) ex) Box<T extends Language> (Kotlin: Box<T : Language>) | super (low bound) ex) Box<T super Kotlin> (Kotlin: x) | |
속성 | out 키워드를 사용한 공변에서는 Box 안의 값을 꺼내서 읽을 때(read)는 문제가 없지만, Box에 값을 넣으려고 할 때(write) 컴파일 오류가 발생한다. | out 키워드를 사용한 반공변에서는 Box 안의 값을 넣을 때(write)는 문제가 없지만, Box에 값을 읽으려고 할 때(read) 컴파일 오류가 발생한다. |
연습 문제
2-1
1
2
3
4
5
6
fun String.helloThis() = "Hello, $this"
fun main() {
require("kotlin".helloThis() == "Hello, kotlin")
require("FP".helloThis() == "Hello, FP")
}