코틀린(Kotlin)과 함수형 프로그래밍
함수형 프로그래밍이란?
함수형 프로그래밍은 절차 지향, 객체지향, 관점 지향 등과 같은 하나의 프로그래밍적 패러다임(프로그램을 구성하는 스타일)입니다.
함수형 프로그래밍의 본질적인 핵심은 표현식으로 데이터를 변환하는 것이고, (이상적으로는) 표현식에 부수효과(side-effect)가 없어야 합니다.
"함수형"이라는 이름에 걸맞게 함수형 프로그래밍은 수학 함수의 컨셉을 기반으로 합니다.
수학 함수는 입력과 출력 집합 사이의 관계를 정의한다. 각 입력은 하나의 출력만 합니다.
예를 들어, 함수 f(x) = x^2에서 f(5)는 언제나 25 입니다.
프로그래밍 언어에서 파라미터가 있는 함수를 호출하면 언제나 같은 값을 반환하도록 보장하는 방법은 가변적인 상태에 접근하는 것을 피하게 하고, 이는 멀티쓰레딩 환경에서 동기화 문제로부터 벗어나게 해 줍니다.
예를 들어, 아래와 같은 함수는 같은 인풋에 대해 항상 동일한 결과를 반환합니다.
fun f(x: Int) : Int {
return x * x
}
f 함수는 외부 상태에 접근하지 않고 파라미터에 대해서만 처리를 하기 때문에 f(5) 호출은 언제나 25를 반환할 것입니다.
fun main(args: Array<String>) {
var i = 0
fun g(x: Int) Int {
return x * i
}
println(g(5)) // 0 출력
i = 5
println(g(5)) // 25 출력
}
반면에 위의 g 함수는 가변적인 상태 i에 따라 같은 입력 값임에도 다른 값을 반환합니다.
사실상 이런 부수 효과를 완전히 배제하면서 프로그래밍하는 것은 쉽지 않습니다. 그래서 "이런 부수 효과가 나타나는 모든 함수는 다 함수형스럽지 않아!" 라고는 할 수 없지만, 적어도 함수형스럽게 짠다는 게 뭔지 알고 그를 '지향'하는 것이 함수형 프로그래밍이 아닐까 싶습니다.
그렇다면 함수형 프로그래밍 스타일은 어떤 이점을 갖고 있을까요?
함수형 프로그래밍 스타일은 다음과 같은 이점을 제공합니다.
- 코드는 읽기 쉽고 테스트하기 쉽다.
외부 가변 상태에 의존하지 않는 함수는 더 쉽게 접근할 수 있고, 더 쉽게 증명할 수 있습니다. - 상태(state)와 부수 효과(side effect)가 신중하게 계획된다.
상태 관리를 코드에 개별적으로 특정 위치로 제한하는 것은 유지와 리팩토링을 쉽게 만듭니다. - 동시성이 좀 더 안전해지며 더 자연스러워진다.
가변 상태가 없다는 것은 동시성 코드가 코드 주변에서 잠금이 적거나 필요 없다는 것을 뜻합니다.
일급 함수(first-class functions)
함수형 프로그래밍의 가장 기본적인 컨셉은 일급 함수(first-class functions) 입니다.
일급 함수를 지원하는 프로그래밍 언어는 함수를 다른 타입으로 취급합니다.
즉, 함수를 변수, 파라미터, 리턴, 일반화 타입 등으로 사용할 수 있게 합니다.
하나하나 예시를 살펴보자면 우선 아래와 같이 변수처럼 함수를 저장할 수 있습니다.
val capitalize = { str: String -> str.capitalize() }
fun main(args: Array<String>) {
println(capitalize("hello, world!")) // 결과 : HELLO, WORLD!
}
코틀린 공식 문서에서는 이런 종류의 함수를 '람다(lambda)'라고 부릅니다.
사실 이 람다 함수는 코틀린 컴파일러에 의해 Funtion1<P, R> 객체로 변환이 되는데, 내부적으로 다음 코드와 동일한 기능을 합니다.
val capitalize = object : Function1<String, String> {
override fun invoke(p1: String): String {
return p1.capitalize()
}
}
이렇듯 코틀린에서 람다 함수는 결국엔 객체로 관리가 되기 때문에 다른 함수의 파라미터로도 사용할 수 있습니다.
다음 예제를 살펴보겠습니다.
fun transform(str: String, fn: (String) -> String): String {
return fn(str)
}
위와 같이 transform 함수에서 두 번째 파라미터 타입으로 (String) -> String 형식의 함수를 파라미터로 받겠다고 정의를 해놓으면 앞서 만들었던 capitalize에 저장된 함수를 파라미터로 사용할 수 있습니다.
fun main(args: Array<String>) {
println(transform("kotlin", capitalize)) // 결과 : KOTLIN
}
그리고 transform 함수와 같이 다른 함수를 파라미터로 사용하거나 리턴 값으로 사용하는 함수를 고차 함수(higher-order function)이라고 합니다.