6 minute read

일급함수

일급함수는 스스로 객체로써 취급되는 함수로 다른 함수를 파라미터로 전달받고 반환할 수 있는 함수를 뜻한다.

# 코틀린의 경우
fun print(body: (Int, Int) -> Int) {
 println(body(5, 5))
}

print({a, b -> a})

# 자바의 경우
public final void print(@NotNull Fuction body) {
 Object result = body.invoke(5, 5);
 System.out.println(result);
}

코틀린의 경우 위와 같이 자동으로 컨버팅 될 수 있기 때문에 유동적인 방법도 사용가능하다.

private fun print(body: (Int, Int) -> Int) {
 println(body(10, 5))
}

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

printResult(::sum) //result = 15
printResult(::sub) //result = 5

inline과 noinline

# 코틀린의 경우
fun doSomething(body: () -> Unit) {
 body()
}

fun callFunction() {
 doSomeThing{ println("out") }
}
 
 # 자바의 경우
public void doSomethings(Function body) {
 body.invoke();
}
 
public void callFunction() {
 doSomething(new Function() {
  @Override
  public void invoke() {
   System.out.println("out");
  }
 });
}

코틀린이나 자바에서 inline 키워드를 쓰지 않는 경우 조합하는 함수가 많아질수록 계속 N개 만큼의 function object가 생성되어 메모리 할당과 가상 호출에 의한 런타임 오버헤드가 발생한다. 이럴 때 사용하게 되는 것이 inline 키워드이다. inline 함수로 정의된 함수는 컴파일 단계에서 호출하는 방식이 아니라 코드 자체가 복사되는 방식이다.

inline fun doSomeThing(body: () -> unit) {
 body()
}

