함수

함수 (Functions)

Kotlin에서는 fun 키워드를 사용해 나만의 함수를 선언할 수 있어요.

fun hello() {
    return println("Hello, world!")
}

fun main() {
    hello()
    // Hello, world!
}

Kotlin에서:

  • 함수 매개변수는 괄호 () 안에 작성해요.
  • 각 매개변수는 타입을 가져야 하고, 여러 매개변수는 쉼표 ,로 구분해요.
  • 반환 타입은 함수의 괄호 () 뒤에, 콜론 :으로 구분해 작성해요.
  • 함수의 본문은 중괄호 {} 안에 작성해요.
  • return 키워드는 함수에서 빠져나오거나 어떤 것을 반환할 때 사용해요.

Note: 함수가 유용한 것을 반환하지 않는다면 반환 타입과 return 키워드는 생략할 수 있어요. 이에 대한 자세한 내용은 Functions without return 문서를 참고하세요.

다음 예시에서:

  • xy는 함수 매개변수예요.
  • xyInt 타입이에요.
  • 함수의 반환 타입은 Int예요.
  • 함수는 호출됐을 때 xy의 합을 반환해요.
fun sum(x: Int, y: Int): Int {
    return x + y
}

fun main() {
    println(sum(1, 2))
    // 3
}

Note: 코딩 컨벤션에서 함수 이름은 소문자로 시작하고 밑줄 없이 카멜 케이스(camel case)를 사용할 것을 권장해요.

출처: Kotlin 공식 문서

본문

명명된 인자 (Named arguments)

간결한 코드를 위해 함수를 호출할 때 매개변수 이름을 꼭 포함할 필요는 없어요. 하지만 매개변수 이름을 포함하면 코드를 더 읽기 쉽게 만들어 줘요. 이것을 명명된 인자(named argument) 사용이라고 해요. 매개변수 이름을 포함한다면 어떤 순서로든 매개변수를 쓸 수 있어요.

Tip: 다음 예시에서 문자열 템플릿($)을 사용해서 매개변수 값에 접근하고, String 타입으로 변환한 다음 출력하기 위해 하나의 문자열로 연결해요.

fun printMessageWithPrefix(message: String, prefix: String) {
    println("[$prefix] $message")
}

fun main() {
    // Uses named arguments with swapped parameter order
    printMessageWithPrefix(prefix = "Log", message = "Hello")
    // [Log] Hello
}

기본 매개변수 값 (Default parameter values)

함수 매개변수에 기본값을 정의할 수 있어요. 기본값이 있는 매개변수는 함수를 호출할 때 생략할 수 있어요. 기본값을 선언하려면 타입 뒤에 대입 연산자 =를 사용해요:

fun printMessageWithPrefix(message: String, prefix: String = "Info") {
    println("[$prefix] $message")
}

fun main() {
    // Function called with both parameters
    printMessageWithPrefix("Hello", "Log") 
    // [Log] Hello
    
    // Function called only with message parameter
    printMessageWithPrefix("Hello")        
    // [Info] Hello
    
    printMessageWithPrefix(prefix = "Log", message = "Hello")
    // [Log] Hello
}

Note: 기본값이 있는 특정 매개변수만 건너뛸 수 있어요. 다만 첫 번째로 건너뛴 매개변수 이후로는 모든 매개변수를 반드시 이름으로 지정해야 해요.

반환 없는 함수 (Functions without return)

함수가 유용한 값을 반환하지 않는다면 그 반환 타입은 Unit이에요. Unit은 값을 하나만 가진 타입이에요 – Unit이죠. 함수 본문에서 Unit을 반환한다고 명시적으로 선언할 필요는 없어요. 즉 return 키워드를 쓰거나 반환 타입을 선언할 필요가 없다는 뜻이에요:

fun printMessage(message: String) {
    println(message)
    // `return Unit` or `return` is optional
}

fun main() {
    printMessage("Hello")
    // Hello
}

단일 표현식 함수 (Single-expression functions)

코드를 더 간결하게 만들려면 단일 표현식 함수(single-expression function)를 사용할 수 있어요. 예를 들어 sum() 함수는 다음과 같이 짧게 만들 수 있어요:

fun sum(x: Int, y: Int): Int {
    return x + y
}

fun main() {
    println(sum(1, 2))
    // 3
}

중괄호 {}를 제거하고 대입 연산자 =로 함수 본문을 선언할 수 있어요. 대입 연산자 =를 사용하면 Kotlin이 타입 추론을 사용하므로 반환 타입도 생략할 수 있어요. 그러면 sum() 함수가 한 줄이 돼요:

fun sum(x: Int, y: Int) = x + y

