[Kotlin] 함수 리터럴(function literal) 2 — 람다(lambda)

dEpayse
dEpayse_publication
14 min readJan 27, 2021

지난 포스트에서 함수 리터럴(function literal)에 관한 내용을 전반적으로 살펴보았다. 함수 리터럴에는 두 가지 타입이 있다 하였는데, 이번 포스트에서는 람다(lambda)를 살펴보자. 람다(Lambda) 또는 람다식(Lambda expression)은 일련의 코드 조각을 뜻하며, 이 코드 조각은 1급 객체(first-class citizen)로 사용된다.

**람다란?**

람다에 알아보기 전에 람다가 왜 람다로 불리는지 알면 처음 접하는 사람에게 좀 더 쉽게 접근할 수도 있을 것 같다. (관심 없으시면 넘어가셔도 됩니다!)아래 인용은 위키 백과에서 가져온 내용이다.

람다 대수(lambda calculus, λ-대수) 또는 람다 계산법은 추상화와 함수 적용 등의 논리 연산을 다루는 형식 체계이다.

와닿진 않지만, 다음과 같은 람다의 특징은 더 직관적일 것이다.

1. 람다 대수는 이름을 가질 필요가 없다. (익명 함수)

2. 두 개 이상의 입력이 있는 함수는 최종적으로 1개의 입력만 받는 람다 대수로 단순화 될 수 있다. (커링)

이제부터 공부할 람다식은 코드 블록의 이름이 없는 것이 큰 특징이다.

람다 정의 및 사용

Fig1. 람다 문법

람다는 항상 중괄호와 함께 정의한다. 화살표를 기준으로 화살표 전은 입력 변수, 그리고 화살표 후는 본문이며, 본문의 마지막 줄에는 반환 값이 위치한다.

람다는 변수에 저장하여 사용할 수도 있다. 변수에 저장할 때는 객체를 저장할 때와 마찬가지로 변수에 타입을 명시해주면서 람다를 변수에 저장할 수 있다. 람다의 타입은 ‘괄호로 둘러싼 입력변수’, ‘화살표’, ‘괄호로 둘러싼 출력변수’로 이루어진다. 출력변수를 둘러싼 괄호는 생략할 수 있다. 이 경우 컴파일러가 타입을 이미 알기 때문에 람다를 정의할 때 입력 변수에 타입을 명시하지 않아도 된다. 물론 Fig1의 람다를 그대로 변수에 저장하는 것도 가능하다.

Fig2. 람다를 변수에 저장하기
// Ex1-1. 람다를 변수에 저장하여 사용하기

fun main(){
//람다를 변수에 저장하여 사용하기
val sum ={ x:Int, y:Int ->
println("x + y = ${x + y}")
}
//람다를 저장할 변수에 타입 명시하기
val multiply:(Int,Int)-> Unit ={ x, y ->
println("x * y = ${x * y}")
}
sum(2,3)
multiply(3,4)
}

/* 결과:
x + y = 5
x * y = 12
*/
// Ex1-2. 함수와 람다 비교

//함수
fun sum1(x:Int, y:Int):Int{
print("x + y =")
return x + y
}

fun main(){
//람다
val sum2 ={ x:Int, y:Int ->
print("x + y =")
x + y
}
//함수와 람다 비교
println(sum1(2,3))
println(sum2(2,3))
}

/* 결과:
x + y = 5
x + y = 5
*/

고차 함수(high-order function)에서 람다 사용하기

자주 쓰는 두 객체에 대해서 다양한 기능을 수행하도록 코드를 작성한다고 해보자. 예시로, 정수 2와 3에 대해 다양한 기능을 수행할 수 있는 함수를 작성해보자. 고차 함수(high-order function)는 함수를 인자로 받거나 함수를 반환하는 함수를 뜻하는데, Ex2 의 함수는 함수를 인자로 받고 있으므로 고차 함수이다.

// Ex2. 고차 함수 정의하기

fun manipulateTwoThree(f:(Int, Int)->(Int)){
println(f(2,3))
}

Ex2 에서는 함수를 parameter로 정의했다. 함수를 parameter로 정의하는 방법은 parameter 이름 뒤에, Fig2에서 봤듯이 함수의 타입을 적어주면 된다. 함수 내부에서는 parameter인 함수 f를 함수처럼 사용할 수 있다.

// Ex2-1. 고차 함수에서 람다 사용하기

//고차 함수 정의
fun manipulateTwoThree(f:(Int, Int)->(Int)){
println(f(2,3))
}

fun main() {
//고차 함수에서 람다 사용
println(manipulateTwoThree({a,b->a+b}))
}

람다 규칙

