Developing Myself Everyday

함수 정의


함수를 정의하려면 함수의 입력값인 매개변수(파라미터), 함수의 출력값인 반환 자료형, 실제 함수의 기능을 처리하는 코드 블록 등을 잘 정의해야 합니다.

 

 

매개변수와 반환값이 없는 함수

가장 기본적인 함수입니다. 상태를 변경하거나 어떤 동작을 수행하고 반환할 값이 없는 경우에 사용합니다.

// fun simpleFunction(): Unit {
fun simpleFunction() {
    println("This is a simple function.")
}

반환값이 없는 함수는 반환값이 없음을 나타내는 `Unit` 자료형을 지정합니다. 이는 Java의 `void`와 유사한 기능을 합니다. 반환값이 없을 경우 `Unit`은 생략 가능합니다.

 

 

매개변수를 받는 함수

매개변수로 전달된 데이터를 처리하거나 조작해야 할 때 사용합니다.

fun greetUser(name: String) {
    println("Hello, $name!")
}

 

 

반환값이 있는 함수

계산 결과를 반환해야 할 때 사용합니다.

fun addNumbers(a: Int, b: Int): Int {
    return a + b
}

 

 

단일 표현식 함수

함수 내용이 단순하고 간단한 경우에 코드를 더 간결하게 작성할 수 있습니다.

// fun multiplyNumbers(a: Int, b: Int): Int {
//    return a * b
// }

fun multiplyNumbers(a: Int, b: Int) = a * b

단일 표현식은 = 다음에 return할 표현식을 작성합니다. 단일 표현식을 사용할 경우에는 반환 자료형을 생략할 수 있습니다.

 

 

기본 인자값을 가지는 함수

어떤 매개변수가 자주 반복되는 경우 기본 인자값을 사용해 호출을 간소화할 수 있습니다.

fun showMessage(message: String = "Default message") {
    println(message)
}

인수를 호출할 때 기본 인자값이 있는 인자를 생략해도 됩니다.

 

 


매개변수의 값은 수정할 수 없습니다. 함수는 입력값을 받아서 처리하여 결과를 반환하는 `블랙 박스`로서 동작하는 것이 이상적입니다. 그렇기 때문에 함수의 매개변수의 값은 수정하지 않고 입력을 처리하여 결과를 반환해야 합니다.

 

위의 함수들을 출력한 결과는 다음과 같습니다.

fun main() {
    // 함수 호출
    simpleFunction() // 출력: This is a simple function.

    val name = "준형"
    greetUser(name) // 출력: Hello, 준형!

    val sum = addNumbers(10, 20)
    println("합 - $sum") // 출력: 합 - 30

    val mul = multiplyNumbers(5, 6)
    println("곱: $mul") // 출력: 곱 - 30

    showMessage() // 출력: Default message (기본 인자값으로 호출)
    showMessage("Custom message") // 출력: Custom message (커스텀 인자값으로 호출)
}

 

 

함수 호출

함수는 동일한 이름으로 여러 개의 함수를 정의할 수 있습니다. 이 이유는 함수의 이름이 같더라도 매개변수와 반환값이 다르면 다른 함수로 식별하기 때문입니다. 그래서 우리는 함수를 호출할 때 정확한 함수를 호출하기 위해 매개변수와 함수 호출인자를 이해해야 합니다.

 

fun main() {
    val sum1 = addNumbers(10, 20)
    val sum2 = addNumbers(a = 10, b = 20)
    val sum3 = addNumbers(b = 10, a = 20)
}

 

인자는 매개변수를 지정한 위치로 지정하거나 매개변수 이름과 인자를 쌍으로 지정해서 함수를 호출할 수 있습니다.

 

 

가변인자(Varargs)

가변인자는 함수를 호출할 때 인자의 개수를 유동적으로 받아들일 수 있도록 하는 Kotlin의 기능입니다. 함수 정의에서 매개변수 이름 앞에 `vararg` 키워드를 붙여 사용합니다.

fun printNumbers(vararg numbers: Int) {
    for (number in numbers) {
        print("$number ")
    }
    println()
}

 

위 메서드는 가변인자로 정수들을 받습니다. 함수 호출 시 인자로 여러개의 정수를 전달할 수 있게 됩니다. 또한 배열을 가변인자로 전달할 수도 있습니다. 이때는 스프레드 연산자인 `*`를 배열 앞에 붙여서 전달합니다.

 

