본문 바로가기
Program Language/Kotlin

[Do it! 코틀린 프로그래밍] 3. 함수와 함수형 프로그래밍

by SungJe 2021. 4. 3.

3. 함수와 함수형 프로그래밍


1. 함수 선언하고 호출하기

함수는 인자를 입력받아 기능을 수행하고 결과값 인자를 입력받아 기능을 수행하고 결괏값을 반환하는 코드의 모음이다. 함수가 호출되면 프레임(Frame)이라는 정보로 스택(Stack) 메모리의 높은 주소부터 낮은 주소로 할당한다. 함수가 종료되면 스택 프레임은 소멸되고 결괏값을 반환한다.

  • 함수의 구조
/* 대괄호([])로 감싼 내용은 생략 가능 */
fun 함수 이름([매개변수 이름: 자료형, 매개변수 이름: 자료형..]): [반환값의 자료형] {
    표현식...
    [return 반환값]
}
  • 반환값이 없는 함수: 반환값의 자료형을 Unit으로 지정한다. 생략시 반환값의 자료형을 Unit으로 추론한다.
fun 함수 이름([매개변수 이름: 자료형, 매개변수 이름: 자료형..]): [Unit] {
    표현식...
}
  • 매개변수 활용 - 기본값 사용
fun add(a: Int, b: Int = 0) = a + b

println(add(3, 5)) // 8
println(add(10)) // 10
  • 매개변수 활용 - 매개변수 이름과 함께 함수 호출
fun multiple(mul: Int = 2, number: Int) = mul * number

println(multiple(number = 13)) // 26
println(multiple(3, 13)) // 39
  • 매개변수 활용 - 가변 인자 사용
fun addition(vararg numbers: Int): Int {
    var sum = 0
    for (number in numbers) {
        sum += number
    }
    return sum
}

println(addition(1, 2)) // 3
println(addition(3, 4, 5, 6)) // 18

NOTE✏️

  • 스택 프레임(Stack Frame): 함수가 호출될 때, 그 함수가 갖는 영역으로 함수가 종료되면 소멸한다.
  • 스택 오버플로(Stack Overflow): 프로그램이 호출 스택에서 이용 가능한 공간 이상을 사용하려고 시도할 때 스택이 오버플로된다고 하며 이 경우 프로르램 충돌이 발생한다.

2. 함수형 프로그래밍

함수형 프로그래밍은 순수 함수를 작성하여 프로그램의 부작용을 줄이는 프로그래밍 기법을 말한다. 코드가 간략화되고 테스트나 재사용성이 좋아 개발 생산성이 늘어나는 장점이 있다.

함수형 프로그래밍의 정의와 특징

1. 순수 함수를 사용해야 한다.

2. 람다식을 사용할 수 있다.

3. 고차 함수를 사용할 수 있다.
  • 순수 함수(Pure Function): 어떤 함수가 같은 인자에 대하여 항상 같은 결과를 반환하여 부작용이 없는 함수를 말한다.

    순수 함수의 조건

    • 같은 인자에 대하여 항상 같은 값을 반환한다.

    • 함수 외부의 어떤 상태도 바꾸지 않는다.

/* 순수 함수 예시 */
fun sum(a: Int, b: Int) = a + b // 동일한 인자인, a와 b를 입력받아 항상 a + b를 반환(부작용이 없음)
  • 람다식(Lambda Expressions): 람다 대수에서 유래한 것으로 다음과 같은 형태이다.
{ x, y -> x + y } // 이름이 없는 함수의 형태

NOTE✏️

  • 람다 대수(lambda calculus): 이론 컴퓨터 과학 및 수리논리학에서 변수의 네임 바인딩과 대입의 방법을 이용하여 함수 정의, 함수 적용, 귀납적 함수 추상화를 수행하고 수학 연산을 표현하는 형식 체계이다. Link
  • 일급 객체(First Class Citizen): 함수형 프로그래밍에서는 함수를 일급 객체로 생각한다.

    일급 객체의 특징

    • 일급 객체는 함수의 인자로 전달할 수 있다.

    • 일급 객체는 함수의 반환값에 사용할 수 있다.

    • 일급 객체는 변수에 담을 수 있다.

  • 고차 함수(High-order Function): 다른 함수를 인자로 사용하거나 함수를 결괏값으로 반환하는 함수를 말한다.

fun highFunc(sum: (Int, Int) -> Int, a: Int, b: Int): Int = sum(a, b) // sum 매개변수는 함수