람다 정의 시

  • 람다를 정의할 때는 파라미터의 타입을 추론할 문맥이 없기 때문에 파라미터 타입을 반드시 명시해야 한다.
  • 본문의 마지막 줄이 람다의 반환 값이다.

함수를 인자로 사용한 함수 호출 시 람다 사용

함수는 다른 함수에 인자로 전달될 수 있다고 했다. 함수를 인자를 사용한 함수를 호출할 때, 몇 가지 규칙에 의해 더 간결한 코드를 작성할 수 있다.

  1. 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면 그 람다를 괄호 밖으로 빼낼 수 있다.
  2. 함수 호출 시 람다가 그 함수의 유일한 인자이고 빈 괄호 뒤에 람다를 썼다면 빈 괄호를 생략할 수 있다.
  3. 함수 호출 시 람다의 파라미터 타입을 컴파일러가 추론할 수 있는 경우 타입을 생략할 수 있다.
  4. 함수 호출 시 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우 it을 바로 쓸 수 있다.

람다를 사용할 때 규칙을 적용해보기 위해, Collections 프레임워크에 제공되는 maxBy()라는 메서드를 사용해보자. maxBy() 메서드는 함수 하나를 인자로 받는다. 인자로 받는 함수의 타입은 Collection이 담고 있는 타입을 입력으로 받고, Comparable 인터페이스를 구현한 타입을 출력으로 한다. 이 람다의 출력이 최대인 객체를 반환하는 것이 maxBy()함수의 기능이다.

// Ex3. maxBy 함수 사용하기

data class Person(val name: String, val age: Int)

fun main() {
val people = listOf(Person("Harry", 42), Person("James", 62), Person("Sirius", 63))
println(people.maxBy({ p: Person -> p.age }))
}

/*결과:
Person(name=Sirius, age=63)
*/

Ex3을 살펴보자. List는 Collection의 하위 클래스이고, listOf() 함수는 List를 만들어 반환한다. 여기서 만든 people이라는 변수는 여러 개의 Person 객체를 담고 있는 List이다. maxBy() 함수는 람다를 argument로 사용한 것을 볼 수 있다. 위에서 언급했듯이 Collection(여기선 List)이 담고 있는 Person 타입을 변수 이름 p로 하여 입력 받고 있고, 출력은 그 Person 객체의 비교 가능한(Comparable 인터페이스가 구현 된) 나이인 age로 하고 있어서 정상적인 람다 인자가 될 수 있다. 이 코드를 통해 사람들(people) 중에서 나이(age)가 가장 많은 사람 객체를 얻을 수 있다.

이제 람다를 더 간단히 사용할 수 있도록 바꾸어보자. 람다를 인자를 사용한 함수인 maxBy()를 사용한 부분만 개선해보자.

// Ex3-1. maxBy 함수 간단하게 사용하기(과정1)

data class Person(val name: String, val age: Int)

fun main() {
val people = listOf(Person("Harry", 42), Person("James", 62), Person("Sirius", 63))
println(people.maxBy(){ p: Person -> p.age })
}

/*결과:
Person(name=Sirius, age=63)
*/

람다 사용 1번 규칙을 적용했다. 코드가 짧아지진 않았지만, 만약 함수에 람다 외 인자가 더 많았다면 더 보기 좋을 것이라고 생각 된다.

//Ex3-2. maxBy 함수 간단하게 사용하기(과정2)

data class Person(val name: String, val age: Int)

fun main() {
val people = listOf(Person("Harry", 42), Person("James", 62), Person("Sirius", 63))
println(people.maxBy{ p: Person -> p.age })
}

/*결과:
Person(name=Sirius, age=63)
*/

람다 사용 2번 규칙을 적용했다. 코드의 길이는 아주 조금 짧아졌지만, 짧아지기 시작했다. 가독성도 훨씬 좋아진 느낌이 든다.

//Ex3-3. maxBy 함수 간단하게 사용하기(과정3)

data class Person(val name: String, val age: Int)

fun main() {
val people = listOf(Person("Harry", 42), Person("James", 62), Person("Sirius", 63))
println(people.maxBy{ p -> p.age })
}

/*결과:
Person(name=Sirius, age=63)
*/

람다 사용 3번 규칙을 적용했다. 코드 길이가 많이 짧아진 것이 보인다.

// Ex3-4. maxBy 함수 간단하게 사용하기(과정4)

data class Person(val name: String, val age: Int)

fun main() {
val people = listOf(Person("Harry", 42), Person("James", 62), Person("Sirius", 63))
println(people.maxBy{ it.age })
}

/*결과:
Person(name=Sirius, age=63)
*/

람다 사용 4번 규칙을 적용했을 때는 필요한 것만 적어 간결함이 매우 돋보인다.

Fig3. 람다를 인자로 사용한 함수 호출 시 규칙 적용

람다 함수 리턴