fun main() {
    // 가변인자를 사용하여 함수 호출
    printNumbers(1, 2, 3) // 출력: 1 2 3
    printNumbers(10, 20, 30, 40, 50) // 출력: 10 20 30 40 50

    // 배열을 가변인자로 전달
    val numArray = intArrayOf(100, 200, 300)
    printNumbers(*numArray) // 출력: 100 200 300
}

 

 

 

가변 객체를 함수의 인자로 전달할 때 주의사항

가변 객체를 함수의 인자로 전달하면 가변 객체 내부의 값을 변경할 수 있습니다. 다만 함수를 정의할 때 매개변수에 가변 매개변수를 지정하면 가변 리스트를 인자로 전달할 경우 함수 밖에 지정한 리스트도 같이 변경됩니다. 예를 들어보겠습니다.

fun modifyList(list: List<Int>): List<Int> {
    return list + listOf(5)
}

fun modifyMutableList(mutableList: MutableList<Int>): MutableList<Int> {
    mutableList.add(5)
    return mutableList
}

fun main() {
    val myList = listOf(1, 2, 3, 4)
    println("modifyList 호출 이전: $myList") // 출력: modifyList 호출 이전: [1, 2, 3, 4]

    val modifyList = modifyList(myList)
    println("modifyList 호출 이후: $myList") // 출력: modifyList 호출 이후: [1, 2, 3, 4]
    println("modifyList 호출 이후: $modifyList") // 출력: modifyList 호출 이후: [1, 2, 3, 4, 5]

    val myMutableList = mutableListOf(1, 2, 3, 4)
    println("modifyMutableList 호출 이전: $myMutableList") // 출력: modifyMutableList 호출 이전: [1, 2, 3, 4]

    val modifyMutableList = modifyMutableList(myMutableList)
    println("modifyMutableList 호출 이후: $myMutableList") // 출력: modifyMutableList 호출 이후: [1, 2, 3, 4, 5]
    println("modifyMutableList 호출 이후: $modifyMutableList") // 출력: modifyMutableList 호출 이후: [1, 2, 3, 4, 5]
}

 

list는 기본적으로 변경할 수 없는 객체이고 mutableList는 변경할 수 있는 list입니다.

 

예시에서 modifyList 함수는 List를 인자로 받고, 해당 List에 5를 추가한 새로운 List를 반환합니다. 이렇게 반환된 새로운 List는 함수 외부에서 변경되지 않고 유지됩니다. 반면에 modifyMutableList 함수는 MutableList를 인자로 받고, 해당 MutableList에 5를 추가하고 그대로 반환합니다. 함수 호출 이후에도 원본 MutableList가 변경된 상태를 유지합니다.

 

 

 

익명 함수와 람다 표현식


익명 함수

익명 함수(Anonymous Function)는 이름 그대로 '이름이 없는 함수' 를 말합니다. 함수를 선언할 때 이름이 없으며, 함수가 정의되는 위치에서 바로 사용됩니다. 익명 함수는 일회성으로 사용되거나 다른 함수의 인자로 전달하는데 사용됩니다.

// 익명 함수 예시
val sum = fun(x: Int, y: Int): Int {
    return x + y
}

fun main() {
    val result = sum(10, 5) // 출력: 15
    println(result)
}

 

 

람다 표현식

람다(Lambda)라는 말은 상수를 의미합니다. 코틀린에서 람다 표현식은 상수처럼 함수를 의미합니다.

 

익명 함수는 다른 함수의 인자로 전달하는데 유용하게 사용됩니다. 이때 람다 표현식을 사용하면 익명 함수를 간단하게 표현할 수 있습니다. 위의 익명 함수를 간단한 형태의 람다 표현식으로 바꿔 보겠습니다.

val sum: (Int, Int) -> Int = { x, y -> x + y }

 

위처럼 람다 표현식은 중괄호로 묶인 코드 블록이며, 인자를 받아서 처리한 뒤 반환 값을 반환합니다.

 

 

고차 함수(Higher-order Function)에서 람다 표현식을 활용

위에서 계속 말했던 함수의 인자로 함수를 전달하는 함수를 고차 함수라고 합니다. 고차 함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수를 말합니다. 람다 표현식을 고차 함수와 함께 사용하면 코드를 더욱 간결하고 가독성 있게 작성할 수 있습니다.

// 고차 함수 예시: 함수를 인자로 받는 함수
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

val sum: (Int, Int) -> Int = { x, y -> x + y }

val multiply: (Int, Int) -> Int = { x, y -> x * y }

fun main() {
    val resultSum = calculate(10, 5, sum) // 출력: 15
    println("Result of sum: $resultSum") 

    val resultMultiply = calculate(10, 5, multiply) // 출력: 50
    println("Result of multiply: $resultMultiply")
}

 

