함수 타입


함수 타입 정의

  • (T) -> Boolean : Boolean을 반환하는 함수 Predicate
  • (T) -> R : 값 하나를 다른 값으로 변환하는 함수 transfrom
  • (T) -> Unit : Unit을 반환하는 함수 operation

함수 타입 활용

  • invoke라는 단 하나의 메서드만 제공함, 명시적 invoke호출과 ()연산자로 호출
  • 함수타입 파라미터를 ()? 로 감싸서 널러블함을 표현할 수 있음 (이경우는 명시적 invoke만 가능)

named parameter

  • 함수 타입을 정의 할 때 ’named parameter’를 사용 가능
  • 오직 개발 편의를 위한 것

익명 함수


  • 익명함수는 함수 타입 객체를 반환하는 표현식
  • generic, default parameter는 지원하지 않음
val add2 = fun(a: Int, b: Int) = a + b
  • 익명함수 자체는 요즘 사용하지 않는다고 함
    • 람다가 더 짧고 지원이 더 잘됨
    • 인텔리제이는 람다만 힌트를 제공
  • 그래도 아래와 같은 상황에는 아직 유용
    • return 범위 명확히 구분하고 싶을 때
    • 타입 명시적 선언 (람다보다 깔끔)
    • return을 명시적으로 사용해야 할 때
    • 고차함수 인자가 2개 이상이고 복잡할 때

람다 표현식


코틀린에서 람다 표현식(lambda expression)은 return을 허용하지 않는 이유는, 람다가 그 자체로 “독립적인 실행 흐름”을 갖는 함수가 아니라, “호출되는 컨텍스트에 의존하는 작은 코드 블록”이기 때문입니다. 즉, 람다 안에서 return을 자유롭게 허용해버리면, 람다를 호출하는 “바깥 함수” 전체의 흐름까지 예측 불가능하게 망칠 수 있기 때문에, 명확한 제약을 둔 것입니다.

  • 익명함수보다 더 짧은 대안
  • 람다 표현식이 더 많은 기능을 지원
항목 익명 함수 (anonymous function) 람다식 (lambda expression)
작성 방식 fun 키워드 사용 { 파라미터 -> 본문 } 형태
return 동작 로컬 return (해당 함수만 빠져나감) 기본적으로 바깥 함수로 비탈출(non-local return) 가능
타입 추론 명확한 타입 명시 가능 타입 추론 많이 의존
제어문 사용 return, break, continue 자유롭게 사용 가능 제한 있음 (non-local return 조심)

함수를 나타내는 객체가 결괏값으로 생성되는 표현식을 함수 리터럴이라 한다

  • 타입 추론을 잘 이용하는게 좋다.
  • 람다의 변수명에 타입을 명시, 혹은 람다 작성시 타입을 명시해서 타입추론을 잘 하게 만드는게 좋다
  • it 키워드를 이용하기에도 도움이 된다.
  • 그 외에는 후행람다와 같은 간단한 문법 정의 이야기이다.

함수 참조


객체로 사용할 수 있는 함수가 필요하다면 람다 표현식으로 새로운 객체를 생성할 수도 있지만, 기존의 함수를 참조할 수도 있습니다.

  • :: 또는 Receiver:: 로 함수 참조가 가능
class Complex(
    val real: Double,
    val imaginary: Double,
)

fun main() {
    val complex: ()-> Complex = ::makeComplex
    val complex2: (Double)-> Complex = ::makeComplex
    val complex3: (Double, Double)-> Complex = ::makeComplex
}

fun makeComplex(
    real: Double = 0.0,
    imaginary: Double = 0.0
) = Complex(real, imaginary)
  • 최신버전에는 디폴트파라미터들도 다 고려해서 타입정의를 허용해주는 것 같다.
  • 메소드를 참조하는경우 리시버를 명시해줘야함
data class Complex(val real: Double, val imaginary: Double) {
    fun doubled(): Complex =
        Complex(this.real * 2, this.imaginary * 2)

