탐비의 개발 낙서장

[Kotlin] 코틀린 함수형 프로그래밍 본문

프로그래밍/Kotlin

[Kotlin] 코틀린 함수형 프로그래밍

탐비_ 2021. 7. 28. 23:47
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의 참조투명성은 시스템을 이해하기 쉽고 예상 가능하게 해 개발을 용이하게 해주니, 두 가지 개념을 함께 사용해서 설계하여 두 기법의 장점만 골라 함께 이용해 개발할 수 있는 방법을 고민해야할 것 같습니다.