fun main() {
    println(sum(1, 2))
    // 3
}

하지만 다른 개발자들이 코드를 빨리 이해하길 원한다면, 대입 연산자 =를 사용할 때도 반환 타입을 명시적으로 정의하는 게 좋아요.

Note: {} 중괄호로 함수 본문을 선언할 때는, Unit 타입이 아니라면 반환 타입을 반드시 선언해야 해요.

함수에서 조기 반환 (Early returns in functions)

함수의 코드가 특정 지점을 넘어 처리되지 않도록 하려면 return 키워드를 사용해요. 이 예시는 조건식이 true로 평가되면 함수에서 조기 반환(early return)하도록 if를 사용해요:

// A list of registered usernames
val registeredUsernames = mutableListOf("john_doe", "jane_smith")

// A list of registered emails
val registeredEmails = mutableListOf("[email protected]", "[email protected]")

fun registerUser(username: String, email: String): String {
    // Early return if the username is already taken
    if (username in registeredUsernames) {
        return "Username already taken. Please choose a different username."
    }

    // Early return if the email is already registered
    if (email in registeredEmails) {
        return "Email already registered. Please use a different email."
    }

    // Proceed with the registration if the username and email are not taken
    registeredUsernames.add(username)
    registeredEmails.add(email)

    return "User registered successfully: $username"
}

fun main() {
    println(registerUser("john_doe", "[email protected]"))
    // Username already taken. Please choose a different username.
    println(registerUser("new_user", "[email protected]"))
    // User registered successfully: new_user
}

함수 연습 (Functions practice)

연습 1 (Exercise 1)

원의 반지름을 정수 형식의 매개변수로 받아 그 원의 넓이를 출력하는 circleArea라는 함수를 작성하세요.

Tip: 이 연습에서는 PI를 통해 값에 접근할 수 있도록 패키지를 import해요. 패키지 import에 대한 자세한 내용은 Packages and imports 문서를 참고하세요.

Hint: 원의 넓이를 계산하는 공식은 면적 = PI × 반지름 × 반지름이며, 여기서 반지름이 r이에요.

import kotlin.math.PI

// Write your code here

fun main() {
    println(circleArea(2))
}
import kotlin.math.PI

fun circleArea(radius: Int): Double {
    return PI * radius * radius
}

fun main() {
    println(circleArea(2)) // 12.566370614359172
}

연습 2 (Exercise 2)

앞선 연습의 circleArea 함수를 단일 표현식 함수로 다시 작성하세요.

import kotlin.math.PI

// Write your code here

fun main() {
    println(circleArea(2))
}
import kotlin.math.PI

fun circleArea(radius: Int): Double = PI * radius * radius

fun main() {
    println(circleArea(2)) // 12.566370614359172
}

연습 3 (Exercise 3)

시간, 분, 초 단위로 주어진 시간 간격을 초로 변환하는 함수가 있어요. 대부분의 경우 함수 매개변수 한두 개만 넘기면 되고 나머지는 0이에요. 기본 매개변수 값과 명명된 인자를 사용해 함수와 그 호출 코드를 개선해서 더 읽기 쉽게 만드세요.

fun intervalInSeconds(hours: Int, minutes: Int, seconds: Int) =
    ((hours * 60) + minutes) * 60 + seconds

fun main() {
    println(intervalInSeconds(1, 20, 15))
    println(intervalInSeconds(0, 1, 25))
    println(intervalInSeconds(2, 0, 0))
    println(intervalInSeconds(0, 10, 0))
    println(intervalInSeconds(1, 0, 1))
}
fun intervalInSeconds(hours: Int = 0, minutes: Int = 0, seconds: Int = 0) =
    ((hours * 60) + minutes) * 60 + seconds

fun main() {
    println(intervalInSeconds(1, 20, 15))
    println(intervalInSeconds(minutes = 1, seconds = 25))
    println(intervalInSeconds(hours = 2))
    println(intervalInSeconds(minutes = 10))
    println(intervalInSeconds(hours = 1, seconds = 1))
}

람다 표현식 (Lambda expressions)

Kotlin은 람다 표현식(lambda expression)을 사용해 함수에 대해 더욱 간결한 코드를 작성할 수 있게 해 줘요.

예를 들어 다음 uppercaseString() 함수는:

fun uppercaseString(text: String): String {
    return text.uppercase()
}
fun main() {
    println(uppercaseString("hello"))
    // HELLO
}

다음처럼 람다 표현식으로도 작성할 수 있어요:

fun main() {
    val upperCaseString = { text: String -> text.uppercase() }
    println(upperCaseString("hello"))
    // HELLO
}

람다 표현식은 처음 보면 이해하기 어려울 수 있으니 하나씩 풀어 볼게요. 람다 표현식은 중괄호 {} 안에 작성해요.

램다 표현식 안에는 이렇게 작성해요:

  • -> 뒤에 매개변수를 작성해요.
  • -> 뒤에 함수 본문을 작성해요.

앞선 예시에서:

  • text는 함수 매개변수예요.
  • textString 타입이에요.
  • 함수는 text에 호출한 .uppercase() 함수의 결과를 반환해요.
  • 전체 람다 표현식은 대입 연산자 =upperCaseString 변수에 할당돼요.
  • 람다 표현식은 upperCaseString 변수를 함수처럼 사용하고 문자열 "hello"를 매개변수로 넘겨 호출해요.
  • println() 함수가 결과를 출력해요.
{ println("Log message") }

Note: 매개변수 없는 람다를 선언한다면 ->를 사용할 필요가 없어요. 예를 들어:

람다 표현식은 여러 방식으로 사용할 수 있어요. 다음과 같이 할 수 있죠:

  • 람다 표현식을 다른 함수의 매개변수로 전달하기
  • 함수에서 람다 표현식 반환하기
  • 람다 표현식을 단독으로 호출하기

다른 함수에 전달하기 (Pass to another function)

람다 표현식을 함수에 전달하는 것이 유용한 대표적인 예는 컬렉션에서 .filter() 함수를 사용하는 경우예요:

fun main() {
    //sampleStart
    val numbers = listOf(1, -2, 3, -4, 5, -6)
    
    val positives = numbers.filter ({ x -> x > 0 })
    
    val isNegative = { x: Int -> x < 0 }
    val negatives = numbers.filter(isNegative)
    
    println(positives)
    // [1, 3, 5]
    println(negatives)
    // [-2, -4, -6]
    //sampleEnd
}

.filter() 함수는 람다 표현식을 predicate(술어)로 받아 리스트의 각 요소에 적용해요. 함수는 predicate가 true를 반환할 때만 요소를 유지해요:

  • { x -> x > 0 }은 요소가 양수면 true를 반환해요.
  • { x -> x < 0 }은 요소가 음수면 true를 반환해요.

이 예시는 람다 표현식을 함수에 전달하는 두 가지 방식을 보여 줘요:

  • 양수의 경우, 예시는 .filter() 함수에 람다 표현식을 직접 추가해요.
  • 음수의 경우, 예시는 람다 표현식을 isNegative 변수에 할당해요. 그리고 isNegative 변수를 .filter() 함수의 함수 매개변수로 사용해요. 이 경우 람다 표현식에서 함수 매개변수(x)의 타입을 지정해야 해요.
val positives = numbers.filter { x -> x > 0 }

Note: 람다 표현식이 유일한 함수 매개변수라면 함수 괄호 ()를 생략할 수 있어요: 이것은 후행 람다(trailing lambda)의 예시이며, 이 장의 마지막에서 더 자세히 다뤄요.

또 다른 좋은 예는 컬렉션의 항목을 변환하는 .map() 함수를 사용하는 경우예요:

fun main() {
    //sampleStart
    val numbers = listOf(1, -2, 3, -4, 5, -6)
    val doubled = numbers.map { x -> x * 2 }
    
    val isTripled = { x: Int -> x * 3 }
    val tripled = numbers.map(isTripled)
    
    println(doubled)
    // [2, -4, 6, -8, 10, -12]
    println(tripled)
    // [3, -6, 9, -12, 15, -18]
    //sampleEnd
}

.map() 함수는 람다 표현식을 변환 함수(transform function)로 받아요:

  • { x -> x * 2 }은 리스트의 각 요소를 가져와 그 요소에 2를 곱한 값을 반환해요.
  • { x -> x * 3 }은 리스트의 각 요소를 가져와 그 요소에 3을 곱한 값을 반환해요.

함수 타입 (Function types)

함수에서 람다 표현식을 반환하기 전에 먼저 함수 타입(function type)을 이해해야 해요.

기본 타입에 대해 이미 배웠지만 함수 자체도 타입을 가져요. Kotlin의 타입 추론은 매개변수 타입에서 함수의 타입을 추론할 수 있어요. 하지만 함수 타입을 명시적으로 지정해야 하는 경우가 있을 수 있어요. 컴파일러는 해당 함수에 무엇이 허용되고 허용되지 않는지 알기 위해 함수 타입이 필요해요.

함수 타입의 구문은 다음과 같아요:

  • 각 매개변수의 타입을 괄호 () 안에 쓰고 쉼표 ,로 구분해요.
  • -> 뒤에 반환 타입을 작성해요.

예를 들어: (String) -> String 또는 (Int, Int) -> Int.