    fun times(num: Int) =
        Complex(real * num, imaginary * num)
}

fun main() {
    val c1 = Complex(1.0, 2.0)
    val f1: (Complex) -> Complex = Complex::doubled
    println(f1(c1)) // Complex(real=2.0, imaginary=4.0)
    val f2: (Complex, Int) -> Complex = Complex::times
    println(f2(c1, 4)) // Complex(real=4.0, imaginary=8.0)
  • 메서드 참조는 프로퍼티가 아닌 타입을 이용해야함 (제네릭 타입 명시 포함)
val func = list.sum // err
val func: (List<Int>) -> Int = List<Int>::sum
  • 한정된 함수 참조(bound reference)를 쓰고 싶으면 리시버 객체를 고정해서 참조할 수 있다.
  • 이때는 타입::메서드 형태가 아니라 객체::메서드 형태로 작성해야 한다.
  • 이렇게 하면 리시버를 따로 넘기지 않고, 나머지 인자만 받는 함수처럼 쓸 수 있다.
val c1 = Complex(1.0, 2.0)

val f3: () -> Complex = c1::doubled
println(f3()) // Complex(real=2.0, imaginary=4.0)

val f4: (Int) -> Complex = c1::times
println(f4(3)) // Complex(real=3.0, imaginary=6.0)
  • Complex::doubled은 (Complex) -> Complex였는데
  • c1::doubled는 () -> Complex로 바뀜. (리시버가 고정됐으니까 인자를 따로 받을 필요가 없어짐)
  • Complex::times는 (Complex, Int) -> Complex였는데
  • c1::times는 (Int) -> Complex로 변함. (리시버인 c1이 이미 채워진 상태)

요약하면,

타입::메서드 → 리시버를 따로 받아야 함 객체::메서드 → 리시버가 고정돼 있어서 나머지 인자만 받음

// MainPresenter는 view를 가지고 있음
class MainPresenter(
    private val view: MainView,
    private val repository: MarvelRepository
) : BasePresenter() {

    fun onViewCreated() {
        // getAllCharacters()는 Single<List<MarvelCharacter>>를 반환한다고 가정
        subscriptions += repository.getAllCharacters()
            .applySchedulers()
            .subscribeBy(
                // onSuccess에 this::show 넘김
                // => subscribeBy가 데이터를 넘겨줄 때, this.show(items)로 호출
                onSuccess = this::show,
                onError = view::showError
            )
    }

    // 이 show 함수는 List<MarvelCharacter>를 받아서 view에 넘김
    fun show(items: List<MarvelCharacter>) {
        view.show(items)
    }
}
  • subscribeBy는 그냥 콜백 함수를 저장해두고 나중에 호출
  • subscribeBy는 그 콜백 함수(this::show)가 어떤 객체(this)를 품고 있는지 몰라도 됨
  • this::show는 이미 MainPresenter 인스턴스에 바인딩되어 있는 함수 참조라서,subscribeBy가 단순히 onSuccess(data) 이렇게 호출만 하면, 내부적으로는 this.show(data) 가 실행되고,그 안에서 자연스럽게 this.view.show(data) 같은 것도 정상 접근 가능하다.

컬렉션 처리


컬렉션 처리는 프로그래밍에서 가장 빈번하게 일어나는 작업 중 하나이자, 수십년동안 함수형 프로그래밍의 주요 셀링포인트 였습니다. 리스프 프로그래밍 언어의 뜻 또한 list processing이다.

forEach와 onEach


inline fun <T> Iterable<T>.forEach(action: (T) -> Unit) {
	for (element in this) action(element)
}

inline fun <T, C:Iterable<T>> C.onEach(
	action: (T) -> Unit
): C {
	for (element in this) action(element)
	return this
}
  • forEach -> Unit
  • onEach -> 이터러블 반환 (체이닝)

users.
	.filter { it.isActive }
	.onEach { log("Sending Msg gor user $it") }
	.flatMap { it.remainingMessages }
	.filter { it.isTobeSent }
	.forEach { sendMessage(it) }

filter


inline fun <T> Iterable<T>.filter(
	predicate: (T) -> Boolean
): List<T> {
	val destination = ArrayList<T>()
	for (element in this) {
		if (predicate(element)) {
			destination.add(element)
		}
	}
}
  • 현실의 필터와는 다르게 맞는것들만 걸러줌
  • 저자는 “predicate에 맞지 않는 원소들을 거르는 필터"라 생각하면 직관적이라고 함

map

inline fun <T, R> Iterable<T>.map(
	transform: (T) -> R
): List<R> {
	val size = if (this is Collection<*>) this.size else 10
	val destination = ArrayList<R>(size)
	for (element in this) {
		destination.add(transform(element))
	}
}
  • 동일 크기의 컬렉션을 반환
  • 성능이 중요한 경우 mapNotNull을 사용

flatMap

inline fun <T, R> Iterable<T>.flatMap(
	transform: (T) -> Iterable<R>
): List<R> {
	val size = if (this is Collection<*>) this.size else 10
	val destination = ArrayList<R>(size)
	for (element in this) {
		destination.addAll(transform(element))
	} 
	return destination
}

fold

inline fun <T, R> Iterable<T>.fold(
	initial: R,
	operation: (acc: R, T) -> R
): R {
	var accumulator = initial
	for (element in this) {
		accumulator = operation(accumulator, element)
	}
	return accumulator
}
  • 누산기
  • reduce와 다르게 초기값을 지정
  • 가장 만능이지만, 직접 사용할일은 적음 (fold를 래핑한 연산들이 거의 다 제공됨)

withIndex와 인덱스된 변형 함수들

fun <T> Iterable<T>.withIndex(): Iterable<IndexedValue<T>> = IdexingIterable { iterator() }

data class IndexedValue<out T>(
	val index: Int,
	val value: T
)
fun main() {
	listOf("A", "B", "C", "D") // List<String>
		.withIndex() // List<IndexedValue<String>>
		.filter { (index, value) -> index % 2 == 0 }
		.map { (index, value) -> "[$index] $value" }
		.forEach { println(it) }
}

// [0] A
// [2] C

partition

inline fun <T> Iterable<T>.partition(
    predicate: (T) -> Boolean
): Pair<List<T>, List<T>> {
    val first = ArrayList<T>()
    val second = ArrayList<T>()
    for (element in this) {
        if (predicate(element)) {
            first.add(element)
        } else {
            second.add(element)
        }
    }
    return Pair(first, second)
}
  • 조건에 맞는 것과 맞지 않는 것을 분리해서 Pair로 반환
  • filter를 두 번 쓰는 것보다 효율적
val numbers = listOf(1, 2, 3, 4, 5, 6)
val (even, odd) = numbers.partition { it % 2 == 0 }
// even: [2, 4, 6], odd: [1, 3, 5]

groupBy

inline fun <T, K> Iterable<T>.groupBy(
    keySelector: (T) -> K
): Map<K, List<T>> {
    val destination = LinkedHashMap<K, MutableList<T>>()
    for (element in this) {
        val key = keySelector(element)
        val list = destination.getOrPut(key) { ArrayList<T>() }
        list.add(element)
    }
    return destination
}
  • 키별로 그룹핑해서 Map으로 반환
  • SQL의 GROUP BY와 비슷한 개념
data class Person(val name: String, val age: Int)

val people = listOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Charlie", 25)
)