위의 예제에서 calculate 함수는 세 개의 인자를 받습니다. 첫 번째와 두 번째 인자 a와 b는 정수형입니다. 세 번째 인자 operation은 두 개의 정수를 받아 정수를 반환하는 함수입니다. calculate 함수 내에서 operation 함수를 호출하여 두 수를 연산하고 결과를 반환합니다.

 

sum과 multiply는 두 개의 정수를 받아서 각각 덧셈과 곱셈 연산을 수행하는 람다 표현식입니다. 이 람다 표현식을 calculate 함수의 인자로 전달하여 두 수를 더하거나 곱한 결과를 구할 수 있습니다.

 

 

고차 함수에서 함수 참조를 사용

고차 함수에서 함수의 인자로 함수를 전달할 때 함수 참조를 이용할 수도 있습니다. 함수 참조는 함수명 뒤에 더블콜론(::)을 붙여서 함수의 이름 자체를 가리켜서 참조합니다. 이렇게 되면 함수 참조로 로딩된 함수의 레퍼런스를 가져올 수 있습니다.

 

// 함수 선언
fun add(a: Int, b: Int): Int {
    return a + b
}

// 1. 함수명 뒤에 더블 콜론(::)을 붙여 함수 참조 생성
val addFunctionReference = ::add

// 2. 람다 표현식을 사용하여 함수 참조 생성
val addLambdaReference: (Int, Int) -> Int = { a, b -> add(a, b) }