println(highFunc({ x, y -> x + y }, 10, 20)) // 람다식 함수를 인자로 넘김

3. 고차 함수와 람다식

고차 함수의 형태

  • 일반 함수를 인자나 반환값으로 사용
fun sum(a: Int, b: Int) = a + b
fun mul(a: Int, b: Int) = a * b
fun funcFunc() = sum(2, 2) // 함수의 반환값으로 함수 사용

val result1 = sum(3, 2) // 일반 인자
val result2 = mul(sum(3, 3), 3) // 인자에 함수 사용
val result3 = funcFunc()
  • 람다식을 인자나 반환값으로 사용
fun highOrder(a: Int, b: Int, sum: (Int, Int) -> Int) = sum(a, b)

val multi = { x: Int, y: Int -> x * y } // 일반 변수에 람다식 할당
val result1 = multi(10, 20) // 람다식이 할당된 편수는 함수처럼 사용 가능
val result2 = highOrder(10, 20, { x, y -> x + y }) // 람다식을 매개변수와 인자로 사용한 함수

println(result1) // 200
println(result2) // 30

람다식과 고차 함수 호출

JVM에서 실행되는 자바나 코틀린은 함수를 호출할 때 인자릐 값만 복사하는 '값의 의한 호출(Call by Value)'이 일반적이다. C/C++에서의 포인터 주소 연산이 없기 때문에 '참조에 의한 호출(Call by Reference)'은 사용되지 않는다.

  • 값에 의한 호출: 함수가 또 다른 함수의 인자로 전달될 경우 람다식 함수는 값으로 처리되어 그 즉시 함수가 수행된 후 값을 전달한다.
fun callByValue(b: Boolean): Boolean {
    println("callByValue function") // ②
    return b // b = true
}

val lambda: () -> Boolean = {
    println("lambda function") // ①
    true // 마지막 표현식의 결과가 반환
}

val result = callByValue(lambda()) // 람다식 함수를 호출
println(result) // ③
  • 이름에 의한 람다식 호출: 매개변수로 람다식을 전달하여 필요할 때 람다식 함수를 호출한다.
fun callByName(b: () -> Boolean): Boolean { // 람다식 자료형으로 선언된 매개변수
    println("callByName function") // ①
    retrun b() // 람다식 함수 호출
}

val otherLambda: () -> Boolean = {
    println("otherLambda function") // ②
    true
}

val result = callByName(otherLambda)
println(result) // ③
  • 다른 함수의 참조에 의한 일반 함수 호출: 람다식이 아닌 일반 함수를 다른 함수의 인자에서 호출하는 고차 함수의 경우 2개의 콜론(::) 기호를 사용한다.
fun sum(x: Int, y: Int) = x + y
fun funcParam(a: Int, b: Int, c: (Int, Int) -> Int): Int {
    return c(a, b)
}

val result = funcParam(3, 2, ::sum) // 인자와 반환값이 있는 함수
val likeLambda = ::sum // 일반 변수에 값처럼 할당

println(result) // 5
println(likeLambda(6, 6)) // 12

람다식의 매개변수

/* 람다식에 매개변수가 없는 경우 */
fun noParam(out: () -> String) = println(out())

noParam({ "Hello Kotlin" })
noParam { "Hello Kotlin" } // 위와 동일 결과, 소괄호 생략 가능

/* 람다식에 매개변수가 1개 있는 경우 */
fun oneParam(out: (String) -> String) = println(out("OneParam"))

oneParam({ a -> "Hello Kotlin $a" })
oneParam { "Hello Kotlin $it" } // 위와 동일 결과, 매개변수가 1개인 경우 it으로 대체 가능

/* 람다식에 매개변수가 2개 이상인 경우 */
fun moreParam(out: (String, String) -> String) = println(out("OneParam", "TwoParam"))

moreParam { a, b -> "Hello Kotlin $a $b" } // 매개변수 이름 생략 불가
moreParam { _, b -> "Hello Kotlin $b" } // 첫 번째 문자열은 사용하지 않고 생략

/* 일반 매개변수와 람다식 매개변수를 같이 사용하는 경우 */
fun withArgs(a: String, b: String, out: (String, String) -> String) = println(out(a, b))

