본문 바로가기
개발/코틀린(kotlin)

[Kotlin] 코틀린 High order function

by darksilber 2020. 1. 29.
반응형

출처 - https://tourspace.tistory.com/120?category=797357

 

 

8. 고차함수

고차함수랑 함수의 인자나, 반환값이 lambda인 경우를 말합니다.

예를들면 list의 filter map은 인자로 람다를 받습니다. 따라서 이 둘은 고차함수 입니다.

 

val add: (Int, Int) -> Int = {a,b -> a + b}

val action: () -> Unit = { println("짜란~") }

 

함수타입은 생략할 수도 있으나, 명시하려면 (인자1:타입, 인자2:타입....) -> 반환타입 으로 표기할 수 있습니다.

인자는 괄호로 묶여야 합니다.

Unit은 반환값이 없음을 나타내는 타입입니다. 실제 타입없이 사용할때는 없어도 되지만 타입을 명시할 때는 필요합니다.

val returnNull = (Int, String) -> String? = { null } 
val lambdaNull : ((Int, String) -> String)? = null

위 두개의 type은 return에 null을 허용하거나, 인자 자체에 null을 허용할때를 표현한 예제 입니다.

※ 함수 타입에 인자를 넣더라도 컴파일시 무시됩니다. 하지만 인자가 있음으로 해서 가독성을 높일 수 있습니다. (타입의 인자와 사용된 인자의 이름역시 달라도 상관 없습니다.)

 

8.1.2 인자로 받은 함수 호출

자바에서 코틀린의 함수를 호출할때 인자를 함수타입으로 넣어야 한다면 이를 대신할 인터페이스를 구현하는 객체를 넣어야 합니다.

interface의 이름은 FunctionN<N1,N2...N, R> 이며 invoke 함수 하나만을 abstract로 갖습니다.

  • Function0<R> : 인자가 없을때
  • Function1<T,R>: 인자가 하나일때
  • Function2<T,V,R>: 인자가 두개일때
  • FunctionN<N1,N2...,R> 인자가 N개일때

만약 자바의 버전이 8 이상이라면 바로 람다식을 사용 할 수 도 있습니다.

//자바
twoAndThree(x -> x+1) 