fun operateOnNumbers(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

val resultUsingFunctionRef = operateOnNumbers(5, 3, addFunctionReference) // 결과: 8
val resultUsingLambdaRef = operateOnNumbers(5, 3, addLambdaReference) // 결과: 8

 

 

 

스코프(Scope)


스코프는 프로그래밍에서 변수와 함수의 유효 범위를 나타내는 개념입니다. 즉, 변수와 함수가 어디서 선언되었고 어디까지 접근할 수 있는지를 결정하는 규칙입니다.

 

코틀린은 렉시컬 스코프(Lexical Scope)라는 스코핑 방식을 따르고 있습니다. 렉시컬 스코프는 변수와 함수의 유효 범위를 정하는 규칙으로, 변수나 함수를 어디에서 선언했는지에 따라 결정됩니다.

 

이런 규칙으로 코틀린에서는 주로 두 가지 유형의 변수 스코프가 있습니다.

 

1. 전역 스코프 (Global Scope)

전역 스코프는 프로그램의 모든 범위에서 접근 가능한 변수를 말합니다. 코틀린에서는 함수 바깥에 선언된 변수들이 전역 스코프에 속합니다. 전역 변수는 프로그램의 어느 곳에서나 접근 가능하며, 프로그램이 실행되는 동안 존재합니다.

 

2. 지역 스코프 (Local Scope)

지역 스코프는 함수 내부에서 선언된 변수를 말합니다. 함수 내에서 선언된 변수들은 해당 함수의 블록 내에서만 접근 가능하며, 함수가 실행되는 동안에만 존재합니다.

 

 

클로저(Closure)

람다 표현식를 한번 보겠습니다. 아래는 입력 매개변수의 값을 두 배로 출력하는 람다 표현식입니다.

​ ​val​ doubleIt = { e: Int -> e * 2 }

 

때때로 우리는 람다 표현식의 출력을 외부 상태에 의존하고 싶을 수가 있습니다. 이런 환경을 우리는 클로저라고 합니다. 그렇게 부르는 이유는 클로저는 지역 스코프가 아닌 속성 및 메서드에 바인딩하기 위해 람다가 선언된 범위를 닫아서 외부 상태를 포획하고 사용할 수 있게 하기 때문입니다. 클로저는 외부 범위에 있는 변수들을 자신의 렉시컬 스코프에 포함시키는 것으로, 람다 내에서 이 변수들을 자유롭게 사용할 수 있게 해줍니다.

 

이제 람다 표현식을 클로저로 바꿔보겠습니다.

​ ​val​ factor = 2
​ 
​ ​val​ doubleIt = { e: Int -> e * factor }

 

위의 예제에서 e는 매개변수이지만, `factor`란 변수는 지역 변수가 아닙니다. 컴파일러는 클로저의 본문이 정의된 범위에서 이 변수를 찾아야 합니다. 그곳에서 찾지 못한다면 컴파일러는 더 바깥쪽의 정의 범위에서 계속해서 탐색합니다. 이러한 과정을 렉시컬 스코핑(Lexical Scoping)이라고 합니다.

 

 

아래의 코드를 한번 자세히 보겠습니다.

fun predicateOfLength(length: Int): (String) -> Boolean {
    return { input: String -> input.length == length }
}

여기서 주목해야 할 점은, 람다 본문에서 사용된 length 변수는 람다 자체의 매개변수가 아닌 외부 함수인 predicateOfLength 함수의 매개변수입니다. 따라서 length는 람다의 렉시컬 스코프에 포획된 클로저의 개념입니다. 클로저는 람다 내부에서 length 변수를 사용하면, 람다는 predicateOfLength 함수의 렉시컬 스코프에서 length 변수를 탐색하여 사용합니다.

 

 

 

클로저의 불변성

이전 예시에서 변수 factorval로 정의되어 불변성을 가지고 있었습니다. 만약 var로 변경하여 변경 가능한 변수로 만든다면, 클로저 내부에서 factor를 수정할 수 있습니다. Kotlin 컴파일러는 이 경우에 경고를 발생시키지 않지만, 결과는 예상치 못하게 나올 수 있으며, 최소한의 경우에는 코드를 이해하는데 혼란을 줄 수 있습니다. 아래 예시를 한번 보겠습니다.

fun main() {
    var  factor = 2

    val  doubled = listOf(1, 2).map { it * factor }
    val  doubledAlso = sequenceOf(1, 2).map { it * factor }

    factor = 0

    println(doubled) // 출력: [2, 4]
    println(doubledAlso.toList()) //출력: [0, 0]
}

시퀀스란 개념은 나중에 배우기에 간단하게만 설명하자면 시퀀스는 리스트와 내부 반복과 지연 평가에서 차이점이 있습니다. 두 컬렉션을 변환하는 코드의 구조가 매우 유사함에도 불구하고 리스트와 시퀀스는 다른 동작을 하고 있습니다. 이를 통해서 알 수 있는건 클로저 내에서 가변 변수를 사용하는 것은 오류의 원인이 되므로 피해야 한다는 것입니다. 혼동을 피하고 오류를 최소화하기 위해 클로저는 순수한 함수로 유지해야 합니다.

 

 

 

함수 자료형 알아보기


함수 자료형

함수도 1급 객체이기 때문에 자료형을 사용해서 변수에 할당하거나 반환값으로 사용할 수 있습니다.

 


1급 객체(First-class object)는 다음의 특징을 가진 객체를 말합니다.

 1. 변수에 할당 가능: 1급 객체는 변수에 할당할 수 있습니다. 즉, 함수를 변수에 대입하여 저장하거나 다른 변수에 전달할 수 있습니다.
 2. 함수의 인자로 전달 가능: 함수를 다른 함수의 인자로 전달할 수 있습니다. 이렇게 전달된 함수는 호출되어 실행될 수 있습니다.
 3. 함수의 반환값으로 사용 가능: 함수가 다른 함수의 반환값으로 사용될 수 있습니다.
 4. 데이터 구조에 저장 가능: 1급 객체는 배열, 리스트, 맵 등과 같은 데이터 구조에 저장할 수 있습니다.
 5. 익명 함수(람다)로 표현 가능: 함수를 익명 함수 형태로 표현할 수 있습니다.

 

함수 자료형을 사용한 예시는 아래와 같습니다.

fun main() {
    // 매개변수와 반환값이 Unit인 함수
    val a: () -> Unit = { println("함수") }

    // 하나의 매개변수와 반환값이 Int인 익명함수
    val b: (Int) -> Int = fun(x: Int): Int {
        return x * 3
    }

    // 두 개의 매개변수와 반환값이 Int인 함수
    fun c(x: Int, y: Int): Int = x + y
    
    // c의 함수 참조
    val d: (Int, Int) -> Int = ::c
    
    a() // 출력: 함수
    println(b(10)) // 출력: 30
    println(c(10, 20)) // 출력: 30
    println(d(10, 20)) // 출력: 30
}

 

 

Null이 가능한 함수 자료형

Null아무런 값을 가지고 있지 않은 상태를 나타냅니다. 코틀린에서는 자바와 달리 모든 변수의 기본 타입이 Null이 될 수 있는 타입입니다.

 

코틀린에서 Null이 될 수 있는 변수를 선언할 때는 타입 뒤에 물음표(?)를 붙여야 합니다. 예를 들어, String?, Int?, Boolean? 등은 Null이 될 수 있는 변수를 선언하는 방법입니다. 반면에 일반적인 변수는 Null이 될 수 없으며, Null이 될 수 있는 변수를 사용할 때는 Null 체크를 통해 안전하게 다룰 수 있습니다.

 

Null에 대해선 6장에서 자세하게 공부합니다.

 

 

invoke()

Null을 처리하기 위해서는 안전호출연산(?.) 처리가 필요합니다. Null이 될 수 있는 변수는 안전호출연산을 붙이는 것으로 호출할 수 있지만 Null이 될 수 있는 함수 자료형을 호출하려면 invoke() 메서드가 필요합니다.

 

invoke() 메서드가 실행되면 함수의 반환 자료형에 속하는 객체를 반환합니다. 

 

아래는 invoke() 메서드를 사용한 예시입니다.

// 널이 될 수 있는 함수 타입
var nullableFunction: ((Int, Int) -> Int)? = { a, b -> a + b }

fun main() {
    val result = nullableFunction?.invoke(10, 5)
    println("Result: $result") // 출력: Result: 15
}

 

일반 함수의 경우에는 단순히 함수의 이름과 인자를 사용하여 호출해야 합니다. invoke() 메서드는 주로 함수 타입을 가진 변수나 프로퍼티, 람다 표현식 등과 같은 경우에 사용되는 메서드입니다.

fun add(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    val result = add.invoke(5, 3) // 오류! 일반 함수는 invoke()로 호출할 수 없음
    println("Result: $result")
}

 

 

함수 오버로딩 (Function Overloading)

함수 오버로딩(Function Overloading)은 같은 이름을 가진 함수가 매개변수의 타입, 개수, 순서가 다르게 정의되는 것을 의미합니다. 즉, 동일한 함수 이름으로 여러 개의 버전을 가질 수 있도록 하는 기능입니다.

 

 

클래스에 연산자 오버로딩

클래스에 연산자 오버로딩을 하는 방법은 operator 키워드를 사용하여 해당 연산자에 대한 메서드를 오버로딩하는 것입니다. invoke() 메서드도 연산자이기 때문에 연산자 오버로딩을 할 수 있습니다.

 

아래는 invoke() 메서드와 plus() 메서드를 오버로딩하여 카운터를 구현한 예시입니다.

class Counter(private var value: Int = 0) {
    operator fun invoke(): Int {
        return value
    }

    operator fun plus(increment: Int): Counter {
        value += increment
        return this
    }
}

fun main() {
    var counter = Counter()
    println(counter()) // 출력: 0
    counter += 5
    println(counter()) // 출력: 5
}

 

 

Object에 연산자 오버로딩

object 표현식에서도 연산자 오버로딩을 할 수 있습니다. 

val a = object : (Int, Int) -> Int {
    override fun invoke(p1: Int, p2: Int): Int {
        return p1 + p2
    }
}

fun main() {
    val result = a(3, 5)
    println(result) // 출력: 8
}

 

 

람다와 invoke()

람다는 컴파일되면서 코틀린에 정의된 FunctionN(P1, .. ,PN, R) 형태로 변환됩니다. 그러면서 럼파일 시점에 

 invoke() 메서드를 생성합니다. 아래의 예시를 보겠습니다.

fun main() {
    val lambda: (Int, Int) -> Int = { a, b -> a + b }

    val result = lambda(5, 3)
    println("Result: $result") // 출력: Result: 8
}

 

위 코드는 두 개의 정수를 더하는 람다 표현식을 정의하고, 이를 lambda 변수에 할당하여 사용합니다. 람다 표현식의 타입은 (Int, Int) -> Int로 두 개의 정수를 받아서 정수를 반환하는 함수 타입입니다.

 

컴파일 시에 람다 표현식은 컴파일러에 의해 아래와 같이 invoke 메서드로 변환됩니다:

fun main() {
    val lambda: (Int, Int) -> Int = object : Function2<Int, Int, Int> {
        override fun invoke(a: Int, b: Int): Int = a + b
    }

    val result = lambda.invoke(5, 3)
    println("Result: $result") // 출력: Result: 8
}

 

 

 

Reference

 

Closures and Lexical Scoping

Programming Kotlin — by Venkat Subramaniam (84 / 173)

medium.com

 

코틀린 invoke 함수(람다의 비밀) · 쾌락코딩

코틀린 invoke 함수(람다의 비밀) 21 Mar 2019 | kotlin invoke operator invoke 란? 코틀린에는 invoke라는 특별한 함수, 정확히는 연산자가 존재한다. invoke연산자는 이름 없이 호출될 수 있다. 이름 없이 호출된

wooooooak.github.io

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!