Kotlin 에서는 함수 리터럴(람다, 익명 함수), 지역 함수(local functions), 객체식(object expression) 등을 통해 함수 안에 함수를 선언하여 중첩할 수 있다. 이qualified return 이라는 구문을 사용하여 람다를 명시적으로 리턴할 수 있다.

Kotlin 에서 모든 표현식(expression)에는 라벨(label)을 붙일 수 있다. 라벨을 붙이는 방법은 표현식 바로 앞에 라벨명@ 으로 표기하면 된다. 람다도 값을 반환하는 ‘식’이기 때문에 람다도 라벨을 붙이는 것이 가능하다. 라벨이 붙은 표현식을 종료하는 방법은 return@{라벨명} 으로 할 수 있다.

// Ex4-1. explicit label 을 활용하여 qualified return 사용하기

fun main() {
val ints = intArrayOf(-2, -1, 0, 1, 2)
ints.filter lambda@ {
val shouldFilter = it > 0
return@lambda shouldFilter
}
}

그러나 람다는 람다가 인자로 전달된 함수명의 이름으로 된 ‘implicit 라벨’을 사용해서 종료할 수 있다.

// Ex4-2. implicit label 을 활용하여 qualifed return 사용하기

fun main() {
val ints = intArrayOf(-2, -1, 0, 1, 2)
ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter
}
}

사용하지 않는 변수

람다에서 사용하지 않는 파라미터는 underscore(_) 기호를 사용하여 표기할 수 있다.

// Ex5. 람다에서 사용하지 않는 파라미터 표기하기

fun main() {
val ints = intArrayOf(-2, -1, 0, 1, 2)
// ints 내부의 값들은 사용하지 않을 때 underscore(_) 기호로 표기할 수 있다.
// underscore 는 람다 내에서 변수처럼 참조하여 사용할 수 없다.
ints.forEachIndexed { idx, _ ->
println(idx)
}
}

Destructing in lambda

Kotlin 에는 구조 분해(Destructructing Declaration)라는 문법이 있는데, 어떤 객체의 프로퍼티들을 임의로 분해하여 사용할 수 있는 기능이다.

// Ex6-1. 구조 분해(Destructing Declaration)

fun main() {
val pair = Pair(3, "three")
val (numberInt, numberStr) = pair
println(numberInt)
println(numberStr)
}

/**
결과:
3
three
*/

이런 구조 분해를 사용할 수 있는 클래스는 연산자(operator) 로 구분된 componentN() 함수가 구현되어 있으면 된다. Kotlin 에서 data class 로 정의하면 componentN() 함수가 자동으로 구현되므로 매우 유용하게 사용할 수 있을 것 같다. (data class 와 componentN() 함수에 관해 더 알고 싶다면 아래 포스트를 참고해주세요.)

람다에서 이런 구조 분해를 사용하면 다음과 같다. map 의 forEach 는 각각의 원소가 Map.Entry 타입인데, 이 인터페이스는 componentN() 함수를 구현하고 있다.

// Ex6-2. 람다의 구조 분해(Destructing Declaration) 사용

fun main() {
val map = mapOf(3 to "three")
// 구조 분해를 사용하지 않았을 때
map.forEach { entry -> println("${entry.value}!") }
// 구조 분해를 사용했을 때
map.forEach { (key, value) -> println("$value!") }
}

/**
결과:
three
three
*/

Closures

Closure 는 자신이 생성될 때의 환경(Lexical environment)을 기억하는 함수를 의미한다. Kotlin 에서도 Closure 를 지원하는데, 이 것은 ‘함수 내부에 중첩된 함수’가 ‘그 함수를 둘러싸는 함수’의 변수들에 접근할 수 있다는 것을 의미한다.

fun main() {
var sum = 0
ints.filter { it > 0 }.forEach {
// 외부 함수의 변수인 sum 을 사용 가능하다.
sum += it
}
print(sum)
}

Reference

Overall part

  1. Dmitry Jemerov and Svetlana Isakova. (2017). Kotlin in Action. USA: Manning
  2. [위키백과] “람다 대수” — https://ko.wikipedia.org/wiki/%EB%9E%8C%EB%8B%A4_%EB%8C%80%EC%88%98
  3. [DailyEngineering] “람다, 익명 함수, 클로저” — https://hyunseob.github.io/2016/09/17/lambda-anonymous-function-closure/
  4. [SUNEET AGRAWAL] “Label Reference in Kotlin” — https://agrawalsuneet.github.io/blogs/label-reference-in-kotlin/
  5. [poiemaweb] “클로저” — https://poiemaweb.com/js-closure

--

--

dEpayse
dEpayse_publication

나뿐만 아니라 다른 사람들도 이해할 수 있도록 작성하는, 친절한 블로그를 목표로.