val grouped = people.groupBy { it.age }
// {25=[Person(Alice, 25), Person(Charlie, 25)], 30=[Person(Bob, 30)]}

associate 계열

inline fun <T, K, V> Iterable<T>.associate(
    transform: (T) -> Pair<K, V>
): Map<K, V> {
    val destination = LinkedHashMap<K, V>()
    for (element in this) {
        destination += transform(element)
    }
    return destination
}
  • 컬렉션을 Map으로 변환
  • associateBy, associateWith 등의 변형도 있음
val words = listOf("apple", "banana", "cherry")
val lengthMap = words.associate { it to it.length }
// {apple=5, banana=6, cherry=6}

val firstCharMap = words.associateBy { it.first() }
// {a=apple, b=banana, c=cherry}

val upperMap = words.associateWith { it.uppercase() }
// {apple=APPLE, banana=BANANA, cherry=CHERRY}

distinct와 distinctBy

fun <T> Iterable<T>.distinct(): List<T> {
    return this.toMutableSet().toList()
}

inline fun <T, K> Iterable<T>.distinctBy(
    selector: (T) -> K
): List<T> {
    val set = HashSet<K>()
    val list = ArrayList<T>()
    for (e in this) {
        val key = selector(e)
        if (set.add(key)) {
            list.add(e)
        }
    }
    return list
}
  • distinct: 중복 제거
  • distinctBy: 특정 속성 기준으로 중복 제거
val numbers = listOf(1, 2, 2, 3, 3, 3)
println(numbers.distinct()) // [1, 2, 3]

val people = listOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Charlie", 25)
)
val distinctByAge = people.distinctBy { it.age }
// [Person(Alice, 25), Person(Bob, 30)] - 나이가 같은 Charlie는 제외

take와 drop 계열

  • take: 앞에서부터 n개 가져오기
  • drop: 앞에서부터 n개 버리기
  • takeWhile: 조건이 참인 동안 가져오기
  • dropWhile: 조건이 참인 동안 버리기
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

println(numbers.take(3)) // [1, 2, 3]
println(numbers.drop(3)) // [4, 5, 6, 7, 8, 9, 10]

println(numbers.takeWhile { it < 5 }) // [1, 2, 3, 4]
println(numbers.dropWhile { it < 5 }) // [5, 6, 7, 8, 9, 10]

zip

infix fun <T, R> Iterable<T>.zip(
    other: Iterable<R>
): List<Pair<T, R>> {
    return zip(other) { t1, t2 -> t1 to t2 }
}
  • 두 컬렉션을 짝지어서 Pair의 리스트로 만듦
  • 길이가 다르면 짧은 쪽에 맞춰짐
val names = listOf("Alice", "Bob", "Charlie")
val ages = listOf(25, 30, 35, 40)

val paired = names.zip(ages)
// [(Alice, 25), (Bob, 30), (Charlie, 35)] - 40은 버려짐

val formatted = names.zip(ages) { name, age -> "$name is $age years old" }
// ["Alice is 25 years old", "Bob is 30 years old", "Charlie is 35 years old"]

순서 관련 함수들

  • sorted: 정렬 (Comparable 구현 필요)
  • sortedBy: 특정 속성으로 정렬
  • sortedWith: 커스텀 Comparator로 정렬
  • reversed: 순서 뒤집기
val words = listOf("banana", "apple", "cherry")

println(words.sorted()) // [apple, banana, cherry]
println(words.sortedBy { it.length }) // [apple, banana, cherry] - 길이순
println(words.sortedWith(compareByDescending { it.length })) // [banana, cherry, apple]
println(words.reversed()) // [cherry, apple, banana]

검색 함수들

  • find: 첫 번째로 조건에 맞는 원소 (없으면 null)
  • first: 첫 번째 원소 (없으면 예외)
  • firstOrNull: 첫 번째 원소 (없으면 null)
  • any: 조건에 맞는 원소가 하나라도 있는지
  • all: 모든 원소가 조건에 맞는지
  • none: 조건에 맞는 원소가 하나도 없는지
val numbers = listOf(1, 2, 3, 4, 5)

println(numbers.find { it > 3 }) // 4
println(numbers.first { it > 3 }) // 4
println(numbers.any { it > 3 }) // true
println(numbers.all { it > 0 }) // true
println(numbers.none { it < 0 }) // true

집계 함수들

  • count: 원소 개수 (조건 있으면 조건에 맞는 개수)
  • sum: 합계 (숫자 타입만)
  • average: 평균 (숫자 타입만)
  • min, max: 최솟값, 최댓값
  • minBy, maxBy: 특정 속성 기준 최솟값, 최댓값을 가진 원소
val numbers = listOf(1, 2, 3, 4, 5)

println(numbers.count()) // 5
println(numbers.count { it > 3 }) // 2
println(numbers.sum()) // 15
println(numbers.average()) // 3.0

val people = listOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Charlie", 20)
)
println(people.minBy { it.age }) // Person(Charlie, 20)
println(people.maxBy { it.age }) // Person(Bob, 30)

체이닝의 장점과 주의사항

  • 함수형 스타일의 컬렉션 처리는 가독성이 좋고 실수가 적다
  • 하지만 중간 컬렉션이 계속 생성되므로 성능에 주의해야 함
  • 성능이 중요한 경우 시퀀스(sequence)를 고려해볼 것
// 매번 새로운 리스트가 생성됨
val result = (1..1000000)
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(10)

// 지연 계산으로 성능 개선
val result2 = (1..1000000).asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(10)
    .toList()

시퀀스 (Sequence)


시퀀스의 특징

  • 지연 계산(lazy evaluation)
  • 중간 연산은 지연되고 종단 연산에서 실행
  • 무한 시퀀스 생성 가능
  • 메모리 효율적
// 컬렉션 방식 - 즉시 계산
val result1 = (1..100)
    .filter { it % 2 == 0 }  // 새로운 리스트 생성
    .map { it * 2 }          // 또 다른 새로운 리스트 생성
    .take(5)                 // 또 다른 새로운 리스트 생성

// 시퀀스 방식 - 지연 계산
val result2 = (1..100).asSequence()
    .filter { it % 2 == 0 }  // 아직 실행 안됨
    .map { it * 2 }          // 아직 실행 안됨
    .take(5)                 // 아직 실행 안됨
    .toList()                // 여기서 모든 연산이 한번에 실행

무한 시퀀스

val fibonacci = generateSequence(1 to 1) { (first, second) ->
    second to (first + second)
}.map { it.first }

val first10Fibonacci = fibonacci.take(10).toList()
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

고차 함수 심화


let, run, with, apply, also

// let - 객체를 it으로 받아서 블록 실행, 블록의 결과 반환
val result = "Hello".let { str ->
    str.uppercase() + " World"
} // "HELLO World"

// run - 객체를 this로 받아서 블록 실행, 블록의 결과 반환
val result2 = "Hello".run {
    uppercase() + " World"
} // "HELLO World"

// with - 객체를 this로 받아서 블록 실행, 블록의 결과 반환 (확장함수 아님)
val result3 = with("Hello") {
    uppercase() + " World"
} // "HELLO World"

// apply - 객체를 this로 받아서 블록 실행, 객체 자신을 반환
val person = Person("").apply {
    name = "Alice"
    age = 25
} // Person(name="Alice", age=25)

// also - 객체를 it으로 받아서 블록 실행, 객체 자신을 반환
val numbers = mutableListOf(1, 2, 3).also { list ->
    println("Original list: $list")
} // 리스트는 그대로, 로그만 출력

takeIf와 takeUnless

// takeIf - 조건이 참이면 객체 반환, 거짓이면
val positiveNumber = (-5).takeIf { it > 0 } // null

// takeUnless - 조건이 거짓이면 객체 반환, 참이면 null
val notEmptyString = "".takeUnless { it.isEmpty() } // null

// 실용적인 예
fun processUser(user: User?): String {
    return user
        ?.takeIf { it.isActive }
        ?.let { "Processing user: ${it.name}" }
        ?: "User is inactive or null"
}