// 또는
twoAndThree(new Function1 { 
	@Override 
    public Integer invoke(Integer number) { 
    	System.out.println(number) 
        return number +1
    } 
}

또한 코틀린의 기본 라이브러리 확장 함수도 자바에서 호출할 수 있습니다.

다만 이런경우 receiver object를 같이 넘겨줘야 합니다.

//자바 List lists = new ArrayList<>(); 
list.add("abc"); 
list.add("def"); 

CollectionKt.forEach(lists, str -> { 
	System.out.prinlnt(str); 
    return Unit.INSTANCE;
}

또한 람다의 반환값이 Unit은 반드시 반환해야 합니다. (자바의 void를 return할 수 없습니다.)



8.1.4 lambda 인수에 default 값 지정

다른 parameter와 마찬가지로  함수타입 인자로 기본값을 지정해 줄수 있습니다.

fun Collection.joinToString( 
		separator: String = ", ", 
        prefix: String = "", 
        postfix: String = "", 
        transform: (T) -> String = { it.toString() }): String { 
    val result = StringBuilder(prefix) 
    
    for ((index, element) in this.withIndex()) { 
    	if (index > 0) result.append(separator) 
        result.append(transform(element))
    } 
    
    result.append(postfix)
    return result.toString() 
}

fun main(args: Array) { 
	val letters = listOf("Alpha", "Beta") 
    println(letters.joinToString()) 
    println(letters.joinToString { it.toLowerCase() }) 
    println(letters.joinToString(separator = "! ", postfix = "! ",
    		transform = { it.toUpperCase() }))
}

만약 null이 될수있는 함수타입이라면 내부적으로 null check을 한 후에 사용해야 합니다.

 

8.1.5 함수를 반환

인자 외에 return값에 함수 타입을 반환하도록 할수도 있습니다.

enum class Delivery { STANDARD, EXPEDITED } 

class Order(val itemCount: Int) 

fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double { 
	if (delivery == Delivery.EXPEDITED) { 
    	return { order -> 6 + 2.1 * order.itemCount } 
    }
    
    return { order -> 1.2 * order.itemCount } 
}

fun main(args: Array) { 
	val calculator = getShippingCostCalculator(Delivery.EXPEDITED) 
    println("Shipping costs ${calculator(Order(3))}")
}

 

8.1.6 람다를 이용한 중복제거

람다를 이용하면 중복되는 부분을 효과적으로 제거할 수 있습니다.

즉 재활용에 강한 코드를 만들수 있다는 장점이 있습니다.

data class SiteVisit( 
	val path: String, 
    val duration: Double, 
    val os: OS 
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID } 

val log = listOf( 
	SiteVisit("/", 34.0, OS.WINDOWS), 
    SiteVisit("/", 22.0, OS.MAC), 
    SiteVisit("/login", 12.0, OS.WINDOWS), 
    SiteVisit("/signup", 8.0, OS.IOS), 
    SiteVisit("/", 16.3, OS.ANDROID) 
) 
   
val averageWindowsDuration = log
								.filter { it.os == OS.WINDOWS }
								.map(SiteVisit::duration)
                                .average() 

fun main(args: Array) { 
	println(averageWindowsDuration) 
}

위 함수는 특정 OS의 평균 접속시간을 구하는 코드 입니다.

이를 Top-level function으로 바꾸고 OS도 인자로 넣을수 있도록 바꿔보겠습니다.

data class SiteVisit(
	val path: String, 
    val duration: Double, 
    val os: OS 
)

enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID } 

val log = listOf( 
	SiteVisit("/", 34.0, OS.WINDOWS), 
    SiteVisit("/", 22.0, OS.MAC), 
    SiteVisit("/login", 12.0, OS.WINDOWS), 
    SiteVisit("/signup", 8.0, OS.IOS), 
    SiteVisit("/", 16.3, OS.ANDROID) 
)

fun List.averageDurationFor(os: OS) = 
			filter { it.os == os }.map(SiteVisit::duration).average() 


fun main(args: Array) { 
	println(log.averageDurationFor(OS.WINDOWS)) 
    println(log.averageDurationFor(OS.MAC)) 
}



8.2 inline function

람다를 사용하면 컴파일러가 내부적으로 익명class로 치환하여 객체를 생성합니다.

이는 java 1.6과 호환하기 위함이며, 성능 저하를 위해서 한번만 생성하여 재사용하는 방법등을 (lambda capturing이 되는 경우를 제외) 이용하여 성능에 신경을 쓰고 있습니다.

 

하지만 적어도 한번은 객체를 생성해야 한다는 부담감은 있습니다.

따라서 이를 극복하기 위한 함수가 inline function 입니다.

 

inline function은 컴파일시 해당 함수의 코드가 호출된 부분으로 그대로 들어가 바이트 코드로 변경됩니다.

inline func <T> synchronized(lock: Lock, action: () -> T) :T { 
	lock.lock() 
    try { 
    	return action() 
   }
   finally {
   	lock.unlock() 
   } 
}

fun foo(string: Array<String>) { 
	val lock = Lock() 
    println("start") 
    synchronized(lock) {
    	println("lambda") 
    }
    println("end") 
}

실제 위 함수가 컴파일되면 아래와 같이 됩니다.

fun __foo__(l: lock) {
	println("start")
    try { 
    	println("lambda") 
    }
    finally { 
    	lock.unlock() 
    }
    println("end") 
}

즉 함수 본문 뿐만 아니라 람다의 구문도 그대로 녹아 들어갑니다.

단! 람다 구문이 외부 변수(lambda capturing)을 한다면, 람다부분의 코드는 호출함수에 녹아들지는 않습니다. (그냥 람다식을 호출 하도록 바이트 코드가 변환됨)

8.2.2 Inline function limitation

inline으로 사용하려는 함수가 수신된 함수타입 인자를 바로 실행하지 않고, 다른 변수에 저장하고 나중에 그 변수를 사용하는경우에는 inline 함수로 만들 수 없습니다.

즉, 함수타입 인자를 바로 호출하지 않고, 어딘가에 저장하여 사용한다면, 그 함수는 inline 함수가 될 수 없습니다.

이때 컴파일러는 관련 에러를 출력합니다.

 

list를 operation 할때 sequence로 만들면 filter, map 함수는 더이상 inline 함수가 아닙니다.

sequence는 최종 연산자를 만나야만 연산이 시작되므로 그전까지 해당 동작을 변수나, 다음 객체 생성자에 담아두고 있어야 합니다. 

따라서 sequence의 filter, map 등의 함수는 inline 될수 없습니다.

 

또한 함수의 인자로 여러개의 함수타입을 받을때 특정 람다만 inline을 하고 싶지 않다면 noinline 키워드를 붙이면 됩니다.

inline fun foo(a: () -> Unit, noinline b: () -> String) {...}


위 코드의 경우 컴파일시 a()는 코드는 inlining 되고, b()는 함수 호출로 변경됩니다.

 

8.2.3 Collection 연산 inlining

collection 함수의 대부분은 람다를 인자로 받습니다.

data class Person(val name: String, val age: Int) 

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun main(args: Array) {
	println(people.filter { it.age > 30 } 
    			  .map(Person::name)) 
}

filter와 map은 inline 함수이기 때문에 해당 코드가 호출된 코드로 녹아들어갑니다.

따라서 직접 for문을 돌리는것과 비용면에서는 차이가 없습니다.

다만 두 함수 모두, 중간연산으로 값을 내어 놓습니다.

filter 수행의 결과를 list로 반환되며, 그 결과로 map을 수행하고 또다른 list를 반환합니다.

따라서 중간 수행 결과를 생성하는것에 대한 비용은 발생합니다.



이를 방지하기 위해 asSequnce()를 사용하면 중간 리스트를 생성하는 비용을 아낄수 있습니다.

중간에 수행해야할 람다식은 객체의 필드에 저장되면 최종 연산자를 만났을때 호출되면서 사용됩니다.

따라서 중간생성비용은 아낄수 있지만 람다가 inline되지는 않습니다.

 

론적으로 원소의 개수가 작을경우 중간결과 리스트 생성 비용보다 inline되지 않는 함수를 호출하는 비용이 더 들수도 있습니다.

따라서 asSequence() 원소가 많은 경우에만 사용해야 합니다.

 

8.2.4 inline으로 선언해야 함수

inline으로 선언할 경우 함수 호출빈도를 낮출 수 있습니다. 따라서 어떤 함수든 inline으로 선언하면 성능이 개선될것 처럼 보입니다.

하지만 실제로는 람다를 인자로 받는 함수의 경우에만 inline 함수의 성능이 증가 합니다.

 

  1. 일반함수의 경우 JVM의 바이트 코드를 기계어로 변경하는 JIT compiler가 inlining을 강력하게 지원 합니다. 따라서 bytecode에는 함수호출로 보일지라도 실제 JIT까지 컴파일되면 코드가 호출됨에 있어 가장 이익이 되는 형태로 inlining을 진행합니다.
  2. inline 함수는 코드 내용이 호출함수쪽으로 녹아들어 갑니다. 따라서 여기저기 호출부분마다 같은 코드가 추가되면서 중복이 생깁니다. 따라서 내용이 길고 여러곳에서 호출되는 inline 함수는 바이트 코드량을 증가시킵니다.
  3. inline 하지 않으면 함수를 직접 호출하므로 stack trace가 깔끔합니다.

 

그럼 inline 함수를 어디다가 써야 할까요?

  1. 람다가 있는 함수는 JVM이 아직 똑똑하게 inlining을 지원하지 않고 함수로 호출합니다. 따라서 적절한곳에 inline을 사용한다면 부가적인 객체생성을 막고 함수 호출 비용을 줄일 수 있습니다.
  2. inline 함수는 non-local return이 가능합니다. 이는 8.3에서 설명합니다.

 

8.2.5 resource 자동관리

자바 7 부터는 try-with-resource란 구문으로 closable을 구현한 객체에 대해서 자동 close() 호출을 보장 합니다.

코틀린에서의 try문에서는 위 구문을 사용할수 없습니다. (1.6 호환이 목적이므로ㅠ.ㅠ)

 

그 대신 use 라는 함수를 기본 라이브러리가 제공해 줍니다.

fun readFirstLineFromFile(path: String): String { 
	BufferedReader(FileReader(path)).use { br -> return br.readLine() 
    } 
}

use를 쓰게되면 finally에서 close를 강제로 해야 된다든가 하는 번거로움이 없어집니다.

안드로이드에서는 cursor를 쓸때 정말 유용합니다.

use는 exception이 발생하여 비정상 종료 되더라고 close를 호출하도록 되어있으니 안심하고 사용해도 됩니다.

 

8.3 High order function에서의 흐름제어

람다 내부에 return이 들어간다면 어떻게 동작할지에 대해 얘기 합니다.

 

8.3.1 람다 내부의 return

data class Person(val name: String, val age: Int) 

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List) { 
	people.forEach { 
    	if (it.name == "Alice") { 
        	println("Found!") 
            return
        }
   }
   println("Alice is not found")
}

