[Kotlin in Action] 5장 람다로 프로그래밍
람다식 또는 람다는 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각이다.
람다는 왜 중요할까
람다 소개: 코드 블록을 함수 인자로 넘기기
"이벤트가 발생하면 이 핸들러를 실행하자"나 "데이터 구조의 모든 원소에 이 연산을 적용하자"와 같은 생각을 코드로 표현하기 위해 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야하는 경우 있다.
기존 자바(8 이전)에서는 내부익명클래스를 통해서 해결했지만 함수형 프로그래밍에서는 함수를 값으로 다뤄서 더 간결히 처리한다.
button.setOnClickListener { /* 클릭 시 수행할 동작 */ }
람다는 메소드가 하나뿐인 무명 객체 대신 사용할 수 있다.
람다와 컬렉션
컬렉션을다룰 때 수행하는 작업은 몇 가지 패턴으로 정의할 수 있다. 그런 패턴은 라이브러리에 있는 편이 좋다. 람다가 없다면 컬렉션을 편리하게 처리할 수 있는 좋은 라이브러리를 제공하기 힘들다.
people.maxBy { it.age } // 람다를 사용해 컬렉션 검색하기
people.maxBy(Person::age) // 멤버 참조를 사용해 컬렉션 검색
람다 식의 문법
항상 중괄호 사이 위치한다
{ x: Int, y: Int ->x + y }
val sum = { x: Int, y: Int -> x + y }
sum(1, 2)
run { println(42) } // 람다식 바로 실행하는 함수 run
{ p: Person -> p.age }
문맥을 통해 컴파일러가 유추할 수 있는 인자 타입은 굳이 적을 필요 없다.
함수의 마지막인자가 람다식이면 그 람다를 괄호 밖으로 빼낼 수 잇다.
people.maxBy() { p: Person -> p.age }
외 다른 인자가 없다면 괄호조차 생략 가능
people.maxBy { p: Person -> p.age }
인자가 다수 개의 람다표현식이라면 마지막 람다만 따로 빼내기보단 인자에 둬서 확실히 하라.
일부의 타입만 표현해도 괜찮다.
컴파일러가 타입을 추론하지 못하는 경우는 언제일까?
람다를 변수에 저장할 땐 추론할 문맥이 없어 파라미터 타입 명시해야한다.
본문이 여러 줄로 이뤄진 경우 본문의 맨 마지막에 있는 식이 람다의 결과 값이 된다.
현재 영역 변수에 접근
자바 메소드 안에서 무명 내부 클래스 정의할 시 메소드의 로컬 변수를 무명 내부 클래스에서 사용할 수 있었는데, 람다도 이렇게 할 수 있다.
(클로져?). 자바와 다른 점은 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다는 점이다. 람다 안에서 바깥의 변수를 변경해도 된다. 이러한 변수를 포획한 변수(capture한 변수)라고 부른다. 이 생명주기는 함수가 반환되면 끝난다.
클로저 개념이 나온다. 람다를 실행 시점에 표현하는 데이터 구조는 람다에서 시작하는 모든 참조가 포홤된 닫힌 객체 그래프를 람다 코드와 함께 저장해야 한다. 그런 데이터 구조를 이르는 말. 함수를 쓸모 있는 1급 시민으로 만들려면 포획한 변수를 제대로 처리해야한다. 제대로 처리하기 위해서도 클로저가 필요하고. 그래서 람다를 클로저라고 부르기도 한다. 람다, 무명 함수, 함수 리터럴, 클로저라고 혼용하는 일이 많다.
어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나 다른 변수에 저장한다면 로컬 변수의 생명주기와 함수의 생명주기가 달라질 수 있다. 즉 함수 동작이 끝난ㄷ ㅟ 실행해도 람다의 본문 코드는 여전히 포획한 변수를 읽거나 쓸 수 있다. 어떻게 그런 동작이 가능할까? 파이널 변수를 포획한 경우에는 람다 코드를 변수 값과 함께 저장한다. 파이널이 아닌 경우 변수를 특별한 래퍼로 감싸서 나중에 변경하거나 읽을 수 있게 한 다음, 래퍼에 대한 참조를 람다 코드와 함께 저장한다.
어떻게 이런 일이 일어나느냐 변경 가능한 변수를 저자하는 원소가 단 하나뿐인 배열을 선언하거나, 변경 가능한 변수를 필드로하는 클래스를 선언하는 것이다. 안에 들어있는 원소는 변경가능할 지라도 배열이나 클래스의 인스턴스에 대한 참조를 final로 만들면 포획이 가능하다. 이런 속임수를 코틀린으로 작성하면 다음과 같다. 레퍼런스값을 복사한다는거야?
class Ref<T> (var value: T) // 변경 가능한 변수를 포획하는 방법을 보여주기 위한 클래스
val counter = Ref(0)
val inc = { counter.value++ } // 공식적으로 변경 불가능한 변수를 포획했지만 그 변수가 가리키는 객체의 필드 값을 바꿀 수 있다.
var counter = 0
val inc = { counter++ }
실제 코드에서 이런 래퍼를 만들지 않아도 된다. 대신, 변수를 직접 바꾼다.
어떻게 작동할까. 람다가 파이널 포획하면 자바와 마찬가지로 변수의 값이 복사된다. 하지만 람다가 변경 가능한 변수(var)를 포획하면 변수를 Ref 클래스 인스턴스에 넣는다. 그 Ref 인스턴스에 대한 참조를 파이널로 만들면 쉽게 람다로 포획할 수 있고, 람다 안에서는 Ref 인스턴스의 필드를 변경할 수 있다.
살짝 어렵네..
알아둘 점
람다를 이벤트 핸들러나 다른 비도기적으로 실행ㅎ되는 코드로 활용하는 경우 함수 호출이 끝난 다음에 로컬 변수가 변경될 수도 있다.
fun tryTOCountButtonClicks(button: Button): Int {
var clicks = 0
button.onClick { clicks++ }
return clicks
}
이건 원래 비동기적으로 구현할 때 생기는 일 아니야?
핸들러가 호출되는건 이미 반혼된 뒤이기 때문? 그러니 카운터 변수를 함수의 내부가 아닌 클래스의 프로퍼티나 전역 프로퍼티 등의 위치로 뺴내서 나중에 변수 변화를 살펴볼 수 있게 해야한다.
멤버 참조
이미 만들어진 함수를 람다로 사용할 수도 있다. 익숙한 그녀석이다. val getAge = Person::age (member reference)라고 부른다. 멤버 참조는 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어준다. ::는 클래스 이름과 참조하려는 멤버(프로퍼티나 메소드) 사이에 위치
val getAge = { person: Person -> person.a.age } Person::age 같은 의미
최상위에 선언된, 다른 클래스의 멤버가 아닌 함수나 프로퍼티도 가능
run(::salute)
람다 대신 멤버 참조를 쓸 수도 있다.
생성자 참조
생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다. :: 뒤 클래스 이름 넣으면 만든다. 레이지 로딩?
data class Person(val name: String)
val createPerson = ""Person // Person의 인스턴스를 만드는 동작을 값으로 저장한다.
println(createPerson("Alice"))
그냥 생성자 쓰면 안되는거니... 왜 이러는거야..
?*
확장함수도 똑같이 사용가능
바운드 멤버 참조
클래스의 메소드, 프로퍼티 참조 얻고 그 참조를 호출할 떄 항상 인스턴스 객체를 제공해야했다. 코틀린 1.1부턴 바운드 멤버 참조를 지원한다. 멤버 참조를 생성할 때 클래스 인스턴스를 함께 저장한 다음 나중에 그 인스턴스에 대해 멤버를 호출해준다. 따라서 호출 시 수식 대상 객체를 별도로 지정해 줄 필요 없다.
val p = Person("Dmitry", 34)
val personsAgeFunction = Person::age // getter인가봐 사용할때 인자로 객체 넣어줘야해
println(personsAgeFunction(p))
val dmitrysAgeFunction = p::age // 클래스 레퍼런스를 앞에 붙여준다.
println(dmitrysAgeFunction()) // 클래스 레퍼런스껄 뱉어준다.
컬렉션 함수형 API
함수형 프로그래밍 스타일 사용하며 컬렉션 다룬다. 함수형 폴그래밍에서 람다나 다른 함수를 인자로 받거나 반환하는 ㅎ마수를 고차함수라 부른다. 기본 함수를 조합해서 새로운 연산을 정의하거나 다른 고차와 조합해 더 복잡한 정의 가능하다. 이러한 기법을 컴비네이션 기법이라 부른다. 조합할 때 사용되는 고차 함수를 컴비네이터라 한다.
filter
컬렉션을 활용할 때 기반이 되는 함수다.
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 })
결과는 입력 원소 중 주어진 술어(predicate)를 만족하는 원소만으로 이루어진 새로운 컬렉션이다.
list.filter { it.age > 30 }
원치않는 원소를 제거할 순 있어도 원소를 변환할 순 없다.
map
원소를 변환한다. 적용한 결과를 모아 새 컬렉션으로 만든다.
list.map { it * it }
멤버 참조를 사용해 더 멋지게 작성 가능
people.map(Person::name)
체이닝 가능
list.filter { it.age >= 30 }.map(People::name)
나이 최댓값 구하고 동년배들 싹다 모아
people.filter { it.age == people.maxBy(Person::agee)!!.age }
여기서는 최대값 구하는 작업을 계속 반복하는 단점 존재.
맥스를 밖에 뺴라.
맵에도 적용 가능
val numbers = mapOf(0 to "zero", 1 to "one")
println(numbers.mapValues { it.value.toUpperCase() })
filterKeys, mapKeys 키, filterValues, mapValues
all, any, count, find: 컬렉션에 술어 적용
모든 원소가 어떤 조건을 만족하는지 판단하는 연산 많다.
all 모든 녀석이?, any 하나라도?. count는 조건을 만족하는 운소의 개수를 반환하며, find 함수는 조건을 만족하는 첫 번째 원소를 반환한다.
어떤조건에 대해 !all을 수행한 결과와 그 조건의 부정에 any를 수행한 건 같다. 가독성을 높이려면 any와 all 앞에 !를 붙이지 않는 편이 낫다.
val list = listOf(1, 2, 3)
println(!list.all { it == 3 }) 앞쪽 !를 못 볼 가능성 높아
println(list.any { it != 3 }) 바꾸려면 술어를 부정해야한다.
참고 함수를 적재적소에 사용하라: count와 size
count 까먹고 컬렉션 필터링한 결과의 크기를 가져오는 경우 있다.
people.filter(canBeInClub27).size
이렇게 처리하면 조건을 만족하는 모든 원소가 들어가는 중간 컬렉션이 생긴다. count는 조건을 만족하는 원소의 개수만 추적해서 더 효율적이다.
술어만족하는 원소 하나만 찾을땐 find 써라. 없을 경우 null 리턴. firstOrNull과 같다네. 조건을 만족하는 원소가 없으면 null이 나온다는 사실을 명확히 하고싶으면 firstOrNull을 쓸 수 있다.
groupBy: 리스트를 여러 그룹으로 이뤄진 맵으로 변경
컬렉션 모든 원소를 특성에 따라 여러 그룹으로 나누고 싶을 때. 나이에 따라 분류해보자.
people.groupBy { it.age })
문자열을 첫 글자에 따라 분류하는 코드
val list = listOf("a", "ab", "b")
println(list.groupBy(String::first))
{ a=[a, ab], b = [b] }
flatMap, faltten: 중첩되 컬렉션 안의 원소 처리
class Book (val title: String, val authors: List<String>)가 있다.
books.flatMap { it.authors }.toSet() // books 컬렉션에 있는 책을 쓴 모든 저자의 집합 2차원일 authors 리스트를 쫙 펴서 set으로 만드네
flatMap 함수는 먼저 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고 (또는 매핑하고) 람다를 적용한 결과 얻어지는 여러 리스트를 한 리스트로 한데 모은다. 또는 펼치기(flatten) 문자열에 적용해보자.
val strings =listOf("abc", "def")
println(strings.flatMap { it.toList() })
abc, def를 각 두 리스트로 만들고 한 곳에 모인다.
중첩된거에 변환할꺼 없이 평탄화만 하려면 flatten()하면 된다.
지연 계산 컬렉션 연산
지금까지 애들은 즉시 생성한다 eagerly. 이는 컬렉션 함수를 연쇄하면 매 단계마다 계산 중간 결과를 새로운 컬렉션에 임시로 담는다는 말이다. 시퀀스를 사용하면 중간 임시 컬렉션 없이 할 수 있다.
people.asSequence() // 원본 컬렉션을 시퀀스로 바꾼다.
.map(Person::name) // 시퀀스도 컬렉션과 똑같은 API를 제공한다.
.filter { it.startsWith("A") }
.toList() // 결과 시퀀스를 다시 리스트로 변환
시퀀스의 원소는 필요할 때 비로소 계산된다. 중간 처리 결과를 저장하지 않고 효율적으로 처리.
어떤 컬렉션이든 시퀀스로 바꿀 수 있다.
큰 컬렉션에 대해 연산을 연쇄시킬 때는 시퀀스를 사용하라. 중간 컬렉션을 생성함에도 불구하고 코틀린에서 즉시 계산 컬렉션에 대한 연산이 더 효율적인 이유를 설명한다. 하지만 컬렉션에 들어있는 원소가 많으면 중간 원소를 재배열하는 비용이 커지기에 지연 계산이 낫다.
시퀀스에 대한 연산을 지연 계산하기에 정말 계산을 실행하게 만들려면 최종 시퀀스의 원소를 하나씩 이터레이션하거나 최종 시퀀스를 리스트로 변환해야한다.
시퀀스 연산 실행: 중간 연산과 최종 연산
중간 연산은 다른 시퀀스를 반환하며 원소를 변환하는 방법을 안다. 최종 연산은 결과를 반환하고 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 운소, 숫자 또는 객체이다.
한 원소씩 모든 변환을 적용하기에 find 같은걸로 하나찾으면 땡인 녀석들이 나오면 모든 원소한테 적용안되고 나와버릴 수 있다.
filter 먼저해서 원소 가능한 줄이고 map 하자.
자바의 스트림과 시퀀스가 같다는걸 알 수 있다. 자바8을 채택하면 현재 코틀린 컬렉션과 시퀀스에서 제공하지 않는 중요 기능 사용 가능하다. CPU에서 병렬저으로 실행하는 기능이 그것이다. 필요에 따라 자바 버전에 따라 시퀀스와 스트림 중 적절한거 써라. parellel 그거 말하나?
시퀀스 만들기
generateSequence()
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
numbersTo100.sum()
섬하기 전에 실행안된다. 계산말이야.
시퀀스를 사용하는 일반적인 사례는 하나는 객체의 조상으로 이뤄진 시퀀스를 만들어내는거다. 어떤객체의 조상이 자신과 같은 타입이고 모든 조상의 시퀀스에서 어떤 특성을 알고싶을 떄가 있다. 상위디렉 뒤지며 숨김 속성을 가진 디렉터리가 있는지 검사함으로 파일이 감춰진디렉터리 안에 들어있는지 알아본다.
자바 함수형 인터페이스 활용
자바 8 이전의 자바에선 인터페이스만 있는 객체 인자에 값 넣으려면 무명클래스의 인스턴스를 만들어야했다. 새로 클래스 파일 안 만들어서 좋긴한다. 인터페이스를 구현하는 녀석. 그래도 보기 안 좋아.
대신 람다를 넣으면 된다.
왜냐하면 함수형 인터페이스 또는 SAM 인터페이스라 해서 단일 추상 메소드이다. 깔끔하다. 코틀린에서 함수를 인자로 받을 필요가 있는 함수는 함수형 인터페이스가 아닌 함수 타입을 인자 타입으로 해야한다. 코틀린 함수를 사용할 떄는 코틀린 컴파일러가 코틀린 람다를 함수형 인터페이스로 변환해주지 않는다.
함수에 람다 넘기면 컴파일러가 자동으로 그런 무명 클래스와 인스턴스를 만들어준다.이 때 유일한 추상 메소드의 본문을 람다 본문으로 사용한다.
인자로 객체 식을 넘겨줄 수도 있따. 람다와 무명 객체 사이에 차이있다. 객체를 명시적으로 선언하면 메소드를 호출할 때마다 새로운 객체가 생성된다. 람다는 다르다. 정의가 들어있는 함수의 변수에 접근하지 않는 람다에 대응하는 무명 객체를 메소드를 호출할 때마다 반복 사용한다. 프로그램 전체에서 이 익명 인스턴스는 단 하나만 만들어진다는 뜻이다.
똑같이 행동하는 인스턴스 인자 케이스는 다음과 같다.
val runnable = Runnable { println(42) } // 전역 변수로 컴파일되므로 프로그램안에서 단 하나의 인스턴스만 존재한다.
fun handleComputation() {
postponeComputation(1000, runnable) // 모든 handleComputation 호출에 같은 객체를 사용한다.
}
람다가 주변 영역의 변수를 포획한다면 매 호출마다 같은 인스턴스를 사용할 수 없다. 그럴 경우 컴파일러가 매번 새로운 인스턴스를 생성한다.
람다의 자세한 구현
코틀린 1.0에서 인라인되지 않은 모든 람다식은 무명 클래스로 컴파일된다. 1.1부턴 자바8 바이트코드로 생성할 수 있지만(-jvm-target 1.8이라고 호출할 때 지정해줘야한다.), 여전히 코틀린 1.0처럼 람다마다 별도의 클래스를 만들어낸다. 향후 별도의 클래스를 만들지 않고 자바 8부터 도입된 람다 기능을 활용한 바이트 코드를 만들어낼 계획이다.
람다가 변수를 포획하면 무명 클래스 안에 포획한 변수를 저장하는 필드가 생기며, 매 호출마다 그 무명 클래스의 인스턴스를 새로 만든다. 하지만 포획하는 변수가 없는 람다에 대해서는 인스턴스가 단 하나만 생긴다. HandleComputation$1처럼 람다가 선언된 함수 이름을 접두사로 하는 이름이 람다를 컴파일한 클래스에 붙는다. 디컴파일하면 볼 수 있다.
컬렉션을 확장한 메소드에 람다를 넘길 경우 코틀린은 이런 방식을 사용하지 않는다. 코틀린 inline으로 표시된 코틀린 함수에게 람다를 넘기면 아무런 무명 클래스도 만들어지지 않는다. 대부분 코틀린 확장 함수들은 inline 표시가 붙어있다.
지금까지 자바 코틀린 변화은 자동으로 이루어졌다. 컴파일러 해줘서. 허나 수동적으로 해야할 때가 있다.
SAM 생성자: 람다를 함수형 인터페이스로 명시적으로 변경
SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수다. 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다. 예를 들어 함수형 인터페이스의 인스턴스를 반환하는 메소드가 있다면 람다를 직접 반화할 수 없고, 반환하고픈 람다를 SAM 생성자로 감싸야 한다.
fun createAllDoneRunnable(): Runnable {
return Runnable { println("All done!") }
}
createAllDoneRunnable().run()
SAM 생성자의 이름은 함수형 인터페이스의 이름과 같다. SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메소드의 본문에 사용할 람다만을 인자로 받아서 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한다. 함수형 인터페이스 인스턴스로 만들어 변수에 저장해 활용할 수 있다네.. 하와와... 객체 선언으로 할 수도 있지만 SAM 생성자 쓰는 쪽이 편해.. 그래서 언제 자동으로 못 바꿔주는 거야...함수형 인터페이스의 인스턴스를 반환하는 메소드가 있다면 람다를 직접 반환할 수 없다라..뭔말?* 기존 방식으로 인스턴스에 접근해 못가져온단 말인가?
람다는 단순 코드 블럭 this 못 써. 스스로를 참조 못해. 이벤트 리스너가 이벤트 처리하다가 자기 자신으 ㅣ리스너를 등록 해제해야하면 람다를 쓸 수 없어. 무명 객체를 사용해 리스너를 구현해. this를 사용할 수 있거든.
가끔 오버로드한 메소드 중에서 어떤 타입의 메소드를 선택해 람다를 변환해 넘겨줘야 할지 모호한 때가 있다. 그런 경우 명시적으로 SAM 생성자를 적용해서 파훼 가능.
수신 객체 지정 람다: with와 apply
수신 객체 지정 람다라고 부른다.
with
어떤 객체의 이름을 반복하지 않고도 그 객체에 대해 다양한 연산을 수행할 수 있다면 좋을 것이다.
with 함수는 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다으 수신 객체로 만든다. 대박. this를 참조해서 접근해도 괜찮고 내부에서
참조하지 않고 그냥 적얻 상관없다. 이게 신기해! 단순히 전달받은 인자에 불과하다며!
수신 객체 지정 람다와 확장 함수 비교
확장 함수 안에서 this는 그 함수가 확장하는 타입의 인스턴스를 가리킨다. 그리고 그 수신 객체 this의 멤버를 호출할 때는 this.를 생략가능..
어떤 의미에서는 확장함수를 수신 객체 지정 함수라 할 수도 있다. 다음과 같은 관계 유추 가능
일반 함수 | 일반 람다
확장 함수 | 수신 객체 지정 람다
람다는 일반 함수와 비슷한 동작을 정의하는 한 방법이다. 수신 객체 지정 람다는 확장 함수와 비슷한 동작을 정의하는 한 방법이다.
with 안에서 생성자 호출해서 만들 수도 있어 밖에서 만듥 넣을 필요 없이.
참고 메소드 이름 충돌
with에게 인자로 넘긴 객체의 클래스와 with를 사용하는 코드 클래스 안에 같은 이름의 메소드 있으면 this 참조앞에 레이블을 붙여 호출하고 싶은 애를 정한다.
this@OuterClass.toString()
with가 반환하는 값은 람다 코들르 실행한 결과며, 그 결과는 람다 식의 본문에 있는 마지막 식의 값이다. 하지만 때로는 람다의 결과 대신 수신 객체가 필요한 경우도 있다. 그럴 때는 apply 라이브러리 함수를 사용할 수 있다.
with는 변환을 하고 apply이는 내부에서 함 쓰고 들어올떄의 원본을 다시 뱉는건가?
apply
항상 자신에게 전달된 객체를 반환한다. 직접 작업하고 내보낼 때 좋네.
얘는 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화해야하는 경우 유용하다. 자바에서는 보통 Bulder 객체가 이런 역할을 담당한다.
byildString의 인자는 수신 객체 지정 람다며, 수신 객체는 항상 StrinBuilder가 된다.
StringBuilder를 활용해 String 만들때 사용하는 해법.
11장 DSL 다룰 때 더 흥미진진한 예제를 볼 수 있다. 수신 객체 지정 람다는 DSL을 만들 때 매우 유용하다.
요약
- 람다를 사용하면 코드 조각을 다른 함수에게 인자로 넘길 수 있다.
- 코틀린에서는 람다가 함수 인자인 경우 괄호 밖으로 람다를 뺴낼 수 있고, 람다의 인자가 단 하나뿐인 경우 인자 이름을 지정하지 않고 it이라는 디폴트 이름으로 부를 수 있다.
- 람다 안에 있는 코드는 그 람다가 들어있는 바깥 함수의 변수를 읽거나 쓸 수 있다.
- 메소드, 생성자, 프로퍼티의 이름 앞에 ::을 붙이면 각각에 대한 참조를 만들 수 있다. 그런 참조를 람다 대신 다른 함수에게 넘길 수 있다.
- filter, map, all, any 등의 함수를 활용하면 컬렉션에 대한 대부분의 연산을 직접 원소를 이터레이션하지 않고 수행할 수 있다.
- 시퀀스를 사용하면 중간 결과를 담는 컬렉션을 생성하지 않고도 컬렉션에 대한 여러 연산을 조합할 수 있다.
- 함수형 인터페이스를 인자로 받는 자바 함수를 호출할 경우 람다를 함수형 인터페이스 인자 대신 넘길 수 있다.
- 수신 객체 지정 람다를 사용하면 람다 안에서 미리 정해둔 수신 객체의 메소드를 직접 호출할 수 있다.
- 표준 라이브러리의 with 함수를 사용하면 어떤 객체에 대한 참조를 반복해서 언급하지 않으면서 그 객체의 메소드를 호출할 수 있다. apply를 사요하면 어떤 객체라도 빌더 스타일의 API를 사용해 생성하고 초기화할 수 있다.
?* 실제 코틀린으로 코딩할 때 람다함수를 인자로 받게끔 많이 API를 설계하나?