리터럴(Literal)은 고정된 값을 표기하는 방법이다. 각 변수에 정수 1이나, 문자열 "Hello"을 대입할 때와 같이 변하지 않는 데이터를 의미한다. 리터럴은 값을 표기하는 방법이라면, 함수 리터럴은 함수의 본문을 표기하는 방법이다. 함수 리터럴의 정의를 Kotlin Docs에서는 "Function literals are functions that are not declared but are passed immediately as an expression."와 같이 표현한다. 이는 선언되지 않고, '식'처럼 즉시 전달되는 함수라는 의미이다. '식'은 '문'과 다르게, 값을 만들어 내고 다른 변수에 대입이 가능한 대상을 의미한다. Kotlin에서는 함수 리터럴을 구현하기 위해, 람다와 익명함수를 사용할 수 있다. 이번 포스팅에서는 람다와 익명함수에 대해서 소개한다.
사전 지식 : Function Type
코틀린에서는 (Int) -> (String)과 같은 함수 타입을 제공한다. 이러한 타입은 Function Signature(함수 표현식이 가져야 하는 특징들 : 매개변수와 출력변수)과 대응되는 특별한 표기법이다. 함수 타입에는 몇가지 특징이 있다.
- 모든 함수 타입은 매개변수 타입과 출력변수 타입을 가져야 한다.
- (A, B) -> (C)의 경우, A, B는 함수의 두 개 인자 타입을 나타내고, C는 출력 변수의 타입을 나타낸다. 매개변수가 없는 경우는 비어 있는 표시인 "()"로, 출력변수가 없는 경우는 Unit(빈 값)으로 표시한다.
- 함수타입은 "."을 사용한 리시버 타입을 추가적으로 표현할 수 있다.
- 위의 예시를 똑같이 A.(B) -> C로 표현할 수 있다. 해당 식은 A를 리시버 오브젝트, B를 매개변수, C를 출력함수로 지정한다고 표현된다.
- 모든 타입은 nullable하게 작성 가능하다.
- 매개 변수, 출력 변수에 nullable도 가능하며, 함수 타입 전체를 nullable하게 구성할 수 있다. ex) ((Int, Int) -> (String))? or (Int) -> (Int?)
- 함수 타입끼리는 서로 중첩될 수 있다.
- ex) (Int) -> ((Char) -> Int)
사전지식 : 고차 함수
코틀린은 고차함수를 지원한다. 고차함수란, 함수를 다른 함수의 인자로 넘길 수 있거나 함수가 반환값으로 함수를 반환하는 함수이다. 함수가 매개변수로 함수를 받는 경우 인자로 람다나 함수 참조를 통해 전달할 수 있다.
fun <T> List<T>.any( // 여러 타입의 리스트에 대해서 호출하기 위해 제네릭으로 선언
predicate : (T) -> Boolean // 리스트 원소마다 적용하기 위한 함수
): Boolean{
for(element in this){
if(predicate(element)){
return true
}
}
return false
}
fun main(){
val strings = listOf("23", "@!@", "")
println(strings.any { it.isNotBlank() }) // > true
println(strings.any(String::isNotBlank)) // > true 멤버 참조를 통해 전달
}
람다(Lambda)
람다란 부가적인 설명이 빠진 함수로, 함수 이름과 함수 생성에 필요한 최소한의 코드만을 가지는 함수를 의미한다. 람다는 보통 한번만 사용되기 위해 사용되며, 다른 변수에 대입하여 사용하거나, 콜백함수에 지정하는 방법으로 사용할 수 있다.
람다 식은 몇가지 특징이 있다.
- 람다 식은 항상 중괄호로 구성되어 있다.
- 매개 변수는 소괄호 안에서 선언된다.
- 화살표(->)를 기준으로 오른쪽은 함수 본문을, 왼쪽은 매개 변수를 의미한다.
- 람다 식의 반환 값이 Unit이 아닌 경우, 반환 값은 람다 식의 마지막 식을 반환 값을 처리 한다.
- 람다에서 사용하지 않는 매개변수는 '_'(언더 바)를 통해 매개변수 미사용 경고를 무시할 수 있다.
- 람다에서 매개변수가 없을 때 '->'는 생략한다.
fun main(){
// (매개변수1 타입, 매개변수2 타입) -> (출력변수 타입) = { 함수 본문 }
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
}
람다 규칙
위의 사전지식에서 함수의 인자로 함수를 전달할 수 있다는 것을 확인하였다. 매개변수를 전달하기 위해서 람다 식을 활용할 때 아래의 규칙을 통해 간결하고 가독성이 좋게 작성할 수 있다.
- 함수를 호출할 때, 함수가 인자의 마지막이라면 괄호 밖에서 선언할 수 있다.
- 함수를 호출할 때, 함수의 유일한 인자가 함수이고, 밖에서 람다를 선언한 경우, 괄호를 생략할 수 있다.
- 코틀린이 람다의 매개변수 타입을 추론할수 있다면 타입을 생략할 수 있다.
- 매개변수가 하나일 경우 코틀린은 자동으로 매개변수 이름을 "it"으로 설정한다.
아래의 예제는 map()함수를 응용한 예제이다. map()은 매개변수로 원소마다 적용할 변환함수를 받는다.
fun main(){
val list = listOf(1, 2, 3, 4)
val result = list.map( { n : Int -> "[$n]"}) // > [[1], [2], [3], [4]]
}
함수의 인자의 마지막이 함수이기 때문에, 괄호밖에서 선언할 수 있다.
fun main(){
val list = listOf(1, 2, 3, 4)
val result = list.map(){ n : Int -> "[$n]"} // > [[1], [2], [3], [4]]
}
함수의 유일한 인자가 함수이고, 밖에서 람다를 선언한 경우, 괄호를 생략할 수 있다.
fun main(){
val list = listOf(1, 2, 3, 4)
val result = list.map{ n : Int -> "[$n]"} // > [[1], [2], [3], [4]]
}
람다를 List<Int>에 사용 중이므로 코틀린은 n의 타입이 Int라는 사실을 알수 있다. 따라서 타입 추론에 따라 n의 자료형을 생략할 수 있다.
fun main(){
val list = listOf(1, 2, 3, 4)
val result = list.map(){ n -> "[$n]"} // > [[1], [2], [3], [4]]
}
파라미터가 하나일 경우 코틀린은 자동적으로 파라미터 이름을 it으로 설정한다. 따라서 더 이상 "n ->"을 생략할 수 있다.
fun main(){
val list = listOf(1, 2, 3, 4)
val result = list.map(){ "[$it]" } // > [[1], [2], [3], [4]]
}
해당 모든 규칙을 적용하면, 람다의 길이가 짧고 간결해진 것을 확인할 수 있다.
람다 특징
람다의 특징에는 아래와 같은 특징이 있다.
- 레이블을 람다에 적용할 수 있다.
- 자신의 영역 밖에 있는 요소를 참조할 수 있다.
2번의 경우 람다 내에서 외부 함수의 변수에 접근할 수 있다. 이러한 능력은 클로저(closure)와 혼동할 수 있다. 클로저는 함수가 자신의 속한 환경의 요소를 포획(capture)하거나 닫아버리는(close up)것을 의미하며, 클로저가 없는 람다가 있을 수 있으며, 람다가 없는 클로저도 있을 수 있다.
fun main(){
var sum = 0
val ints = listOf(1, 2, 3, 4)
ints.filter { it > 0 }.forEach {
sum += it // 자신의 밖에 정의된 sum을 포획(capture), 포획한 요소를 읽고 변경할 수 있다.
}
print(sum) // > 10
}
익명 함수
익명 함수는 람다와 같이 함수 리터럴로 사용된다. 익명 함수는 람다와 같이 함수 이름이 없으며, fun 키워드를 통해서 정의한다. 람다가 복잡한 경우, 익명 함수나 지역 함수를 통해서 대치할 수 있다.
fun main(){
var sum = 0
val ints = listOf(1, 2, 3, 4)
println(ints.any(
fun(i: Int): Boolean {
if(i > 0){
return true
}
return false
})
) // > true
}
마지막으로
람다를 사용하면, 술어(Boolean 값을 반환하는 함수)를 대치하는 것과 같이 간결하고 명확한 코드를 작성할 수 있다. 이러한 기능은 직접 구성해야 할 이터레이션(반복되는 코드 단락)을 처리할 수 있고, 해당 이터레이션을 구현하면서 발생할 수 있는 실수를 줄일 수 있다.
함수형 프로그래밍은 문제를 작은 단계로 나누고, 작고 쉬운 작업도 함수가 직접 수행한다. 이런 작고 디버깅이 잘된 해법을 갖추면 이들을 쉽게 조합하여 사용할 수 있고, 더 튼튼한 구조로 코드를 구성할 수 있다.
참고자료
https://kotlinlang.org/docs/lambdas.html#anonymous-functions
High-order functions and lambdas | Kotlin
kotlinlang.org
[Kotlin] 함수 리터럴(function literal) 2 — 람다(lambda)
Kotlin의 람다를 정의, 사용 방법과 규칙 그리고 고차 함수에 대해 다룹니다.
medium.com
아토믹 코틀린 4장 : 함수형 프로그래밍