- 함수 타입
- 고차 함수와 코드를 구조화할 때 고차 함수를 사용하는 방법
- 인라인 함수
- 비로컬 return과 레이블
- 무명 함수
고차 함수
람다를 인자로 받거나 반환하는 함수
-> 코드를 간결, 중복 없애고, 더 나은 추상화를 구축
인라인 함수(inline)
람다를 사용함에 따라 발생할 수 있는 성능상 부가 비용을 없애고 람다 안에서 더 유연하게 흐름을 제어할 수 있는 특성인 함수
고차 함수를 정의하려면 함수 타입에 대해 먼저 알아야 한다
// 타입 추론
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
↪ 컴파일러는 sum과 action이 함수 타입임을 추론한다
// 변수 구체적 타입 선언
val sum :(Int, Int) -> Int = { x, y-> x + y }
val action: () -> Unit = { println(42) }
그냥 함수를 정의한다면 함수의 파라미터 목록 뒤에 오는 Unit 반환 타입 지정을 생략해도 되지만,
함수 타입을 선언할 때는 반환 타입을 반드시 명시해야 하므로 Unit을 빼먹어서는 안 된다
함수 타입에서도 반환 타입을 널이 될 수 있는 타입으로 지정 할 수 있다
var canReturnNull:(Int, Int) -> Int? = {x, y -> null}
var funOrNull: ((Int, Int) -> Int)? = null
// 간단한 고차 함수 정의하기
fun twoAndThree(operation: (Int, Int) -> Int) { //함수 타입인 파라미터를 선언한다
val result = operation(2, 3) // 함수 타입인 파라미터를 호출한다
println("The result is $result")
}
>>> twoAndThree { a,b -> a + b }
the result is 5
>>> twoAndThree { a,b -> a * b }
the result is 6
컴파일된 코드 안에서 함수 타입은 일반 인터페이스로 바뀐다
-> 함수 타입의 변수는 FuntionN 인터페이스를 구현하는 객체를 저장한다
함수 인자의 개수에 따라
Funtion0<R>(인자가 없는 함수)
, Funtion1<P1,R>(인자가 하나인 함수)
등의 인터페이스를 제공
-> 각 인터페이스에는 invoke 메소드 정의가 하나 들어 있다
-> invoke를 호출하면 함수를 실행할 수 있다
함수 타입인 변수는 인자 개수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하며, 그 클래스의 invoke 메소드 본문에는 람다의 본문이 들어간다
자바 8에서는 람다를 넘기면 자동으로 함수 타입의 값으로 변환되고,
자바 8 이전의 자바에서는 필요한 FunctionN 인터페이스의 invoke 메소드를 구현하는 무명 클래스를 넘기면 된다
파라미터를 함수 타입으로 선언할 때도 디폴트 값을 정할 수 있다
JoinToString을 호출할 때마다 매번 람다를 넘기게 만들면 불편할 때가 있다
-> 함수 타입의 파라미터에 대한 디폴트 값을 지정하면 문제 해결
-> 디폴트 값으로 람다 식을 넣으면 된다
fun <T> Collection<T>.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)) // "transform" 파라미터로 받은 함수를 호출한다.
}
result.append(postfix)
return result.toString()
}
fun main(args: Array<String>) {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString()) // 디폴트 변환 함수를 사용한다. -> Alpha, Beta
println(letters.joinToString { it.toLowerCase() }) // 람다를 인자로 전달한다. -> alpha, beta
println(letters.joinToString(separator = "! ", postfix = "! ",
transform = { it.toUpperCase() })) // 이름 붙은 인자 구문을 사용해 람다를 포함하는 여러 인자를 전달한다. -> ALPHA! BETA!
}
함수 타입에 대한 디폴트 값을 선언할 때 특별한 구문이 필요하지 않다
-> = 뒤에 람다를 넣으면 된다
널이 될 수 있는 함수 타입을 사용할 수 도 있다
-> 널이 될 수 있는 함수 타입으로 함수를 받으면 그 함수 타입을 직접 호출할 수 없다는 점에 유의
data class Person(
val firstName: String,
val lastName: String,
val phoneNumber: String?
)
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
fun getPredicate(): (Person) -> Boolean { // 함수를 반환하는 함수를 정의한다.
val startsWithPrefix = { p: Person ->
p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startsWithPrefix // 함수 타입의 변수를 반환한다.
}
return { startsWithPrefix(it)
&& it.phoneNumber != null } // 람다를 반환한다.
}
}
fun main(args: Array<String>) {
val contacts = listOf(Person("Dmitry", "Jemerov", "123-4567"),
Person("Svetlana", "Isakova", null))
val contactListFilters = ContactListFilters()
with (contactListFilters) {
prefix = "Dm"
onlyWithPhoneNumber = true
}
println(contacts.filter(
contactListFilters.getPredicate()))
}
// 웹사이트 방문 기록을 분석하는 예시
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()
// 일반 함수를 통해 중복 제거하기
// mobile 사용자의 평균 방문 시간
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
fun main(args: Array<String>) {
println(log.averageDurationFor(OS.WINDOWS))
println(log.averageDurationFor(OS.MAC))
}
// 고차 함수를 통해 중복 제거하기
// ios 사용자의 /signup 페이지 평균 방문 시간
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
fun main(args: Array<String>) {
println(log.averageDurationFor {
it.os in setOf(OS.ANDROID, OS.IOS) })
println(log.averageDurationFor {
it.os == OS.IOS && it.path == "/signup" })
}
인라인 함수를 사용한다면
람다식을 사용했을 때 무의미한 객체 생성을 막을 수 있습니다
// 일반 함수로 작성 시
fun print()
{
println("Hello, world!")
}
fun main()
{
print()
}
// 인라인 함수 사용 시
inline fun print()
{
println("Hello, world!")
}
fun main()
{
print()
}
// 컴파일 시에 아래 처럼 컴파일 된다
fun main()
{
println("Hello, world!")
}
// 다른 예시
fun doSomething() {
println("Before lambda")
doSomethingElse {
println("Inside lambda")
}
println("After lambda")
}
1)
fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}
위에를 컴파일 한다면
public static final void doSomething() {
System.out.println("Before lambda");
doSomethingElse(new Function() {
public final void invoke() {
System.out.println("Inside lambda");
}
});
System.out.println("After lambda");
}
-> 새로운 객체를 생성하여 넘겨준다
2)
inline fun doSomethingElse(lambda: () -> Unit) {
println("Doing something else")
lambda()
}
위에를 컴파일 한다면
public static final void doSomething() {
System.out.println("Before lambda");
System.out.println("Doing something else");
System.out.println("Inside lambda");
System.out.println("After lambda");
}
-> 무의미하게 Function 객체를 항상 만들어내는 것이 없어졌다
인라이닝을 하는 방식으로 인해 람다를 사용하는 모든 함수를 인라이닝할 수는 없다
-> 람다가 본문에 직접 펼쳐지기 때문에 함수가 파라미터로 전달받은 람다를 본문에 사용하는 방식이 한정될 수 밖에 없다
-> 파라미터로 받은 람다를 다른 변수에 저장하고 나중에 그 변수를 사용한다면 람다를 표현하는 객체가 어딘가는 존재해야 하기 때문에 람다를 인라이닝할 수 없다
둘 이상의 람다를 인자로 받는 함수에서 일부 람다만 인라이닝하고 싶을 때가 있다면
-> 인라이닝하면 안되는 람다를 파라미터로 받는다면
noinline 변경자를 파라미터 이름 앞에 붙여서 인라이닝을 금지할 수 있다
inline fun doSomething(action1: () -> Unit, action2: () -> Unit) {
action1()
anotherFunc(action2) // error
}
fun anotherFunc(action: () -> Unit) {
action()
}
fun main() {
doSomething({
println("1")
}, {
println("2")
})
}
inline 함수는 내부적으로 코드를 복사하기 때문에, 인자로 전달받은 함수는 다른 함수로 전달되거나 참조할 수 없다
위의 코드에서 doSomething()에 두 번째 인자로 넘겨받은 action2를 또 다른 고차 함수인 anotherFunc()에 인자로 넘겨주려 하고 있다
이때 doSomething()은 inline 함수로 선언되어 있기 때문에 인자로 전달받은 action2를 참조할 수 없기 때문에 전달하는 것이 불가능하다
이렇게 모든 인자를 inline으로 처리하고 싶지 않을 때 사용하는 것이 noinline 키워드다
inline fun doSomething(action1: () -> Unit, noinline action2: () -> Unit) {
action1()
anotherFunc(action2)
}
fun anotherFunc(action: () -> Unit) {
action()
}
fun main() {
doSomething({
println("1")
}, {
println("2")
})
}
// 람다를 사용해 컬렉션 거르기
data class Person(val name: String, val age: Int)
val people = listOf(Person("Kim",29), Person("Park",30))
>>> println(people.filter { it.age < 30 }
[Person(name = Kim, age = 29)]
// 컬렉션 직접 거르기
>>> val result = mutableListOf<Person>()
>>> for(person in people) {
if(person.age < 30) result.add(person
}
둘의 바이트코드는 거의 동일
filter 함수는 인라인 함수이다
filter와 map을 연쇄해서 사용하면 두 함수의 본문은 인라이닝되며, 추가 객체나 클래스 생성은 없다
-> 💣 리스트를 걸러낸 결과를 저장하는 중간 리스트를 만든다
asSequence를 통해 리스트 대신 시퀀스를 사용하면 중간 리스트로 인한 부가 비용은 줄어든다
-> 중간 시퀀스는 람다를 필드에 저장하는 개체로 표현, 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출
크기가 큰 컬렉션에만 asSequence를 붙이는게 낫다!
일반 함수 호출의 경우 JVM은 이미 강력하게 인라이닝을 지원
람다를 인자로 받는 함수를 인라이닝하면 얻게되는 이익
람다로 중복을 없앨 수 있는 일반적인 패턴 중 한 가지
-> 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 자원 관리
use 함수
닫을 수 있는(closeable) 자원
에 대한 확장 함수, 람다를 인자로 받는다
람다를 호출한 다음에 자원을 닫아준다
-> 람다가 정상 종료한 경우는 물론 람다안에서 예외가 발생한 경우에도 자원을 확실히 닫는다
use 함수도 인라인 함수
// 일반 함수 사용시
fun lambdaParam(a: Int, b: Int, lambda: (Int, Int) -> Unit){
lambda(a, b)
}
fun func(){
println("start of func")
lambdaParam(13, 3) { a, b ->
val result = a+b
if(result > 10) return
println("result: $result")
}
println("end of func")
}
>>> 에러 발생
// inline 함수 사용시
inline fun lambdaParam(a: Int, b: Int, lambda: (Int, Int) -> Unit){
lambda(a, b)
}
fun func(){
println("start of func")
lambdaParam(13, 3) { a, b ->
val result = a+b
if(result > 10) return
println("result: $result")
}
println("end of func")
}
람다 식에서도 로컬 return을 사용할 수 있다
람다 안에서 로컬 return은 for 루프의 break와 비슷한 역할을 한다
return으로 실행을 끝내고 싶은 람다 식 앞에 레이블을 붙이고,
return 키워드 뒤에 그 레이블을 추가하면 된다
1) 레이블을 통해 로컬 리턴 사용
people.forEach label@{
if(it.name == "Kim") return@label
}
2) 함수 이름을 return 레이블로 사용
people.forEach {
if(it.name == "Kim") return@forEach // 람다식으로부터 반환 시킨다
}
// 무명 함수 안에서 return 사용하기
fun lookForAlice(people: List<Person>) {
people.forEach(fun (person) { // 람다 식 대신 무명 함수 사용
if (person.name == "Alice") return
// return은 가장 가까운 함수를 가리키는데
// 이 위치에서 가장 가까운 함수는 무명 함수
println("${person.name} is not Alice")
})
}
// filter에 무명 함수 넘기기
people.filter (fun (person): Boolean {
return person.age < 30
})
블록이 본문인 무명 함수는 반환 타입을 명시해야 하지만,
식을 본문으로 하는 무명 함수의 반환 타입은 생략 가능
// 식이 본문인 무명 함수 사용하기
people.filter(fun (person) = person.age < 30)
무명 함수 안에서 레이블이 붙지 않은 return 식은 무명 함수 자체를 반환시킬 뿐 무명 함수를 둘러싼 다른 함수를 반환시키지 않는다
return은 fun 키워드를 사용해 정의된 가장 안쪽 함수를 반환시킨다
-> 무명 함수는 fun을 사용해 정의되므로 그 함수 자신이 반환 된다
--