로컬 함수란 함수 내 함수를 말합니다

 

interface Foo {
    void bar(int x);
}

public class Test {
    public static void main(String[] args) {
        // Hack to give us a mutable variable we can
        // change from the closure.
        final int[] mutableWrapper = { 0 };

        Foo times = new Foo() {
            @Override public void bar(int num) {
                mutableWrapper[0] *= num;
                System.out.println(mutableWrapper[0]);
            }
        };

        for (int i = 1; i < 100; i++) {
            mutableWrapper[0] = i;
            times.bar(2);
            i = mutableWrapper[0];

            times.bar(i);
            i = mutableWrapper[0];
        }
    }
}

자바에서는 Java7 이전의 경우 이런식으로 내부에서 anonymous inner class를 만들어 써야 합니다

클로저를 흉내내기 위해 이상한 핵을 써야 하기도 합니다. (final 필드만 접근가능하기 때문)

Java8부터는 lambda가 있긴 한데 여러모로 불편합니다

 

파이썬, js 해보신 분들은 nested function이 사실 당연하게 동작하는건데 자바로 넘어오게 되면 좀 피곤합니다

 

fun dfs(graph: Graph) {
    val visited = HashSet<Vertex>()
    fun dfs(current: Vertex) {
        if (!visited.add(current)) return
        for (v in current.neighbors)
            dfs(v)
    }

    dfs(graph.vertices[0])
}

코틀린은 로컬 함수를 지원하기 때문에 코드 중복을 더 편하고 쉽게 줄일 수 있습니다

 

한계

인라인은 (아직은) 불가능합니다

 

동작방식

fun main(args: Array<String>) {
    var counter = 1
    fun hello() {
        ++counter
    }

    hello()
    hello()

    println(counter)  // 3
}

정말 간단한 예제를 코틀린으로 짜봤습니다

 

import kotlin.Metadata;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import kotlin.jvm.internal.Intrinsics;
import kotlin.jvm.internal.Ref.IntRef;
import org.jetbrains.annotations.NotNull;

@Metadata(
   mv = {1, 5, 1},
   k = 2,
   d1 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
   d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "kotlin_temp"}
)
public final class MainKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      final IntRef counter = new IntRef();
      counter.element = 1;
      <undefinedtype> $fun$hello$1 = new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            IntRef var10000 = counter;
            ++var10000.element;
            int var1 = var10000.element;
         }
      };
      $fun$hello$1.invoke();
      $fun$hello$1.invoke();
      int var3 = counter.element;
      System.out.println(var3);
   }
}

IntelliJ에서 디컴파일된 자바 코드를 보면 이렇습니다

Function 인터페이스를 이용해 익명 내부클래스를 만들고 invoke 메소드를 구현하고 있네요

동작방식은 자바에서 Function 인터페이스를 쓴 것과 사실 다른게 없습니다 ㅎㅎ

 

오버헤드

local function을 감싸고 있는 function이 호출될 때마다 Function 객체를 만들기 때문에 약간의 오버헤드가 생길 수 있다고 합니다

안드로이드에서는 함수 개수가 더 늘어나 DEX count 제한에 조금이나마 부담될 수도 있다네요. 코틀린에는 inline이 있긴 한데 아직 로컬 함수는 인라인이 불가능해서 아쉬운 부분입니다

 

Reference

- https://kotlinlang.org/docs/functions.html#local-functions

- https://medium.com/tompee/idiomatic-kotlin-local-functions-4421f86ac864

반응형