fun main(args: Array) {
	lookForAlice(people)
}

forEach를 사용하여 람대 내부에 return을 넣었습니다.

이 결과는 "Found" 입니다. ("Alice is not found"는 출력되지 않습니다.)

사실 forEach 대신에 그냥 for문을 사용했더라도 같은 동작이 일어납니다.

 

즉 람다 내부에서 return을 사용하면 람다뿐만 아니라 외부의 function까지도 종료됩니다. 이런걸 non-local return 이라고 합니다.

(자신의 외부함수까지 종료함)

이는 자바와 같은 return 동작을 하도록 코틀린 compile가 만들어주기 때문이며, 이렇게 람다 내부에 return을 쓸수 있는건 inline 함수뿐입니다.



8.3.2 label을 이용한 local return

람다내부에 label을 사용하면 람다내부만 빠져나올 수 있습니다.

data class Person(val name: String, val age: Int) 

val people = listOf(Person("Alice", 29), Person("Bob", 31))

fun lookForAlice(people: List) { 
	people.forEach label@{
    	if (it.name == "Alice") return@label 
    }
    println("Alice might be somewhere") 
}

fun main(args: Array) { 
	lookForAlice(people) 
}


람다 뒤에 @라벨 을 붙이고, return뒤에 @라벨명을 다시 붙이면 해당 람다만 빠져 나오는 return이 됩니다.

