Kotlin 함수(Functions)


개요

먼저 간단한 함수를 정의하는 예를 보도록 하자.

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

위에 예에서는 hello 함수를 정의하였다. 코틀린에서는 함수를 정의하려면 fun이라는 키워드를 사용하여 정의한다. “fun 키워드, 함수 이름, 인수 목록"을 순서대로 입력한다. 입수에는 타입을 명시해야 한다.

함수는 보통 클래스에 속하지만 항상 킅래스에 속할 필요가 없고, 최상위 레벨(Top-level)로도 정의할 수 있다.

값을 반환하는 함수

인수를 사용하여 계산을 수행하고 결과를 반환하는 함수를 정의해 보자. 아래 예제는 간단한 덧셈을 수행하는 함수 sum를 정의하였다.

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

앞에 hello 함수와 다른 점은 반환 유형을 지정하고, return 키워드로 값을 반환한다는 점이다. 반환 값의 유형은 함수의 인수 목록 바로 뒤에 콜론(:) 을 사이에 두고 작성한다. sum 함수의 반환값의 형태는 2개의 Int 형태 값을 더한 것이어서 반환 타입도 Int가 된다. return 키워드는 함수의 값을 반환하는 키워드이다.

반환 값이 있는 함수는 끝에 타입을 명시해 줘야하고 return 키워드를 사용하여 반환해야 한다.

Single expression functions(한 줄로 함수를 정의하는 방법)

또는 다음과 같이 한줄에 가능한 작성 가능한 함수는 = 으로 반환 값으로 바로 정의할 수도 있다.

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

디폴트 인수

함수의 인수에는 기본값을 설정할 수 있다.

fun hello(name: String, exclamation: Boolean = false) {
  val suffix = if (exclamation) "!" else ""
  println("Hello, ${name}${suffix}")
}

hello 함수는 Boolean 타입 exclamation이라는 인수를 가지고 있고 기본값을 설정하였다. 기본값이 있는 인수(디폴트 인수)는 호출시 값 지정을 생략 할 수 있다. 생략하면 기본값이 사용된다.

예제: 기본 인수의 함수 사용 예

// 두번째 인수 생략
hello("Kotlin")       // Hello, Kotlin

// 두번째 인수 지정
hello("Kotlin", true) // Hello, Kotlin!

그리고, 함수 호출시에 인수에 건네주는 값을 이름 지정하여 전달할 수도 있다.

예제: 명명된 인수

hello(name = "Foo")

// 인수 목록의 순서를 따를 필요는 없다.
hello(exclamation = false, name = "Baz")

디폴트 인수는 호출시 인수 이름을 기술하는 것으로 지정하려는 인수에만 전달할 수 있다.

fun drawCircle(x: Int, y: Int, r: Int,
    lineColor: String = "#000",
    fillColor: String = "transparent",
    borderWidth: String = "1px") {
  ... 생략 ...
}

fun main() {
    drawCircle(100, 100, 50, borderWidth="2px")
}

Unit 함수

값을 반환하지 않는 함수는 Unit 함수로 처리된다.

fun printMessage(msg: String): Unit {
    println(msg)
}

Unit는 아래와 같이 보통 생략된다.

fun printMessage(msg: String) {
    println(msg)
}

Nothing 형 함수

항상 예외를 반환하고 반환 값을 되돌릴 수 없는 함수는 Nothing형 함수로 정의한다.

fun raiseError(msg: String): Nothing {
    throw MyException(msg)
}

가변 인수

vararg는 가변 인수를 선언한다.

fun foo(vararg args: String) {
    args.forEach {
        println(it)
    }
}

fun main() {
    foo("A", "B", "C")
}

가변 인수 함수에 배열을 변수로 전달할 때 스프레드 연산자(*)를 사용하여 배열을 확장하고 전달해야 한다.

fun main() {
    val arr = arrayOf("A", "B", "C")
    foo(*arr)
}

함수 참조 (::)

함수 이름 앞에 ::를 넣으면 함수를 참조하는 객체를 얻을 수 있다. 아래의 예에서는 add 함수를 함수 객체 method에 대입하여 호출한다.

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

fun main() {
    val method = ::add
    println(method(3, 5))
}

재귀 호출

함수가 자신을 호출하는 것을 재귀 호출이라고 한다. 재귀 호출을 사용하면 루프를 선언적으로 작성할 수 있다. 예를 들어, 인수 목록의 합계 값을 반환하는 함수를 예를 들어 보자. 우선 일반 버전이다.

예제: for에 의한 루프

fun sum(list: List<Int>): Int {
  var sum = 0
  for (e in list) {
    sum += e
  }
  return sum
}

for에 의해 루프를 돌리고 있다. 변수 sumvar가 선언되었으며 반복적으로 새로운 값이 할당된다.

다음은 재귀 호출 버전이다.

예제: 재귀 호출에 의한 루프

fun sum(list: List<Int>): Int =
    if (list.isEmpty()) 0 else list.first() + sum(list.drop(1))

for로 재대입를 하지 않게 되었다. 대신 sum 함수 정의 부분에서 자신을 호출한다.

참고로 isEmpty(), first(), drop(1)는 정수의 List인 list의 메서드이다. 각각 목록이 공백인가 어떤가, 목록의 첫번째 요소, 첫번째부터 한개씩 요소를 제외한 새로운 목록를 반환한다.

최대 공약수를 구하는 함수을 구해 보자.

// 큰 수를 반환한다.
fun max(a: Int, b: Int): Int = if (a < b) b else a

// 작은 수를 반환한다.
fun min(a: Int, b: Int): Int = if (a <= b) a else b

// 최대 공약수를 반환한다.
fun gcd(a: Int, b: Int): Int {
  var x = max(a, b)
  var y = min(a, b)
  while(y != 0) {
    val w = y
    y = x % y
    x = w
  }
  return x
}

최대 공약수를 구하는 gcd함수를 재귀 호출을 사용해 다시 구현해 보자.

예제: gcd를 재귀 함수로 만들기

fun gcd(a: Int, b: Int): Int {
  val x = max(a, b)
  val y = min(a, b)
  return if (y == 0) x
         else gcd(y, x % y)
}

var 그리고 while 루프도, 그리고 임시 변수 w도 지울 수 있다! 이와 같이 재귀 호출을 사용하면 코드가 깔끔하고 읽기 쉬워지는 경우가 많다.

재귀 호출의 단점은 함수를 여러 번 호출하여 스택을 소비하는 것이다. 여러 번(환경에 따라 매우 많은 횟수) 함수를 계속 호출하면 스택 오버플로가 발생하여 프로그램이 충돌한다. 이를 피하기 위해 Kotlin은 꼬리 호출 최적화(tail call optimization) 라는 구조를 가지고 있다.

꼬리 호출은 재귀 호출이 끝에 있는 호출이다. 예를 들어, 바로 위에 예제의 gcd함수는 꼬리 호출을 수행한다. 이러한 재귀 함수에 tailrec 키워드 후행 호출 최적화가 이루어져 스택 오버플로가 발생하지 않는 코드로 변형된다.

예제: 꼬리 호출 최적화가 효과적인 gcd 함수

tailrec fun gcd(a: Int, b: Int): Int {
  val x = max(a, b)
  val y = min(a, b)
  return if (y == 0) x
         else gcd(y, x % y)
}

최적화가 유효하게 되는 것은 어디까지나 꼬리 호출만이기 때문에, 함수에 따라서는 재귀의 방법을 학습해둔 필요가 있다.