일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- GC
- generics
- 딥러닝
- jsoup
- 안드로이드 스튜디오
- data class
- thread pool
- android studio
- 파이참
- Android IDE
- sealed class
- 파이썬
- aging
- InteliJ
- android studio 설치
- Android
- Schedule
- PyCharm
- 안드로이드 스튜디오 설치
- Kotlin
- jvm
- process
- HTML Parser
- cache
- 안드로이드
- fixedRateTimer
- 크롤링
- 객체지향
- Anaconda
- GIT
- Today
- Total
탐비의 개발 낙서장
[Kotlin] 코틀린 함수형 프로그래밍 본문
Programming Paradigms
명령형 프로그래밍 언어
"어떻게 할 것인가(How)"
- 프로그램의 상태와 상태를 변경시키는 구문의 관점에서 연산을 설명하는 방식의 언어
- 세분화하면 절차지향 프로그래밍 언어와 객체지향 프로그래밍 언어로 나눌 수 있습니다.
- 절차지향 프로그래밍은 수행되어야 할 연속적인 계산 과정을 포함하는 방식으로 구성됩니다. (C, C++ 등)
- 객체지향 프로그래밍은 객체들의 집합으로 프로그램의 상호작용을 표현합니다. (C++, JAVA, C# 등)
함수형 프로그래밍 언어
"무엇을 할 것인가(What)"
- 어떤 방법으로 해야하는지를 나타내기보다 무엇과 같은지를 설명하는 방식의 언어
- 함수형 프로그래밍은 순수 함수를 조합하여 소프트웨어를 만드는 방식으로 구성됩니다. (Clojure, Haskell 등)
특성 비교
특성 | 절차형 접근방식 | 함수형 접근방식 |
프로그래머 관심 사항 |
작업을 수행하는 방법(알고리즘) 상태 변경 추적 |
원하는 정보가 무엇인가 어떤 변환이 필요한가 |
상태(값) 변경 | 상태값 참조가 중요함 | 값을 복사하여 사용하므로 변하지 않음 |
실행 절차 | 중요도 높음 | 중요도 낮음 |
제어 흐름 | 반복문, 조건문, 함수 호출 | 변화하는 값 자체를 다루지 않기 때문에 재귀를 비롯한 함수 호출로 구현 |
구현 단위 | 구조체 또는 클래스 | 일급 객체와 데이터 컬렉션으로 함수 사용 |
람다식 / Closure
람다식
- 수학의 람다 대수에서 유래하여 만들어진 식으로, 중괄호 기호와 화살표 기호를 사용합니다.
- 람다식은 1급 객체인 이름 없는 함수입니다.
{ x, y -> x + y }
Closure
- Closure는 close over라는 의미로 일반적으로 상위 함수의 변수를 접근할 수 있는 하위 함수를 의미합니다.
- 원래는 선언된 범위(scope)에서 바깥의 변수를 캡쳐해서 저장하고 닫히는 개념입니다.
- 하지만 Kotlin / JS / Swift 클로저는 캡처한 변수를 참조(reference)합니다. (순수 함수가 아니게 됨)
- forEach 함수는 익명 함수를 인자로 받습니다. 람다식으로 생성한 익명함수도 클로저 함수가 되어,
Outer Scope의 변수에 접근할 수 있습니다.
val list = listOf(-1, 0, 1, 2, 3, 4)
var sum = 0
list.filter { it > 0 }.forEach {
sum += it // 외부 Scope의 sum에 접근
}
print(sum) // sum = 10
- 아래의 경우에도 반환값인 람다함수 { it * num }이 상위 함수인 myClosure의 지역 변수를 사용한 것을 알 수 있습니다.
하지만 myClosure가 종료 되었음에도, main 함수의 f1(10) / f2(10)에서 지역 변수 num이 계속 사용되었습니다.
fun myClosure(num: Int) : (Int) -> Int {
println(num)
return { it * num }
}
fun main(args: Array<String>) {
val f1 = myClosure(2)
val f2 = myClosure(3)
println("${f1(10)}") // result : 2 20
println("${f2(10)}") // result : 3 30
}
Closure vs Function
- 함수는 함수 안에 정의된 로컬 변수의 life cycle은 함수의 life cycle과 같습니다.
- 그러나 어떤 함수가 1) 자신의 로컬 변수를 포획(Capture)한 람다를 반환하거나, 2) 다른 변수에 저장한다면
함수가 종료되어도 로컬 변수가 사라지지 않아 람다의 본문 코드가 포획한 변수에 접근 / 수정할 수 있습니다.
함수형 프로그래밍 개념
불변성 Immutable
- 데이터는 변경되지 않으며 프로그램의 상태만 표현한다.
- 함수에서 데이터는 변경하지 않고 새로운 데이터를 만들어 반환한다.
- 람다 계산법의 근간이 되는 개념은 심볼의 값이 변경되지 않는다는 것입니다.
- 변경할 수 없는 상수 데이터만 이용하고 함수의 흐름에 따라 프로그래밍하자는 접근입니다.
1급 객체 / 1급 시민 First Object
- 변수나 데이터 구조 안에 담을 수 있다.
- 파라미터로 전달 할 수 있다.
- 반환값으로 사용할 수 있다.
- 할당에 사용된 이름과 관계없이 고유한 구별이 가능하다.
- 동적으로 프로퍼티 할당이 가능하다.
- 만약 함수가 일급 객체라면 일급 함수라고 부를 수 있으며, 이름이 없다면 람다식이라고 부를 수 있습니다.
- Kotlin에서는 함수가 일급 객체로, 함수도 객체처럼 변수나 함수의 인자로 다룰 수 있습니다.
순수 함수 Pure Function
- 동일한 입력에는 항상 같은 값을 반환합니다.
- 함수의 실행이 프로그램의 실행에 영향을 미치지 않습니다. (= 부작용(side-effect)이 없는 함수)
- 순수 함수의 조건을 만족하면 참조투명성을 가진다고 할 수 있습니다.
var c = 10
// 언제 어디서 실행해도 결과가 같고, 외부 상태를 변경하지 않으므로 순수 함수임
fun pureAdd(x: Int, y: Int) = x + y
// 외부의 c라는 값이 변하면 결과 값이 달라지므로 순수 함수가 아님
fun impureAdd1(x: Int) = x + c
// 외부의 c라는 값을 변경하는 코드를 포함하므로 순수 함수가 아님
fun impureAdd2(x: Int, y: Int){
c = b
return x + y
}
고차 함수 High-Order Function
- 고차 함수는 다른 함수를 인자로 사용하거나, 함수를 결과 값으로 반환하는 함수를 의미합니다.
- 람다 계산법에서 만들어진 용어로, 아래와 같은 특징을 가집니다.
- 함수에 함수를 파라미터로 전달 할 수 있다.
- 함수의 반환값으로 함수를 사용할 수 있다.
- 고차 함수의 예시로, map / reduce / filter 를 이야기합니다.
- 코틀린에서는 map / fold(reduce) / filter 로 각각 매칭됩니다.
- map()은 collection 원소를 원하는 형태로 변환하여 List 형태로 반환합니다.
- filter()는 collection 중 주어진 predicate를 만족하는 원소 리스트를 반환합니다.
- fold()는 초기 값을 설정해주고, 첫번째에서 마지막 요소까지 현재의 계산 값에 각각을 적용하는 함수입니다.
- reduce()는 첫번째에서 마지막 요소까지 현재의 계산 값에 각각을 적용하는 함수입니다.
val list = listOf(1, 2, 3, 4)
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
// map Example
val mapped = people.map { it.name }
// mapped = ["Alice", "Bob"]
// filter Example
val filtered = list.filter { it % 2 == 0 })
// filtered = [2, 4]
// fold Example
val reduced = list.fold(0){ sum, element -> sum + element }
// reduced = 10
- 또 다른 고차함수로 all / any / find 등이 있습니다.
- all은 collection 전체가 주어진 predicate를 만족하는지를 판단합니다.
- any는 collection 원소 중 하나라도 주어진 predicate를 만족하는지를 판단합니다.
- find는 주어진 predicate에 만족하는 첫번째 원소를 반환합니다.
val canBeUnder30 = { p: Person -> p.age < 30 }
// all Example
val allEx = people.all(canBeUnder30)
// allEx = false
// any Example
val anyEx = people.any(canBeUnder30)
// anyEx = true
// find Example
val findEx = people.find(canBeUnder30)
// reduced = Person[ name: "Alice", age: 29 ]
OOP vs FP
객체지향 프로그래밍 Object Oriented Programming
- 데이터(상태)를 다루는 개념으로, 함수의 동작부를 캡슐화해서 코드를 이해하게 합니다.
- 클래스 디자인과 객체들의 관계를 중심으로 코드 작성이 이루어집니다.
- 상태, 멤버 변수, 메소드 등이 긴밀한 관계를 가지고 특히 멤버 변수의 상태에 따라 결과가 달라집니다.
함수형 프로그래밍 Functional Programming
- 간결한 코드 작성으로 동작부를 최소화해서 코드의 이해를 돕습니다.
- 값의 연산 및 결과 도출 중심으로 코드 작성이 이루어집니다.
- 간결한 과정으로 처리하고 매핑하는데에 주 목적을 둡니다.
생각
기존에 대부분 JAVA를 이용해서 프로그래밍 해왔기 때문에 객체지향적으로 프로그래밍 해야겠다는 생각이 항상 우선적으로 따라오는 경향이 있는 것 같습니다.
OOP를 메인으로 개발해 오면서 OOP의 핵심은 다형성이라고 생각했는데, FP의 핵심적인 요소인 참조 투명성과 함께 사용하면 더 좋은 방향이 될 것이라고 생각합니다.
OOP의 다형성을 이용하게 되면 결합성을 낮춘 시스템을 만들어주게되고, FP의 참조투명성은 시스템을 이해하기 쉽고 예상 가능하게 해 개발을 용이하게 해주니, 두 가지 개념을 함께 사용해서 설계하여 두 기법의 장점만 골라 함께 이용해 개발할 수 있는 방법을 고민해야할 것 같습니다.
'프로그래밍 > Kotlin' 카테고리의 다른 글
[Kotlin] Observer 패턴과 Publisher-Subscriber 패턴 (0) | 2021.08.03 |
---|---|
[Kotlin] 코틀린 Timer (0) | 2021.08.02 |
[Kotlin] 코틀린 객체지향 설계와 다양한 클래스 타입 (0) | 2021.07.27 |
[Kotlin] XML Parser 구현 (0) | 2021.07.26 |
[Kotlin] JSoup 이용 웹 크롤링 해보기 (0) | 2021.07.21 |