withArgs("Arg1", "Arg2", { a, b -> "Hello Kotlin $a $b" })
// 함수의 마지막 인자가 람다식인 경우 소괄호 바깥으로 분리 가능
withArgs("Arg1", "Arg2") { a, b -> "Hello Kotlin $a $b" }

4. 코틀린의 다양한 함수 알아보기

  • 익명 함수(Anonymous Function): 일반 함수이지만 이름이 없는 것이다. 람다식에서는 return이나 break, continue처럼 제어문을 사용하기 어렵기 때문에 경우에 따라서 사용한다.
val add: (Int, Int) -> Int = fun(x, y) = x + y // 익명 함수를 사용한 add 선언
val addLikeLambda = fun(x: Int, y: Int) = x + y // 람다식 표현법과 유사
val result = add(10, 2) // add의 사용
  • 인라인 함수(Inline Function): 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣어 분기 없이 처리되기 때문에 코드의 성능을 높일 수 있다.
inline fun shortFunc(a: Int, out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}

// 인라인 함수 shortFunc()의 내용이 복사됨
shortFunc(3) { println("First call: $it") }
shortFunc(5) { println("Second call: $it") }
/* noInline 키워드를 사용하여 인라인 함수 제한 */
inline fun shortFunc(a: Int, noinline out: (Int) -> Unit) {
    println("Before calling out()")
    out(a) // 인라인으로 처리되지 않고 분기하여 호출됨
    println("After calling out()")
}
/* corssinline 키워드를 사용하여 인라인 함수 비지역 반환을 방지 */
inline fun shortFunc(a: Int, crossinline out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}

shortFunc(3) {
    println("First call: $it")
//    return // 주석을 지우면 에러 발생('return' is not allowed here)
}
  • 확장 함수(Extension Function): 코틀린은 기존의 클래스에 원하는 함수를 포함시켜 확장하는 개념을 제공한다.
// String 클래스를 확장해 getLongString() 함수 추가
fun String.getLongString(target: String) = if(this.length > target.length) this else target

val source = "Hello World!"
val target = "Kotlin"
println(source.getLongString(target)) // Hello World!
  • 중위 함수(Infix Notation): 클래스의 맴버를 호출할 때 사용하는 점(.)과 이름뒤에 소괄호를 생략하여 직관적인 형태로 표현할 수 있다.

    중위 함수의 조건

    • 멤버 메서드 또는 확장 함수여야 한다.
    • 하나의 매개변수를 가져야 한다.
    • infix 키워드를 사용하여 정의한다.
// Int를 확장해서 multiply() 함수를 하나 더 추가함
infix fun Int.multiply(x: Int) = this * x

val multi1 = 3.multiply(10) // 일반 표현법
val multi2 = 3 multiply 10 // 중위 표현법
  • 꼬리 재귀 함수(Tail Recursive Function): 재귀 함수가 꼬리를 무는 형태로 반복하여 스택 오버플로 현상을 해결할 수 있다. Link
// 꼬리 재귀를 사용한 팩토리얼 함수
tailrec fun factorial(n: Int, run: Int = 1): Long {
    return if (n == 1) run.toLong() else factorial(n-1, run * n)
}

val number = 5
println("Factorial: $number -> ${factorial(number)}") // Factorial: 5 -> 120

5. 함수와 변수의 범위

함수는 실행 블록({})을 가지고 있다. 블록 내부에 정의된 변수와 함수는 블록이 끝나면 삭제된다.

  • 함수의 범위

    • 최상위 함수(Top-level Function): 파일을 만들고 곧바로 main() 함수나 사용자 함수를 만든 경우
    • 지역 함수(Local Function): 함수 내부에 또 다른 함수가 선언되어 있는 경우
fun main() { // 최상위 함수
    ...

    fun localFunc() { // 지역 함수
        ...
    }
}

fun userFunc() { // 최상위 함수
    ...
}
  • 변수의 범위

    • 전역 변수(Global Variable): 최상위에 있는 변수, 프로그램이 실행되는 동안 메모리에서 삭제되지 않는다.
    • 지역 변수(Local Variable): 특정 코드 블록 안에 있는 변수, 코드 블록을 벗어나면 메모리에서 삭제된다.
var global = 10 // 패키지 안의 모든 범위에서 적용되는 전역 변수

fun main() {
    val local = 20 // main() 함수 블록 안에서만 유지되는 지역변수
}

fun outsideFunc() {
    global += 1
//     local += 1 // main() 함수의 지역 변수에 접근 불가(Unresolved reference)
}