upperCaseString()에 대한 함수 타입이 정의된 경우 람다 표현식은 다음과 같아요:

val upperCaseString: (String) -> String = { text -> text.uppercase() }

fun main() {
    println(upperCaseString("hello"))
    // HELLO
}

람다 표현식에 매개변수가 없다면 괄호 ()를 비워 두면 돼요. 예를 들어: () -> Unit

Note: 매개변수와 반환 타입은 람다 표현식 안에서든 함수 타입으로든 반드시 선언해야 해요. 그렇지 않으면 컴파일러는 람다 표현식의 타입을 알 수 없어요. 예를 들어 다음은 작동하지 않아요: val upperCaseString = { str -> str.uppercase() }

함수에서 반환하기 (Return from a function)

람다 표현식은 함수에서 반환될 수 있어요. 컴파일러가 반환된 람다 표현식의 타입을 이해하도록 함수 타입을 선언해야 해요.

다음 예시에서 toSeconds() 함수는 (Int) -> Int 타입이에요. 항상 Int 타입의 매개변수를 받고 Int 값을 반환하는 람다 표현식을 반환하기 때문이에요.

이 예시는 toSeconds()가 호출됐을 때 어떤 람다 표현식을 반환할지 결정하는 데 when 표현식을 사용해요:

fun toSeconds(time: String): (Int) -> Int = when (time) {
    "hour" -> { value -> value * 60 * 60 }
    "minute" -> { value -> value * 60 }
    "second" -> { value -> value }
    else -> { value -> value }
}

fun main() {
    val timesInMinutes = listOf(2, 10, 15, 1)
    val min2sec = toSeconds("minute")
    val totalTimeInSeconds = timesInMinutes.map(min2sec).sum()
    println("Total time is $totalTimeInSeconds secs")
    // Total time is 1680 secs
}

단독으로 호출하기 (Invoke separately)

람다 표현식은 중괄호 {} 뒤에 괄호 ()를 붙이고 그 안에 매개변수를 넣어 단독으로 호출할 수 있어요:

fun main() {
    //sampleStart
    println({ text: String -> text.uppercase() }("hello"))
    // HELLO
    //sampleEnd
}

후행 람다 (Trailing lambdas)

이미 보았듯이 람다 표현식이 유일한 함수 매개변수라면 함수 괄호 ()를 생략할 수 있어요. 람다 표현식이 함수의 마지막 매개변수로 전달된다면 함수 괄호 () 밖에서 작성할 수 있어요. 두 경우 모두 이 구문을 후행 람다(trailing lambda)라고 해요.

예를 들어 .fold() 함수는 초기 값과 연산을 받아요:

fun main() {
    //sampleStart
    // The initial value is zero. 
    // The operation sums the initial value with every item in the list cumulatively.
    println(listOf(1, 2, 3).fold(0, { x, item -> x + item })) // 6

    // Alternatively, in the form of a trailing lambda
    println(listOf(1, 2, 3).fold(0) { x, item -> x + item })  // 6
    //sampleEnd
}

람다 표현식에 대한 자세한 내용은 Lambda expressions and anonymous functions 문서를 참고하세요.

투어의 다음 단계는 Kotlin의 클래스에 대해 배우는 거예요.

람다 표현식 연습 (Lambda expressions practice)

연습 1 (Exercise 1)

웹 서비스가 지원하는 동작 리스트, 모든 요청의 공통 접두사, 특정 리소스의 ID가 있어요. ID가 5인 리소스에 대해 title 동작을 요청하려면 다음 URL을 만들어야 해요: https://example.com/book-info/5/title. 람다 표현식을 사용해서 동작 리스트에서 URL 리스트를 만드세요.

fun main() {
    val actions = listOf("title", "year", "author")
    val prefix = "https://example.com/book-info"
    val id = 5
    val urls = // Write your code here
    println(urls)
}
fun main() {
    val actions = listOf("title", "year", "author")
    val prefix = "https://example.com/book-info"
    val id = 5
    val urls = actions.map { action -> "$prefix/$id/$action" }
    println(urls)
}

연습 2 (Exercise 2)

Int 값과 동작(타입 () -> Unit인 함수)을 받아, 그 동작을 주어진 횟수만큼 반복하는 함수를 작성하세요. 그런 다음 이 함수를 사용해서 "Hello"를 5번 출력하세요.

fun repeatN(n: Int, action: () -> Unit) {
    // Write your code here
}

fun main() {
    // Write your code here
}
fun repeatN(n: Int, action: () -> Unit) {
    for (i in 1..n) {
        action()
    }
}

fun main() {
    repeatN(5) {
        println("Hello")
    }
}