본문 바로가기

코틀린

코틀린: 함수형 프로그래밍과 일급 객체(updated)

함수형 프로그래밍에서 사용하는 일급 객체(first class citizen)란 용어가 낯선가요? 이코노미석, 비지니스석처럼 구분하는 느낌이 들어 거북하지 않았나요? 알고보면 아주 간단한 개념입니다. 함수형 프로그래밍에서 일급 객체란 함수를 변수(=값)처럼 다룰 수 있다는 뜻입니다. 자세한 내용은 쉽게 다가가는 최신 프로그래밍: 코틀린 - 3.2 함수형 프로그래밍을 참고하기 바랍니다.

아래 예에서는 두 수를 더하는 함수 sum()을 정의합니다. sum()의 형식인자 a, b는 Int 타입 변수를 전달받습니다. 함수 sum()은 덧셈 결과인 Int 타입 변수 c를 반환합니다. 여기까지는 일반 함수입니다.

fun sum(a:Int, b:Int): Int {
    val c: Int = a + b
    return c
}

fun main() {
    println(sum(3, 4))
}

함수가 일급 객체라면 "함수를 변수처럼 다룰 수 있어야 합니다."  아래처럼 함수 sum()을 변수 funcVar에 할당할 수 있습니다. funcVar은 함수 sum()을 참조합니다. "::"은 참조 연산자입니다. 변수 funcVar의 타입은 무척 복잡하죠. KFunction2에서 2는 형식인자가 2개라는 뜻입니다. 3번째 타입 Int는 이 함수의 반환 타입을 가리킵니다.

import kotlin.reflect.KFunction2

fun sum(a:Int, b:Int): Int {
    val c: Int = a + b
    return c
}

fun main() {
    val funcVar = ::sum
    // val funcVar: KFunction2<Int, Int, Int> = ::sum
    println(funcVar(3, 4))
}

이번엔 덧셈 함수 firstCitizen()을 정의합니다. 함수 sum()의 형식인자로 Int 타입 변수 대신 함수 firstCitizen()을 직접 전달하려면 어떻게 해야 할까요?  sum()의 형식인자 타입을 함수 firstCitizen()의 서명에 맞게 함수 타입(Int 타입 형식 인자 2개, 반환 타입은 Int)으로 선언합니다.  이렇게 함수를 변수처럼 사용할 수 있으면 이 함수를 일급 객체라고 부릅니다.

fun firstCitizen(a: Int, b: Int) = a + b

fun sum(op: (Int, Int) -> Int): Int {
    val c = op(3, 4)
    return c
}

fun main() {
    println(sum(::firstCitizen))  // 7
}

 

잠깐! 함수 sum()은 덧셈을 위한 형식인자를 따로 정의하지 않았죠. 아래처럼 2개의 피연산자 x, y를 추가해서 구현할 수 있습니다.

fun firstCitizen(a: Int, b: Int) = a + b

fun sum(x: Int, y: Int, op: (Int, Int) -> Int): Int {
    val c = op(x, y)
    return c
}

fun main() {
    println(sum(3, 4, ::firstCitizen))  // 7
}

일반 함수를 사용하는 대신 람다 식을 사용하면 훨씬 간단히 구현할 수 있습니다. 일반 함수를 참조했을 때 타입과 비교하면, 람다 식을 참조하는 변수 firstCitizenLambda의 타입은 함수 sum()의 형식인자 타입과 같습니다. 또, 람다 식을 할당받은 변수는 참조 연산자(::)가 필요 없습니다.

val firstCitizenLambda: (Int, Int) -> Int = { a, b -> a + b }

fun sum(x: Int, y: Int, op: (Int, Int) -> Int): Int {
    val c = op(x, y)
    return c
}

fun main() {
    println(sum(3, 4, firstCitizenLambda))  // 7
}

 

함수를 형식인자로 전달하는 과정이 복잡해 보이지만, 형식인자로 전달된 함수를 호출할 때 비로소 연산을 실행합니다. 이런 상황을 이해하기 쉽도록 op(x, y) 대신 op.invoke(x, y)로 쓸 수 있습니다.

val c = op.invoke(x, y)

 

함수를 형식인자로 전달하면 뭐가 좋을까요? 다양한 함수를 형식인자로 전달할 수 있습니다. 위 예에서는 덧셈 함수만 전달했지만, 곱셈 함수, 나눗셈 함수 등 다양한 함수를 전달할 수 있죠... 레고 블록 조립할 때 생각해 보세요. 기본적인 블록들을 결합해 해적선, 마법의 성 등을 만들죠. 함수형 프로그램도 마찬가지입니다. 똘똘한 함수 조각들을 만들어 놓고, 이 함수 조각들을 조합해 더 복잡한 함수를 만들 수 있죠. 이런 접근 방식이 바로 함수형 프로그래밍이 등장하게 된 배경입니다.