본문 바로가기
Kotlin/Kotlin 프로그래밍

[Kotlin] 함수와 함수형 프로그래밍 (3) - 코틀린의 다양한 함수

by 주 녕 2021. 4. 13.
728x90

모든 내용은 Do it! 코틀린 프로그래밍을 바탕으로 정리한 것입니다. 

 

코틀린의 다양한 함수

익명 함수 (Anonymous Function)

이름이 없는 일반 함수

    fun(x: Int, y: Int): Int = x + y
    
    val add1: (Int, Int) -> Int = fun(x, y) = x + y
    val add2 = fun(x: Int, y: Int) = x + y
    val add3 = { x: Int, y: Int -> x + y }
    
    val result = add1(10, 2)
  • 변수 선언에 그대로 사용할 수 있음
    • 익명 함수의 선언 자료형을 람다식 형태로 쓰면 변수를 함수처럼 사용할 수 있음
    • 매개변수에 자료형을 쓰면 선언부에 자료형은 생략 가능
    • 람다 표현식과 매우 유사

람다식으로 표기할 수 있는데 굳이 익명 함수를 쓰는 이유?

→ 람다식에서는 return, break, continue와 같은 제어문을 사용하기 어렵기 때문

→ 함수 본문 조건식에 따라 함수를 중단하고 반환해야 하는 경우 익명 함수를 사용해야 함

 

인라인 함수 (Inline Function)

  • 함수 본문 내용을 모두 복사해 넣어 함수의 분기 없이 처리
    • 코드가 복사되어 들어가기 때문에 내용은 대게 짧게 작성
    • 보통 함수는 호출되었으 ㄹ때 다른 코드로 분기해야 하기 때문에 내부적으로 기존 내용을 저장했다가 다시 돌아올 때 복구하는 작업에 프로세스오 메모리를 꽤 사용해야 하는 비용이 듦
    • 인라인 함수는 내용이 복사되어 들어가기 때문에 매번 분기하지 않아도 됨
  • 람다식 매개변수를 가지고 있는 함수에서 동작함
fun main() {
    shortFunc(3) { println("First call : $it") }
    shortFunc(5) { println("Second call : $it") }
}

inline fun shortFunc(a: Int, out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}
public final class InlineFunctionKt {
   public static final void main() {
      int a$iv = 3;  // 1회 복사
      int $i$f$shortFunc = false;
      String var2 = "Before calling out()";
      boolean var3 = false;
      System.out.println(var2);
      int var5 = false;
      String var6 = "First call : " + a$iv;
      boolean var7 = false;
      System.out.println(var6);
      var2 = "After calling out()";
      var3 = false;
      System.out.println(var2);
      a$iv = 5;  // 2회 복사
      $i$f$shortFunc = false;
      var2 = "Before calling out()";
      var3 = false;
      System.out.println(var2);
      var5 = false;
      var6 = "Second call : " + a$iv;
      var7 = false;
      System.out.println(var6);
      var2 = "After calling out()";
      var3 = false;
      System.out.println(var2);
   }

   public static final void shortFunc(int a, @NotNull Function1 out) {
      int $i$f$shortFunc = 0;
      Intrinsics.checkNotNullParameter(out, "out");
      String var3 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var3);
      out.invoke(a);
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
   }
}

디컴파일 결과에서 보면 shortFunc()의 내용이 main() 블록 안에 2번 복사된 것을 확인할 수 있음

 

인라인 함수 제한하기 (noinline 키워드)

매개변수로 사용한 람다식의 코드가 너무 길거나, 인라인 함수의 본문 자체가 너무 길면 컴파일러에서 성능 경고 할 수 있음

인라인 함수가 너무 많이 호출되면 오히려 코드 양만 늘어나서 좋지 않을 수 있음

 

noinline가 있는 람다식은 인라인으로 처리되지 않고 분기하여 호출

fun main() {
    shortFunc(3) { println("First call $it") }
}

inline fun shortFunc(a: Int, noinline out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}
public final class NoinlineTestKt {
   public static final void main() {
      byte a$iv = 3;
      Function1 out$iv = (Function1)null.INSTANCE;
      int $i$f$shortFunc = false;
      String var3 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var3);
      out$iv.invoke(Integer.valueOf(a$iv)); // 복사하지 않고 분기
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
   }

   public static final void shortFunc(int a, @NotNull Function1 out) {
      int $i$f$shortFunc = 0;
      Intrinsics.checkNotNullParameter(out, "out");
      String var3 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var3);
      out.invoke(a); // 인라인X
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
   }
}

 

인라인 함수와 비지역 반환 (crossinline 키워드) 

익명 함수와 인라인 함수에서는 return을 사용하여 함수를 끝낼 수 있음

fun main() {
    shortFunc(3) {
        println("First call : $it")
        return
    }
}

inline fun shortFunc(a: Int, out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}
  • out(a)는 인라인되어 대체되기 때문에  return문까지 포함됨
  • 비지역 반환(Non-local Return) : 람다식 함수에서 return문을 만낫지만 의도하지 않게 바깥의 함수가 반환 처리 되는 것
  • crossinline 키워드 : 비지역 반환을 금지해야 하는 람다식에 사용 → return을 금지함(코드 작성 단계에서 오류)

 

확장 함수 (Extension function)

클래스처럼 필요로 하는 대상에 함수를 더 추가할 수 있는 확장 함수의 개념 제공

fun 확장 대상.함수 이름(매개변수, ...): 반환값 { }

fun main() {
    val source = "Hello World!"
    val target = "Kotlin"
    println(source.getLongString(target))
}

fun String.getLongString(target: String): String = 
    if (this.length > target.length) this else target
  • String 클래스에 getLongString()을 새로운 멤버 메소드로 추가
  • 기존 클래스의 선언 구현부를 수정하지 않고 외부에서 손쉽게 기능 확장 가능 (기존의 표준 라이브러리 확장에 유용)
  • 확장하려는 대상에 동일한 이름의 멤버 함수 또는 메소드가 존재한다면 항상 확장 함수보다 멤버 메소드가 우선 호출!

 

중위 함수 (Infix Notation)

클래스 멤버를 호출할 때 사용하는 점(.)을 생략하고 함수 이름 뒤에 소괄호를 붙이지 않아 직관적인 이름을 사용할 수 있는 표현법

→ 일종의 연산자를 구현할 수 있는 함수 (특히 비트 연산자)

  • 멤버 메소드 또는 확장 함수여야 함
  • 하나의 매개변수를 가져야 함
  • infix 키워드를 사용하여 정의함
fun main() {
    val multi = 3 multiply 10
    println("multi: $multi")
}

infix fun Int.multiply(x: Int): Int {
    return this * x
}

 

→ Int의 확장 함수이며, 하나의 매개변수를 가지고 있으며 infix 키워드를 사용한 중위 함수임

3.multiply(10) 보다 3 multiply 10과 같은 직관적인 형태로 변경하여 사용할 수 있음

 

꼬리 재귀 함수 (Tail Recursive Function)

재귀 : 자기 자신을 다시 참조하는 방법

→ 반드시 조건에 맞게 설계 해야 함. 그렇지 않으면 스택 오버플로 오류가 발생함

  • 무한 호출에 빠지지 않도록 탈출 조건을 만들어 둠
  • 스택 영역을 이용하므로 호출 횟수를 무리하게 많이 지정해 연산하지 않음
  • 코드를 복잡하게 하지 않음

코틀린에서는 꼬리 재귀 함수를 사용하여 스택 오버플로 현상을 해결할 수 있음 → tailrec 키워드

 

factorial 함수

<일반 재귀 함수>

fun main() {
    val number = 4
    val result: Long
    
    result = factorial(number)
    println("Factorial: $number -> $result")
}

fun factorial(n: Int): Long {
    return if (n==1) n.toLong() else n * factorial(n-1)
}

<꼬리 재귀 함수>

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

tailrec fun factorial(n: Int, run: Int = 1): Long {
    return if (n==1) run.toLong() else factorial(n-1, run*n)
}
  • 일반적인 재귀에서는 재귀함수가 먼저 호출되고 계산되지만 꼬리 재귀에서는 계산을 먼저하고 재귀 함수가 호출됨
  • 인자 안에서 팩토리얼의 도중 값을 계산하고 호출함
    • 그때그때 계산하므로 스택 메모리를 낭비하지 않아도 됨 → n값을 크게 설정해도 상관 없음

 


함수와 변수의 범위

최상위 함수와 지역 함수

  • 최상위 함수(Top-level Function) : 파일을 만들고 곧바로 main() 함수나 사용자 함수를 만들 수 있음 → 선언 순서 영향X
  • 지역 함수(Local Function) : 함수 안에 또 다른 함수가 선언되어 있는 경우 → 선언 순서에 영향O
fun a() = b()
fun b() = println("b")

fun c() {
    // fun d() = e()
    fun e() = println("e")
}

fun main() {
    a()
    // e()
}

→ c()안의 d()는 지역 함수이며 e()의 이름을 모름 (아직 선언되지 않은 상태: Unresolved Reference 오류)

 

지역 변수와 전역 변수

  • 지역 변수(Local Variable) : 특정 코드 블록 안에 있는 변수 → 블록을 벗어나면 프로그램 메모리에서 삭제됨
  • 전역 변수(Global Variable) : 최상위에 있는 변수 → 프로그램이 실행되는 동안 값이 유지됨
var global = 10

fun main() {

    val local1 = 20
    val local2 = 21

    fun nestedFunc() {
        global += 1
        val local1 = 30
        println("nestedFunc local: $local1")
        println("nestedFunc local2: $local2")
        println("nestedFunc global: $global")
    }

    nestedFunc()
    outsideFunc()
    
    println("main global: $global")
    println("main local1: $local1")
    println("main local2: $local2")

}

fun outsideFunc() {
    global += 1
    val outVal = "outside"
    println("outsideFunc global: $global")
    println("outsideFunc outval: $outVal")
}

  • global : 여러 함수에서 접근하면서 1씩 증가하여 최종 값은 12가 출력됨
  • main() 블록의 local1과 nestedFunc() 블록의 local1은 서로 다른 변수임
    • nestedFunc() 블록에서 local1을 새롭게 정의했기 때문에 main() 블록의 local1이 가려지게 된 것
    • main()의 local1은 main() 블록이 끝나지 않는 한 계속 유지됨
  • outVal : 오로지 outsideFunc() 블록에서만 유효함

 

728x90

댓글