- 함수 타입
- 고차 함수와 코드를 구조화할 때 고차 함수를 사용하는 방법
- 인라인 함수
- 비로컬 return과 레이블
- 무명 함수
💡 다른 함수를 인자로 받거나 함수를 반환하는 함수
list.filter { x > 0 }
람다를 인자로 받는 함수를 정의하려면 먼저 람다 인자의 타입을 어떻게 선언할 수 있는 지 알아야 한다.
// 타입 추론으로 인해 변수 타입을 지정하지 않아도 된다.
val sum = { x: Int, y: Int -> x, y }
val action = { println(42) }
// 구체적인 타입 선언을 추가한다면 : 파라미터 타입 -> 반환 타입
val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println(42) }
var canReturnNull: (Int, Int) -> Int? = { x, y -> null }
var funOrNull: ((Int, Int) -> Int)? = null
canReturnNull의 타입과 funOrNull의 타입 사이에는 큰 차이가 있다는 사실에 유의하라.
fun performRequest(
url: String,
callback: (code: Int, content: String) -> Unit
) { /*...*/ }
val url = "http://kotl.in"
performRequest(url) { code, content -> /*...*/ }
performRequest(url) { code, page -> /*...*/ }
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
fun String.filter(
predicate: (Char) -> Boolean
): String
fun 수신객체타입.filter(
// 파라미터 이름: 파라미터 함수 타입
파라미터 이름: (파라미터로 받는 함수의 파라미터 타입) -> 파라미터로 받는 함수의 반환 타입
): String
✅ 술어(명제) : '참' 혹은 '거짓'임을 검증할 수 있는 '객관적 사태'가 포함된 문장
fun String.filter(predicate: (Char) -> Boolean): String {
val sb = StringBuilder()
for (index in 0 until length) {
val element = get(index)
if (predicate(element)) sb.append(element)
}
return sb.toString()
}
println("ab1c".filter { it in 'a'..'z' }) // abc
함수 타입인 변수는 인자 개수에 따라 적당한 FunctionN 인터페이스를 구현하는 클래스의 인스턴스를 저장하며, 그 클래스의 invoke 메소드 본문에는 람다의 본문이 들어간다.
// 코틀린
fun processTheAnswer(f: (Int) -> Int) {
println(42)
}
// 자바 8 이후
processTheAnswer(number -> number + 1); // 43
// 자바 8 이전
processTheAnswer {
new Function1<Integer, Integer>() {
@Override
public Integer invoke(Integer number) {
System.out.println(number);
return number + 1;
}
}
}
List<String> strings = new ArrayList();
strings.add("42");
CollectionsKt.forEach(strings, s -> {
System.out.println(s);
return Unit.INSTANCE;
});
✅ 하드 코딩 : 프로그래밍에서 특정 값이나 정보를 소스 코드 내에 직접 입력하여 사용하는 것
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
fun main() {
val stringBuilder = StringBuilder()
val list = listOf(1, 2, 3, 4, 5)
stringBuilder.append("[")
list.joinTo(stringBuilder, transform = { element ->
(element * element).toString()
})
stringBuilder.append("]")
println(stringBuilder.toString()) // [1, 4, 9, 16, 25]
}
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))
}
result.append(postfix)
return result.toString()
}
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
=
뒤에 람다를 넣으면 된다.// buildString 함수 사용하기: stringBuilder를 직접 다룰 필요 없이 문자열 생성
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() }
): String = buildString {
append(prefix)
for ((index, element) in this@joinToString.withIndex()) {
if (index > 0) append(separator)
append(transform(element))
}
append(postfix)
}
fun main() {
val letters = listOf("Alpha", "Beta")
println(letters.joinToString()) // Alpha, Beta
println(letters.joinToString { it.toLowerCase() }) // alpha, beta
println(letters.joinToString(separator = "!", postfix = "! ") { it.toUpperCase() }) // ALPHA! BETA
}
fun foo(callback: (() -> Unit)?) {
// ...
if (callback != null) {
callback()
}
}
// 널이 될 수 있는 함수 타입 파라미터를 사용하기
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: ((T) -> String)? = null
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
val str = transform?.invoke(element)
?: element.toString()
result.append(str)
}
result.append(postfix)
return result.toString()
}
// 함수를 반환하는 함수 정의하기
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 }
}
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
println("Shipping costs ${calculator(Order(3))}") // Shipping costs 12.3
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
}
data class Person(
val firstName: String,
val lastName: String,
val phoneNumbeer: 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.phoneNumbeer != null }
}
}
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()
)) // [Person(firstName=Dmitry, lastName=Jemerov, phoneNumber=123-4567)]
// 사이트 방문 데이터 정의
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.0, OS.ANDROID),
)
// 사이트 방문 데이터를 하드 코딩한 필터를 사용해 분석하기
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
// 윈도우 사용자의 평균 방문 시간 출력
println(averageWindowsDuration) // 23.0
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
println(log.averageDurationFor(OS.WINDOWS)) // 23.0
println(log.averageDurationFor(OS.MAC)) // 22.0
val averageMobileDuration = log
.filter { it.os in setOf(OS.IOS, OS.ANDROID) }
.map(SiteVisit::duration)
.average()
println(averageMobileDuration) // 12.15
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
println(log.averageDurationFor {
it.os in setOf(OS.ANDROID, OS.IOS)
}) // 12.15
// ios 사용자의 /signup 페이지 평균 방문 시간
println(log.averageDurationFor {
it.os == OS.IOS && it.path == "/signup"
}) // 8.0
inline fun <T> synchronzied(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
}
finally {
lock.unlock()
}
}
val l = Lock()
synchronzied(l) {
...
}
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}
fun __foo__(l: Lock) {
// synchronized 함수를 호출하는 foo 함수의 코드 1
println("Before sync")
// synchronized 함수가 인라이닝된 코드 1
l.lock()
try {
// 람다 코드의 본문이 인라이닝된 코드
println("Action")
// synchronized 함수가 인라이닝된 코드 2
} finally {
l.unlock()
}
// synchronized 함수를 호출하는 foo 함수의 코드 2
println("After sync")
}
synchronized 함수의 본문뿐 아니라 synchronized에 전달된 람다의 본문도 함께 인라이닝된다는 점에 유의하라.
class LockOwner(val lock: Lock) {
fun runUnderLock(body: () -> Unit) {
// 람다 대신에 함수 타입인 변수를 인자로 넘긴다.
synchronized(lock, body)
}
}
class LockOwner(val lock: Lock) {
fun __runUnderLock__(body: () -> Unit) {
lock.lock()
try {
// synchronized를 호출하는 부분에서 람다를 알 수 없으므로
// 본문(body())은 인라이닝되지 않는다.
body()
} finally {
lock.unlock()
}
}
}
fun <T, R> Sequence<T>.map(transform: (T) -> R): Sequence<R> {
return TransformingSequence(this, transform)
}
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) {
// ...
}
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter { it.age < 30 }) // [Person(name=Alice, age=29)]
val result = mutableListOf<Person>()
for (person in people) {
if (person.age < 30) result.add(person)
}
println(result) // [Person(name=Alice, age=29)]
println(people.filter { it.age > 30 }
.map(Person::name)) // [Bob]
지연 계산을 통해 성능을 향상시키려는 이유로 모든 컬렉션 연산에 asSequence를 붙여서는 안 된다.
inline 키워드는 람다를 인자로 받는 함수만 성능이 좋아질 가능성이 높다. 다른 경우에는 주의 깊게 성능을 측정하고 조사해봐야 한다.
✅ 스택 트레이스 : 프로그램의 실행 과정에서 호출된 메서드들의 순서와 위치 정보를 나타내는 것
💡 자원 관리 : 람다로 중복을 없앨 수 있는 패턴으로, 어떤 작업을 하기 전에 자원을 획득하고 작업을 마친 후 자원을 해제하는 패턴
✅ 자원 : 컴퓨팅 환경에서 사용되는 파일, 락, 데이터베이스 트랙잭션 등 모든 유한한 시스템 요소들
val l: Lock = ...
// 락을 잠근 다음에 주어진 동작을 수행한다.
l.withLock {
// 락에 의해 보호되는 자원을 사용한다.
}
// 락을 획득한 후 작업하는 과정을 별도의 함수로 분리한다.
fun<T> Lock.withLock(action: () -> T): T {
lock()
try {
return action()
} finally {
unlock()
}
}
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
use 함수
를 자원 관리에 활용할 수 있다.fun readFirstLineFromFile(path: String): String {
// BufferedReader 객체를 만들고 "use" 함수를 호출하면서 파일에 대한 연산을 실행할 람다를 넘긴다.
BufferedReader(FileReader(path)).use { br ->
// 자원(파일)에서 맨 처음 가져온 한 줄을 람다가 아닌 readFirstLineFromFile에서 반환한다.
return br.readLine()
}
}
✅ 닫을 수 있는 자원? : 시스템 리소스를 사용한 후에 반드시 해제해야 하는 객체로, 주로 I/O 관련 작업에 사용되는 파일, 네트워크 연결, 데이터베이스 연결 등이 있다.
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
fun lookForAlice(people: List<Person>) {
for (person in people) {
if (person.name == "Alice") {
println("Found!")
return
}
}
// "people" 안에 엘리스가 없다면 이 줄이 출력된다.
println("Alice is not found")
}
lookForAlice(people) // Found!
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not found")
}
넌로컬(non-local) return
이라 부른다.// 레이블을 통해 로컬 리턴 사용하기
fun lookForAlice(people: List<Person>) {
// 람다에 레이블을 붙이거나 return 뒤에 레이블을 붙이기 위해 @ 사용하기
people.forEach label@{
// return@label은 앞에서 정의한 레이블을 참조한다.
if (it.name == "Alice") return@label
}
println("Alice might be somewhere")
}
lookForAlice(people) // Alice might be somewhere
// 함수 이름을 return 레이블로 사용하기
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") return@forEach
}
println("Alice might be somewhere")
}
람다 식의 레이블을 명시하면 함수 이름을 레이블로 사용할 수 없다는점에 유의하라. 람다식에는 레이블이 2개 이상 붙을 수 없다.
println(StringBuilder().apply sb@ {
listOf(1, 2, 3).apply {
this@sb.append(this.toString())
}
}) // [1, 2, 3]
fun lookForAlice(people: List<Person>) {
// 람다 식 대신 무명 함수를 사용한다.
people.forEach(fun(person) {
// "return"은 가장 가까운 함수를 가리키는데 이 위치에서 가장 가까운 함수는 무명 함수다.
if (person.name == "Alice") return
println("${person.name} is not Alice")
})
}
lookForAlice(people) // Bob is not Alice
// filter에 무명 함수 넘기기
people.filter(fun (person) : Boolean) {
return person.age < 30
}
people.filter(fun (person) = person.age < 30)