라벨을 선언하지 않는나면 람다식 이름으로 사용이 가능합니다.

... 
fun lookForAlice(people: List) {
	people.forEach {
    	if (it.name == "Alice") return@forEach 
    }
    println("Alice might be somewhere") 
}
...


this에도 lable을 붙여서 사용할 수 있습니다.

println(StringBuilder().apply sb@ { 
	listOf (12,3).apply { 
    	// this로 reciever object(listOf)에 접근하고 @sb로 다시 StringBuilder에 접근했다. 
        this@sb.append(this.toString())
    }
}

// 결과 
[1,2,3]

 

8.3.3 무명 함수의 local return

label을 이용하여 local return을 만들수도 있지만, 라벨을 붙이는 작업 자체가 가독성이 떨어집니다.

따라서 local return을 위한 무명 함수 anonymous function이 지원됩니다.

data class Person(val name: String, val age: Int)

val people = listOf(Person("Alice", 29), Person("Bob", 31)) 

fun lookForAlice(people: List<Person>) { 
	people.forEach(fun (person) { 
    	if (person.name == "Alice") return
        println("${person.name} is not Alice") 
    }) 
}

fun main(args: Array<String>) { 
	lookForAlice(people) 
}

무명 함수는 함수명과 타입을 생략할 수 있습니다.

코드 작성중간에 local return을 많이 해야하는 경우 무명함수를 이용하면 편리합니다.

반응형

댓글