fun callFunction() {
 doSomething { println("out") }

inline 함수를 사용하면 Function 인스턴스를 만들지 않고 callFunction 내부에 삽입되어 바로 선언된다. inline 함수는 private 키워드를 사용하여 정의할 수 없고 internal을 사용해야한다. 모든 람다함수에 inline을 쓰고싶지 않을 경우 noinline 키워드를 추가한다.

-인라인 함수의 장,단점 인라인 함수를 쓰면 메모리 할당과 오버헤드에 효과적이고 메서드를 호출하는게 아니라 복사하기 때문에 성능의 향상으로 이어지지만 컴파일시 변환되는 Byte Code의 길이는 더 길어진다.

꼬리재귀

talerec은 꼬리재귀(tail recursive)라는 의미로, 추가적인 연산이 없이 자신 스스로 재귀적으로 호출하다가 어떤 값을 리턴하는 함수를 의미한다.

자신만 반복적으로 호출하는 재귀함수는 while과 같이 루프를 사용하는 코드로 변환이 가능하다. 이렇게 변환하면 재귀함수가 홏ㄹ되면서 소비되는 스택을 아낄 수 있다. 루프는 동일한 결과를 출력하면서 재귀함수보다 더 적은 자원을 사용하게 된다.


# 재귀함수
fun factorial(n: Int, acc: Int): Int {
 return if (n <= 0) {
  acc
 } else {
  factorial(n-1, n*acc)
 }
}

# 꼬리재귀
fun factorial(n: Int, acc: Int): Int {
 return if (n <= 0) {
  acc
 } else {
  factorial(n-1, n*acc)
 }
}

두 방식의 결과는 같지만 바이트 코드를 비교하면 과정은 다르게 수행되었다.

# 재귀함수가 컴파일된 바이트 코드를 자바로 변환시
public static final int factorial(int n, int acc) {
 if (n <= 0) {
  return acc;
 }
 return factorial(n-1, n*acc)
}

# 꼬리재귀가 컴파일된 바이트 코드를 자바로 변환시
public static fianl int factorial(int n, int acc) {
 for (int n2 = n; n2 > 0; n2--) {
  acc = acc * n2;
 }
 return acc;
}

위의 코드와 같이 꼬리재귀를 사용하면 루프코드로 컴파일된다.

제네릭

코틀린에서 사용하는 제네릭은 자바에서 사용하는 것과 동일하게 사용할 수 있지만, 자바에서 사용하는 제네릭은 정의를 하지 않아도 기본 Object로 만들어주지만, 코틀린에서 사용하는 제네릭은 명시적으로 꼭 적어주도록 만들었다.

# Java Generics 정의
interfacce Generic<T> {
 void setItem(T item);
}

# Kotilin Generics 정의
interface Generic<in T> {
 fun setItem(item: T)
}

자바와 코틀린의 인터페이스 정의는 동일하면 T타입을 제네릭으로 선언하였다.

# 코틀린에서 제네릭을 상속 받는 경우
class Sample : Generic<Generic Type을 정의> {
 overrie fun setItem(item: generic Type을 정의) {
  //doSomeThing
 }
}

고차함수

고차 함수는 함수를 파라미터로 가져오고 함수를 리턴할 수 있다. 주요 유스케이스는 콜백이며, 자바에서 인터페이스를 생성하고 한쪽에서 인터페이스를 구현해야 하는 반면에 코틀른에서는 함수를 변수에 저장할 수 있어 나중에 사용할 수 있다. 또는 또 다른 함수 안에서 생성될 수 있다. 함수가 선언되지 않았는데 전달되는 것을 람다 또는 익명 함수라고 한다.

# 변수로 함수 넘기기
fun calc(x: Int, y:Int, operation: (Int, Int)->Int) = operation(x, y)
run { println(calc(5,5, {x, x -> x+x})) }
//실행결과 : 10

# 함수 리턴
fun getCalcFunctions(action: Int): (Int, Int)->Int {
  when(action)
    1 -> return {x: Int, y:Int -> x+y}
    2 -> return {x: Int, y:Int -> x-y}
}

val calFunction = getCalcFunctions(2)
run { println(calFunction(5, 5)) }
//실행결과 : 0

익명함수

익명함수라는 말은 람다식을 지칭한다. 익명 함수라는 말은 쉽게 말해서 “fun” 키워드를 통해 생성한 함수가 아닌것을 의미한다.

# 기본형태
val lambda09 = { parm1: Int, parm2: Int -> parm1 + parm2 }

fun main(args: Array<String>) {
 println(lambda09(1, 2))
}

함수를 “fun”키워드를 통해 생성하지 않고 변수에 할당하는 모습을 볼 수 있다.

# 내용이 여러 줄일 경우 가장 마지막 줄이 반환값이 된다.
val lambda09 = {parm1: Int, parm2: Int ->
 println("labmda")
 parm1 + parm2
}

# 인자가 없으면 매개변수 부분을 입력하지 않아도 된다.
val labmda09 = {
 val parm1 = 10
 val parm2 = 10
 println("${parm1 + parm2}")
}

클로저

클로저(Closure)는 상위 함수의 영역(outer scope)의 변수를 접근할 수 있는 함수를 말한다. 코틀린은 클로저를 지원하며, 그렇게 때문에 익명함수는 함수 밖에서 정의된 변수에 접근할 수 있다.

fun add(x: Int): (Int) -> Int {
 return fun(y: Int): Int {
  return x + y
 }
}
 
fun main(args: Array<String>) {
 val func = add(10)
 val result = func(20)

위의 코드에서 add 함수는 익명함수를 리턴한다. 익명함수는 전역변수도, 익명함수의 인자도 아닌 외부에서 정의된 변수 x에 접근하지 못해 컴파일 에러가 발생할 것 같지만 코틀린이 클로저를 지원하기 때문에 컴파일 에러가 발생하지 않는다.

# forEach 함수는 익명함수를 인자로 받는다. 람다식으로 생성한 익명함수도 클로저 함수이며, outer scope의 변수에 접근할 수 있다.
var sum = 0
ints.filter { it > 0 }.forEach {
 sum = sum + it
}
print(sum)

# 자바로 아래처럼 리스너를 만들면 컴파일 에러 발생
int x = 5;

view.setOnClickListener(new View.OnClickListener() {
 @Override public void onClick(View v) {
  System.out.prinln(x); //x가 final이 아니기 때문에 클로저를 지원하지 않는 자바에서는 컴파일 에러가 발생.
 }
});

# 코틀린은 가능!
 var x = 5
 
 view.setOnClickListener {
  println(x)
 }

코틀린 레퍼런스 클로저 설명에 의하면 람다식 또는 익명함수(로컬 함수 및 객체 식)는 해당 클로저에 액세스 할 수 있다. 클로저에서 캡처된 변수는 람다에서 수정할 수 있다라고만 되어 있다. obejct-C나 스위프트의 클로저의 개념과는 해석이 다른 것 같다.

var sum = 0
ints.filter { it > 0 }.forEach {
 sum += it
}
println(sum)

함수형 프로그래밍의 클로저라는 기능에 대해 알아보기 위해 스위프트의 클로저 개념을 공부해보기로 했다. 스위프트에서 클로저란 한 번만 사용하기 위한 일회용 함수를 작성할 수 있는 블럭이라고 한다. 자신이 정의되웠던 문맥으로부터 모든 상수와 변수의 값을 캡쳐하거나 레퍼런스를 저장한다.

# 클로저의 형태
{ (param1: Int, param2: Int: Stirng) -> Int in
 return param1 + param2
}

# 클로저 예제
let numbers = [1, 4, 56, 22, 5]
func sortAscending(_ i: Int, - j: Int) -> Bool {
 return i < j
}

//클로저 미사용
let sortedNumbers = numbers.sorted(by: sortAscending)

//클로저 사용
let sortedNumbers = numbers.sorted(by: { (i: Int, j: Int) -> Bool in
  return i < j
})

클로저가 단일 명령문만을 가지고 있다면 ‘return’키워드 없이도 명령문의 값을 반환할 수 있다.

let sortedNumbers = numbers.sorted(by: { (i: Int, j: Int) -> Bool in
 i < j
 })

스위프트는 클로저의 타입을 문맥으로부터 추론해 결정한다. 클로저에서 ‘타입’, ‘반환 타입’, ‘return’을 제거해서 축약할 수 있다.

// 매개변수 타입 추론 축약
let sorted = numbers.sorted(by: { i, j in i < j})

// 축약형 인수형 문법
let sortedNubmers = numbers.sorted(by: { $0 < $1 })

축약형 인수형 문법 구조 적용 매개변수 이름을 사용하지 않고 인자로 넘어오는 순서대로 $0, $1,…를 사용하면 in과 매개변수 목록도 없앨 수 있다. 두 인자를 받아서 연산자에 넘기는 경우 ‘<’연산자를 바로 넘겨도 된다.

let sortedNumbers = numbers.sorted() { $0 < $1 }

# 클로저가 함수에 대한 유일한 인자일 때 괄호를 완전히 생략할 수 있다.
let sortedNumbers = numbers.sorted { $0 < $1 }

스위프트의 클로저를 공부하면서 느낀점은 코틀린의 클로저와 다르다는 것이다. 스위프트에 대한 견해 많이 부족하여 스위프트의 클로저의 내용과 코틀린 클로저의 내용을 제대로 비교할 수는 없지만 나름대로 생각한 차이점을 스위프트 축약 단계를 예로 들어 써보고자 한다.

1. 클로저가 단일 명령문만을 가지고 있는경우 ‘return’키워드를 생략할 수 있다.

# 코틀린의 경우 단일 명령문이 아니더라도 마지막 명령문이 자동으로 리턴된다.
val add = {x: Int, y: Int ->
 x * y
 x + y
}

println(add(10, 20))
//결과값: 30

2. 매개변수 타입 추론 축약

# 코틀린의 경우에도 매개변수 타입을 추론할 수 있다면 축약가능하다.
fun exam(x: Int, operation: (Int, Int) -> Int) {
    
}

exam(10, {x, y -> x + y})

3. 코틀린의 경우 인수형으로 축약할 수 없다.

4. 클로저가 함수에 대한 유일한 인자일 때 괄호 생략

# 코틀린의 경우에도 함수가 유일한 인자일 때 괄호를 생략할 수 있다.
fun exam(operation: (Int, Int) -> Int) {
    
}

exam {x, y -> x + y}

# 마지막 인자가 함수일 때 소괄호 밖 중괄호로 인자를 뺄 수 있다.
fun exam(x: Int, operation: (Int, Int) -> Int) {
    
}

exam(10) {x, y -> x + y}

Categories:

Updated: