티스토리 뷰
728x90
고차 함수
함수형 언어는 함수를 first class 값으로 취급한다.
즉, 함수를 다른 일반적인 타입과 똑같이 변수에 값을 읽거나 쓸 수 있고 함수에 값을 전달하거나 함수가 값을 반환할 수 있다는 뜻이다.
- 고차 함수
- 함수를 인자로 받거나 함수를 반환하는 함수.
fun aggregate(numbers: IntArray, op: (Int, Int) -> Int): Int {
var result = numbers.firstOrNull()
?: throw IllegalArgumentException("Empty array")
for (i in 1..numbers.lastIndex) result = op(result, numbers[i])
return result
}
fun main() {
println(aggregate(intArrayOf(1, 2, 3), { result, op -> result + op }))
}
- op 파라미터를 통해 함수를 인자로 받을 수 있다.
- 이 파라미터의 타입은 함수 타입인 (Int, Int) → Int 다. (Int 두개를 받아서 Int를 반환)
2. 함수 타입
- 함수 타입: 함수처럼 쓰일 수 있는 값들을 표시하는 타입.
- ex) (Int, Int) -> Boolean
- 괄호로 둘러싸인 타입 목록: 함수값에 인자로 전달될 데이터의 종류와 수
- 화살표 뒤의 타입: 함수의 반환 타입
- 함수가 인자를 받지 않는 경우에도 괄호를 적어야 한다.
- ex: () -> Unit
- 함수 타입은 함수의 파라미터 뿐만 아니라, 타입이 쓰일 수 있는 모든 곳에 사용할 수 있다.
val lessThan: (Int, Int) -> Boolean = { a, b -> a < b }
-
- ex: 변수에 함수 타입 지정
val lessThan = { a, b -> a < b } // error
val lessThan = { a: Int, b: Int -> a < b } // Ok
- 변수 타입을 생략하면 컴파일러가 파라미터의 타입을 추론할 수 없다. → 파라미터 타입을 명시해준다.
fun main() {
val stringPlus: (String, String) -> String = String::plus
println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!"))
}
- 함수 타입의 값은 일반 함수처럼 호출하거나 invoke() 메소드를 통해 호출할 수 있다.
fun interface StringConsumer {
fun accept(s: String)
}
fun main() {
val consume = StringConsumer { s -> println(s) }
consume.accept("Hello")
}
- 함수값은 항상 (P1, ... Pn) -> R 형태의 함수 타입에 속해서 SAM 인터페이스로 암시적 변환할 수 없었다.
- 코틀린 인터페이스 앞에 fun을 붙이면 SAM 인터페이스로 취급한다.
- 자바와 마찬가지로 코틀린 인터페이스를 람다로 인스턴스화할 수 있다.
- 함수 타입 전체를 괄호로 둘러싸고 물음표를 붙이면, 함수도 nullable type으로 지정할 수 있다.
- 함수 타입을 다른 함수 타입 안에 내포시킬 수 있다.
- 함수 타입의 파라미터 목록에 파라미터 이름을 포함시킬 수 있다. (함숫값에는 영향 x)
3. 람다식
- 이름을 지정하지 않고 정의하는 함수
{ a, b -> a + b }
-
- 파라미터 목록: a, b
- 람다식 본문: a + b
- 반환 타입을 지정할 필요가 없다. (반환 타입이 자동으로 추론됨)
- 람다식 본문의 가장 마지막 문장이 반환값이 된다.
- 파라미터 목록을 괄호로 둘러싸지 않는다.
fun aggregate(op: (Int, Int) -> Int): Int {
return op(3, 5)
}
fun main() {
println(aggregate { a, b ->
println("test")
a + b })
}
- 람다가 함수의 마지막 파라미터인 경우, 람다를 괄호 밖에 위치시킬 수 있다.
- 함수의 파라미터에 람다만 존재할 경우, 괄호를 제거할 수 있다.
- 인자가 없으면 -> 를 생략할 수 있다.
fun check(s: String, condition: (Char) -> Boolean): Boolean {
for (c in s) {
if (!condition(c)) return false
}
return true
}
fun main() {
println(check("Hello") { c -> c.isLetter() })
println(check("Hello") { it.isLowerCase() })
}
- 인자가 하나인 경우는 파라미터 목록과 -> 를 생략하고, 인자를 it으로 가리킬 수 있다.
- 사용하지 않는 람다 파라미터를 밑줄 기호(_)로 지정할 수 있다.
4. 익명 함수
fun(a, b): Int { return a + b }
fun(a, b) = a + b
- 함수의 이름을 지정하지 않는다. fun 키워드 다음에 바로 파라미터 목록이 온다.
- 파라미터 타입을 추론할 수 있으면 지정하지 않아도 된다.
- 식이기 때문에 인자로 넘기거나 변수에 대입하는 등 일반 값처럼 쓸 수 있다.
- 함수 본문이 식이 아닌 경우 반환 타입을 지정해야 한다.
- 람다와 달리, 익명 함수를 인자 목록의 괄호 밖으로 위치시킬 수 없다.
5. callable reference
fun check(s: String, condition: (Char) -> Boolean): Boolean {
for (c in s) {
if (!condition(c)) return false
}
return true
}
fun isCapitalLetter(c: Char) = c.isUpperCase() && c.isLetter()
fun main() {
// 함수 정의를 람다로 다시 전달
println(check("Hello") { isCapitalLetter(it) })
// callable reference를 통해 함수 정의를 함숫값 처럼 사용
println(check("Hello", ::isCapitalLetter))
}
- 함수 정의를 람다나 익명 함수 같은 함숫값처럼 사용하고 싶은 경우 callable reference를 사용한다.
- 함수 이름 앞에 ::를 붙인다.
- 함수명을 패키지명 없이 간단한 형태로만 써야 하므로, 다른 패키지 함수의 callable reference를 사용하려면 import 해야 한다.
class Foo()
fun main() {
val createFoo = ::Foo
val f = createFoo()
}
- 클래스 이름 앞에 적용하면, 클래스의 생성자에 대한 callable reference를 얻는다.
class Test(val s: String) {
fun test() {
println(s)
}
}
fun main() {
val t = Test("abc")::test
t()
}
- 인스턴스의 멤버 함수에 대한 callable reference를 사용할 수 있다. ( → bound callable reference )
fun max(a: Int, b: Int) = if (a > b) a else b
fun max(a: Double, b: Double) = if (a > b) a else b
fun main() {
val f: (Int, Int) -> Int = ::max
val g = ::max // error
}
- 오버로딩된 함수를 callable reference로 사용하고 싶다면 함수 타입을 지정해주어야 한다.
fun max(a: Int, b: Int) = if (a > b) a else b
fun main() {
println((::max)(3,2))
}
- callable reference를 직접 호출하고 싶다면 reference 전체를 괄호로 둘러싼 후 인자를 지정해야 한다.
fun main() {
class A(var p: Int)
val a = A(3)::p
println(a.get())
val b = A::p
println(b.get(A(2)))
}
- 프로퍼티에 대한 callable reference를 생성할 수 있다. (프로퍼티 정보를 담고 있는 리플렉션 객체 → 추후 공부)
- 지역 변수에 대한 callable reference는 불가능하다.
6. 인라인 함수
- 고차 함수의 함숫값(익명 함수, 람다 등)은 객체로 표현된다. 메모리를 할당하고 가상 호출되어 런타임의 오버헤드를 발생시킨다. (컴파일 시 invoke() 함수만을 가지는 인터페이스의 객체로 변환)
- → 인라인 함수를 사용하여 런타임 시간을 줄일 수 있다.
- 인라인 함수: 함수 내에서 함숫값을 사용하는 부분을 함수의 본문 안으로 대체하는 것.
- 인라인 함수는 컴파일된 코드의 크기가 커지지만, 대상 함수가 작은 경우 성능이 향상된다.
- ex:
inline fun aggregate(numbers: IntArray, op: (Int, Int) -> Int): Int {
var result = numbers.firstOrNull()
?: throw IllegalArgumentException("Empty array")
for (i in 1..numbers.lastIndex) result = op(result, numbers[i])
return result
}
fun main() {
println(aggregate(intArrayOf(1, 2, 3)) { a, b -> a + b })
}
- aggregate 함수는 inline fun이기 때문에 컴파일 시 아래와 같이 번역된다.
- 예시의 op 처럼, 파라미터로 전달된 함숫값도 인라인된다.
fun main() {
val numbers = intArrayOf(1, 2, 3)
var result = numbers.firstOrNull()
?: throw IllegalArgumentException("Empty array")
for (i in 1..numbers.lastIndex) result = result + numbers[i]
println(result)
}
var lastAction: () -> Unit = {}
inline fun runAndMemorize(action: () -> Unit) {
action()
lastAction = action // error
}
- 인라인 함수는 실행 시점에 별도의 존재가 아니므로 변수에 저장되거나 인라인 함수가 아닌 함수에 전달될 수 없다.
var lastAction: () -> Unit = {}
inline fun runAndMemorize(noinline action: () -> Unit) {
action()
lastAction= action // ok
}
- 파라미터의 특정 함숫값을 인라인하지 않기 위해 파라미터 앞에 noinline 변경자를 붙일 수 있다.
- 공개 인라인 함수에 비공개 멤버를 넘길 수 없다. (비공개 멤버가 외부로 노출될 수 있음)
class Foo(var a: Int) {
var b
inline get() = a
inline set(value) { /**/ }
}
- 프로퍼티 접근자를 인라인 할 수 있다. (함수 호출을 없애기 때문에 프로퍼티의 액세스 성능 향상)
- 프로퍼티 자체(게터, 세터 모두), 게터, 세터에 지정할 수 있다.
- backing field가 없는 프로퍼티에 대해서만 가능하다.
7. 비지역적 제어 흐름
fun forEach(a: IntArray, action: (Int) -> Unit) {
for (n in a) action(n)
}
fun main() {
forEach(intArrayOf(1, 2, 3, 4)) {
if (it < 2 || it > 3) return // error
println(it)
}
}
- return은 자신을 둘러싸고 있는 fun/get/set으로 정의된 가장 안쪽 함수로부터 빠져나온다.
- 위 코드는 main 함수로부터 반환하는 코드가 된다. → 비지역적 return
fun forEach(a: IntArray, action: (Int) -> Unit) {
for (n in a) action(n)
}
fun main() {
forEach(intArrayOf(1, 2, 3, 4), fun(it: Int) {
if (it < 2 || it > 3) return
println(it)
})
}
- 람다 대신 익명 함수를 사용하면 해결할 수 있다.
fun forEach(a: IntArray, action: (Int) -> Unit) {
for (n in a) action(n)
}
fun main() {
forEach(intArrayOf(1, 2, 3, 4)) myFun@ {
if (it < 2 || it > 3) return@myFun
println(it)
}
}
- 람다 자체로부터 제어 흐름을 반환하고 싶다면 return 문에 문맥 이름을 추가한다. → qualified return
fun forEach(a: IntArray, action: (Int) -> Unit) {
for (n in a) action(n)
}
fun main() {
forEach(intArrayOf(1, 2, 3, 4)) {
if (it < 2 || it > 3) return@forEach
println(it)
}
}
- 람다를 고차 함수의 인자로 넘기는 경우 레이블을 명시적으로 선언하지 않아도, 함수 이름을 통해 반환할 수 있다.
- 람다가 인라인될 경우 return을 사용할 수 있다. (고차 함수를 호출하는 코드를 고차 함수 본문과 람다 본문으로 대체하기 때문)
- break, continue를 람다 안에 넣으면, 람다를 둘러싼 루프를 대상으로 제어 흐름이 변경된다. 인라인되더라도 비지역적 break, continue는 사용할 수 없다.
728x90
'app > kotlin' 카테고리의 다른 글
[kotlin/코틀린] 영역 함수 (0) | 2023.07.19 |
---|---|
[kotlin/코틀린] 확장 (0) | 2023.07.19 |
[kotlin/코틀린] 객체 (0) | 2023.06.22 |
[kotlin/코틀린] 프로퍼티 (0) | 2023.06.22 |
[kotlin/코틀린] 널 가능성 (0) | 2023.06.22 |