본문 바로가기
  • GDG on campus Ewha Tech Blog
3-1기 스터디/Kotlin

[7주차] 람다로 프로그래밍(1)

by 애_용 2021. 12. 26.

람다 식과 멤버 참조

람다 소개

람다 식은 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각을 뜻한다. 코틀린 표준 라이브러리는 람다를 많이 사용한다. (ex) 컬렉션 처리)

“이벤트가 발생하면 이 핸들러를 실행하자” / “데이터 구조의 모든 원소에 이 연산을 적용하자”와 같은 생각을 코드로 표현하기 위해 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야 하는 경우가 자주 있다.

이전에 자바에서는 무명 내부 클래스가 있었는데, 이는 코드를 함수에 넘기거나 변수에 저장할 수 있기는 하지만 상당히 번거롭다.

함수형 프로그래밍에서는 함수를 값처럼 다루는 접근 방법을 택함으로써 이 문제를 해결한다.

  • 무명 내부 클래스로 리스너 구현하기
button.setOnClickListener(new onClickListener() {
	@Override
	public void onClick(View view) {
		/* 클릭 시 수행할 동작 */
	}
});
  • 람다로 리스너 구현하기
button.setOnClickListener { /* 클릭 시 수행할 동작 */ }

이 코드는 자바 무명 내부 클래스와 같은 역할을 하지만 훨씬 간결하고 읽기 쉽다.

람다와 컬렉션

코드에서 중복을 제거하는 것은 프로그래밍 스타일을 개선하는 중요한 방법 중 하나다.

라이브러리 함수를 쓰는 예제를 보자.

// 람다를 사용해 컬렉션 검색하기
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.maxBy { it.age }) // 나이 프로퍼티를 비교
// Person(name=Bob, age=31)

다음 코드도 위와 같은 일을 한다.

// 멤버 참조를 사용해 컬렉션 검색하기
people.maxBy(Persion::age)

람다 식 문법

{ x: Int, y: Int -> x + y }

코틀린 람다 식은 항상 중괄호로 둘러싸여 있다.

람다 식을 변수에 저장할 수 있다. 람다가 저장된 변수를 다른 일반 함수와 마찬가지로 다룰 수 있다.

val sum = { x: Int, y: Int -> x + y }
println(sum(1, 2)) // 3

코드의 일부분을 블록으로 둘러싸 실행할 필요가 있다면 run 을 사용한다. run은 인자로 받은 람다를 실행해주는 라이브러리 함수이다.

run { println(42) } // 42

실행 시점에 코틀린 람다 호출에는 아무 부가 비용이 들지 않으며, 프로그램의 기본 구성 요소와 비슷한 성능을 낸다.

아까 봤던 예제로 돌아가서 하나씩 개선을 해보자.

// 멤버 참조를 사용해 컬렉션 검색하기
people.maxBy(Persion::age)

코틀린에는 함수 호출 시 맨 뒤에 있는 인자가 람다 식이라면, 그 람다를 괄호 밖으로 빼낼 수 있는 문법 관습이 있다.

people.maxBy() { p: Person -> p.age }

이 코드처럼 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출 시 빈 괄호를 없애도 된다.

people.maxBy { p: Person -> p.age }

여기서 더 간단하게 다듬을 수 있다. 파라미터 타입을 없애면 된다!

people.maxBy { p: Person -> p.age }
people.maxBy { p -> p.age } // 컴파일러가 추론

마지막으로 람다의 파라미터 이름을 디폴트 이름인 it으로 바꾸면 람다 식을 더 간단하게 만들 수 있다.

people.maxBy { it.age }

람다를 변수에 저장할 때는 파라미터의 타입을 추론할 문맥이 존재하지 않는다. 따라서 파라미터 타입을 명시해야 한다.

val getAge = { p: Person -> p.age }
people.maxBy(getAge)

현재 영역에 있는 변수에 접근

람다를 함수 안에서 정의하면 함수의 파라미터뿐 아니라 람다 정의의 앞에 선언된 로컬 변수까지 람다에서 모두 사용할 수 있다.

이런 기능을 보여주기 위해 forEach 표준 함수를 사용해보자. **forEach는 컬렉션의 모든 원소에 대해 람다를 호출해준다.**

다음 리스트는 메시지의 목록을 받아 모든 메시지에 똑같은 접두사를 붙여서 출력해준다.

fun printMessagesWithPrefix(messages: Collection<String>, prefix: String) {
	messages.forEach { // 각 원소에 대해 수행할 작업을 람다로 받는다.
		println("$prefix $it") 
	}
}
val errors = listOf("403 Forbidden", "404 Not Found")
printMessagesWithPrefix(errors, "Error:")
/*
Error: 403 Forbidden
Error: 404 Not Found
*/

또한 코틀린 람다 안에서는 파이널 변수가 아닌 변수에 접근할 수 있다. 또한 람다 안에서 바깥의 변수를 변경해도 된다.

또한 코틀린에서는 자바와 달리 람다 밖 함수에 있는 파이널이 아닌 변수에 접근할 수 있고, 그 변수를 변경할 수도 있다.

기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 반환되면 끝난다. 하지만 어떤 함수가 자신의 로컬 변수를 포획한 람다를 반환하거나 다른 변수에 저장하면, 로컬 변수의 생명주기와 함수의 생명주기가 달라질 수 있다.

멤버 참조

코틀린에서는 자바 8과 마찬가지로 함수를 값으로 바꿀 수있다. 이때 이중 콜론(::)을 사용한다.

val getAge = Person::age

::를 사용하는 식을 멤버 참조라고 부른다. 멤버 참조는 프로퍼티나 메서드를 단 하나만 호출하는 함수 값을 만들어준다.

다음 람다 식을 더 간략하게 표현한 것이다.

val getAge = { person: Person -> person.age }

생성자 참조를 사용하면 클래스 생성 작업을 연기하거나 저장해둘 수 있다.

:: 뒤에 클래스 이름을 넣으면 생성자 참조를 만들 수 있다.

val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p) // Person(name=Alice, age=29)

확장 함수도 멤버 함수와 똑같은 방식으로 참조할 수 있다.

fun Person.isAdult() = age >= 21
val predicate = Person::isAdult

댓글