본문 바로가기
My Image
프로그래밍/kotiln

람다로 프로그래밍(5장)

by Lim-Ky 2023. 2. 27.
반응형

람다식 또는 람다

  • 기본적으로 다른 함수에 넘길 수 있는 작은 코드 조각
  • 람다를 사용하면 쉽게 공통 코드 구조를 뽑아낼 수 있음
  • 함수를 값처럼 다루는 접근 방법
  • 무명 클래스의 역할을 대신 함
  • 람다는 함수에 인자로 넘어갈때 정의해서 바로 사용함
  • 실행시점의 코틀린 람다 호출은 아무 비용도 들지 않는다

람다의 사용처

  • 컬렉션 처리
  • 자바 라이브러리에 람다 함께 사용
  • 수신 객체 지정 람다

람다 소개 : 코드 블록을 함수 인자로 넘기기

컬레션 처리에서 람다 없이 자바에서 처리하는 방법

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

/* Java */
fun findTheOldest(people: List<Person>) {
    var maxAge = 0
    var theOldest: Person? = null
    for (person in people) {
        if (person.age > maxAge) {
            maxAge = person.age
            theOldest = person
        }
    }
    println(theOldest)
}

위 코드를 코틀린 람다로 리팩토링 해보자

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

fun main(args: Array<String>) {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    findTheOldest(people)
}

/* Kotlin */
>>> val people = listOf(Person("Alice", 29), Person("Bob", 31))
>>> println(people.maxBy { it.age }) //나이 프로퍼티를 비교해서 값이 가장 큰 원소 찾기
>>> println(people.maxBy { Person::age }) //멤버 참조를 사용해도 가능
Person(name=Bob, age=31)
  • maxBy는 모든 컬렉션에서 사용할 수 있는 최대값을 구해주는 함수
  • maxBy는 비교에 사용할 값을 돌려주는 함수를 인자로 받는다.
  • 중괄호에 둘러싸인 코드 {it.age} 를 비교에 사용할 값으로 넘겨준다. (it은 그 인자를 가리킴)
  • 함수나 프로퍼티를 반환하는 역할을 수행하는 람다는 멤버 참조로도 대치할 수 있음

 

람다 식의 문법

람다 식 문법

  • 람다 식은 항상 중괄호로 둘러싸여 있다
  • 인자 목록 주변엔 괄호가 없다
  • 화살표 (->) 가 인자 목록과 람다 본문을 구분해준다
  • 람다식을 변수에 저장 할 수 있다 (변수 이름 뒤에 괄호를 놓고 그 안에 필요한 인자를 넣어서 람드를 호출할 수 있음)
    • val sum = { x : Int, y : Int -> x + y }
    • println(sum(1, 2)) 출력3
  • 원한다면 람다식을 직접 호출도 가능
    • { println(42) }()  출력42
  •  run은 인자로 받은 람다를 실행할 수 있는 함수
    • run { println(42) } 출력42
  • 컴파일러가 문맥으로부터 유추할 수 있는 인자 타입은 생략 가능
  • 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다 (default name은 it, but 중첩 람다식이면 it 자제, 파라미터 명시)
  • 맨 끝 인자가 람다식이면 람다식을 괄호 밖으로 빼낼 수 있는 관습이 존재
  • 하나밖에 없는 인자가 람다식이면 람다식을 괄호 밖으로 빼고, 빈 괄호 생략 가능
people.maxBy({ p: Person -> p.age }) //정식으로 람다 작성
people.maxBy(){ p: Person -> p.age } //마지막 인자 람다식이면 괄호 밖으로 뺄 수 있는 관습
people.maxBy{ p: Person -> p.age } //인자가 하나고 괄호 밖으로 람다식을 썼다면, 빈 괄호 생략 가능
people.maxBy{ p -> p.age } //똑똑한 컴파일러가 문맥으로 부터 타입 추론 가능해서 타입 생략
people.maxBy{ it.age } //인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다 (default name은 it)

 

람다의 변수 접근

  • 람다를 함수 안에서 정의하면 함수의 파라미터, 람다 정의 앞에 선언된 로컬 변수까지 람다에서 접근 가능
  • 람다는 자바와 달리 final이 아닌 변수에 접근 및 변경 가능
  • 람다 안에서 사용하는 외부 변수를 '람다가 포획한 변수'라고 칭함
  • 기본적으로 함수 안에 정의된 로컬 변수의 생명주기는 함수가 반환되면 끝나지만, 변수포획을 당하면 로컬변수의 생명주기가 달라질 수 있음 (즉 함수는 끝난 뒤에도 포획 당한 변수를 읽거나 쓸 수 있음)
    • final 변수 : 람다 코드와 변수 값과 함께 저장
    • final 아닌 변수 : 변수를 특별한 래퍼로 감싸고(읽고/쓸수있음), 해당 래퍼에 대한 참조를 람다 코드와 함께 저장
fun printProblemCounts(responses: Collection<String>) {
    var clientErrors = 0
    var serverErrors = 0
    responses.forEach {
        if (it.startsWith("4")) {
            clientErrors++  //람다 안에서 람다 밖의 변수를 변경
        } else if (it.startsWith("5")) {
            serverErrors++  //람다 안에서 람다 밖의 변수를 변경
        }
    }
    println("$clientErrors client errors, $serverErrors server errors")
}

 

멤버 참조

  • ::를 사용하는 식을 '멤버 참조' 라고 부른다
  • 프로퍼티나 메서드를 단 하나만 호출하는 함수 값을 만들어 준다
  • 클래스 :: 프로퍼티 또는 클래스 :: 메서드 형태로 사용
  • 멤버 참조 뒤에는 괄호를 넣으면 안된다
people.maxBy{ p -> p.age} //람다표기
people.maxBy{ it.age}     //람다표기
people.maxBy{ Person::age }  //멤버 참조

fun limky() = println("Limky!!")
>>> run(::limky) // 최상위 함수를 참조, 클래스 이름 생략하고 ::으로 시작 가능
>>> Limky!!

fun Person.isAdult() = age >= 21 //확장함수 정의
val predicate = Person:: isAdult //확장함수에도 멤버 참조 가능

 

생성자 참조

  • 멤버 참조과 동일하게 :: 사용한다.
  • 클래스 생성 작업을 저장하거나, 생성 작업을 연기할 수 있다.
  • :: 클래스 이름 형태로 사용
val createPerson = ::Person
val p = createPerson("Alice", 29)
println(p)
>>> Person(name=Alice, age=29)

 

컬렉션 함수형 API

필수적인 함수 filter

  • 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
  • 반환하는 원소를 모을땐 새로운 컬렉션을 생성
  • 원치 않는 원소를 제거할 때 사용
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter{ it.age > 30 })
>>> [Person(name=Bob, age=31)]

필수적인 함수 map

  • 원소를 변환하기 위해선 map 함수를 사용해야 함
  • 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새로운 컬렉션을 만든다
val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.map { it.name })
println(people.map(Person::name ) //멤버 참조 사용도 가능
>>> [Alice, Bob]
>>> [Alice, Bob]

 

filter 와 map 을 조합해서 사용하면?

컬렉션에서 나이가 31 이상인 사람의 이름을 알아보자..

people.filter{it.age > 30}.map(Person::name)
>>>[Bob]

이밖에도 map 을 처리하는 함수들이 있다

  • 키 관련 함수(걸러내는 : filterKeys, 변환하는 : mapKeys)
  • 값 관련 함수(걸러내느 : filterValues, 변환하는 : mapValues)
val numbers = mapOf(0 to "zero", 1 to "one")
val valuesMap = numbers.mapValues{ it.value.toUpperCase() }) //value값을 대문자로 변환한 후 새로운 컬렉션으로 반환
>>> {0=ZERO, 1=ONE}

 

all, any, count, find : 컬렉션에 술어 적용

  • 술어 : 참 거짓을 반환하는 함수
  • all : 모든 원소가 만족하면 true
  • any : 하나의 원소라도 만족하면 true
  • count : 만족하는 원소의 갯수를 리턴
  • find : 만족하는 첫번째 원소를 반환, 없으면 Null (null 를 반환하는걸 명시적으로 표현하고 싶다면 firstOrNull)
val canBeInClub27 = { p: Person -> p.age <= 27 } //술어 생성

val people = listOf(Person("Alice", 29), Person("Bob", 31))

people.all(canBeInClub27) // false
!people.all(canBeInClub27) // true

people.any(canBeInClub27) // true
!people.any(canBeInClub27) // false


people.filter(canBeInClub27).size // 1 (중간 컬렉션이 생겨서 비효율적)
people.count(canBeInClub27) // 1 (중간 컬렉션이 생기지 않아 효율적)

people.find(canBeInClub27) // 조건에 맞는 첫번째 객체 반환

 

groupBy : 리스트를 여러 그룹으로 이뤄진 맵으로 변경

  • 컬렉션의 원소를 구분하는 특성을 가지고 각 그룹을 만든다
val list = listOf("a", "ab", "b")
println(list.groupBy(String::first)) //String의 확장함수, 멤버 참조를 통해 접근 가능
>>> {a=[a, ab], b=[b]}

 

flatMap 과 flatten : 중첩된 컬렉션 안의 원소 처리

flatMap

  • 인자로 주어진 람다를 컬렉션의 모든 객체에 적용하고, 그 결과로 얻어지는 여러 리스트를 한 리스트로 만든다
val strings = listOf("abc", "def")

//toList 함수는 문자열에 적용하면, 문자열에 속한 문자들로 이루어진 리스트를 생성
println(strings.flatMap{ it.toList() }) 
>>> [a, b, c, d, e, f]
  • flatMap 에서 저자로만 이뤄진 리스트를 만들고, toSet 을 통해 중복된 저자를 제거
class Book(val title: String, val author: List<String>)

fun main(args: Array<String>) {
    val books = listOf(Book("Thursday Next", listOf("Jasper Fforde")),
                       Book("Mort", listOf("Terry Pratchett")),
                       Book("Good Omens", listOf("Terry Pratchett",
                                                 "Neil Gaiman")))
    println(books.flatMap { it.authors }.toSet() )// 저자들로만 리스트를 만들고, toSet을 통해 중복을 제거
}

>>>Jasper Fforde, Terry Pratchett, Neil Gaiman]

flatten

  • 중첩된 컬렉션의 원소를 한 리스트로 모으기만 해줌
  • 특별히 변환해야 할 내용이 없다면 사용 가능

 

지연 계산(lazy) 컬렉션 연산

  • 즉시 계산 컬렉션 연산
    • map, filter 연산은 결과 컬렉션을 즉시 생성
    • 따라서, 매 단계마다 계산의 중간 결과를 새로운 컬렉션에 임시로 저장
  • 지연 계산 컬렉션 연산
    • 시퀀스를 통해서 중간 임시 컬렉션을 사용하지 않고 컬렉션 연산 가능
    • 시퀀스는 컬레션에서 제공하는 함수 동일하게 사용 가능
//즉시 계산
//map과 filter 각각 중간 컬렉션 생성됨 (총 2개)
people.map(Person::name).filter{ it.startsWith("A") }

//지연 연산
//시퀀스는 원소를 하나씩 각 단계별로 수행하는 방식이기 때문에 중간 컬렉션 생성하지 않음
//최종 결과만 컬렉션 생성
people.asSequence()
  .map(Person::name)
  .filter{ it.startsWith("A") }
  .toList()
  • 컬렉션 -> 시퀀스 변환
    • asSequence() 확장 함수를 호출하면 어떤 컬렉션이든 시퀀스로 바꿀 수 있다
  • 시퀀스 -> 컬렉션 변환
    • 시퀀스를 이용한 결과는 시퀀스 형태이다.
    • 시퀀스 원소를 인덱스를 사용해 접근하는 등의 다른 API 메서드가 필요하면 리스트로 변환 필요

 

시퀀스 연산 실행 : 중간 연산과 최종 연산

  • 시퀀스는 중간 연산과 최종 연산으로 나뉜다
  • 중간 연산은 시퀀스를 반환하며, 최초 시퀀스의 원소를 변환하는 방법을 안다 (*지연 계산이 수행 됨)
  • 최종 연산은 결과를 반환하며, 중간 연산들을 실제로 수행하게 된다
  • 시퀀스는 원소들이 연산이 순차적으로 수행된다. 
// 최종 연산이 없어서 아무 내용도 출력되지 않음
listOf(1,2,3,4).asSequence()
  .map{ print("map($it)"); it * it }
  .filter{ print("filter($it)"); it % 2 == 0 }


/ .toList() 최종 연산 수행 시점에 map, filter동작도 수행
listOf(1,2,3,4).asSequence()
  .map{ print("map($it)"); it * it }
  .filter{ print("filter($it)"); it % 2 == 0 }
  .toList() // 최종 연산
  
  >>> map(1)filter(1)map(2)filter(4)map(3)filter(9)map(4)filter(16) //순차적인 원소 처리

 

지연 연산의 이점

지연 연산의 연산 특징은 결과를 만들어 내는데 불필요한 연산을 하지 않아도 된다.

println(listOf(1, 2, 3, 4).asSequence().map{ it * it }.find{ it > 3})
>>> 4

지연 연산시 원소를 한번씩 하나씩 처리한다

  • 즉시 연산보다 지연 연산의 연산 횟수가 2번이나 적다. (지연 연산이 효율적)
  • 시퀀스는 Java 8 의 Stream 과 매우 유사하게 동작함
  • 스퀀스와 달리 Java 8 의 Stream 연산은 CPU에서 병렬실행도 가능하다!

스퀀스 만들기

  • generateSequence() 함수를 사용해 스퀀스를 직접 생성할 수 있다
    • 이 전의 원소를 인자로 받아서 다음 원소를 계산하는 시퀀스 생성
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }

println(numbersTo100.sum())

 

자바 함수형 인터페이스 활용

  • 자바 8 이전에는 특정 로직을 수행하기 위해서는 무명 클래스를 이용했다
  • 코틀린에서는 무명 클래스 인스턴스 대신 람다를 넘길 수 있다
  • 이게 가능한 이유는 SAM 인터페이스이기 때문이다
  • SAM 인터페이스는 추상메소드가 단 하나만 있는 인터페이스 (ex Runnable, Callable)
//Java 8 이상
public interface OnClickListener{
	void onClick(View v);
}

public class Button{
	public void setOnClickListener(OnClickListener l)
}

button.setOnClickListener{ /* 클릭시 수행할 동작 */} // 람다로 인자를 전달

//Java 8 미만
button.setOnClickListener(new OnClickListener(){
	@Override
    public void onClick(View v){
    	/* 클릭시 수행할 동작 */
    }
})

//코틀린
//무명 클래스 인스턴스를 정의하고 활용할 필요가 없어서 깔끔함
button.setOnClickListener( view -> ...) //void onClick(View v); 파라미터와 대응 함

 

자바 메서드에 람다를 인자로 전달

  • 람다를 인자로 넘길때 컴파일러는 자동으로 람다를 무명클래스와 인스턴스를 생성해줌
  • 명시적으로 객체 식을 넘기는 경우 메서드를 호출할 때마다 새로운 객체가 생성됨
  • 하지만 변수 포획이 없는 람다를 넘기는 경우 메서드를 호출할 때 마다 동일 인스턴스를 사용
  • 만약 명시적인 object 선언(객체 식)을 사용하면서 람다와 동일한 동일 인스턴스 사용을 하려면 전역으로 선언해야 함
//Java 메서드 정의
void postponeComputatuion(int delay, Runnable computation);

//Java 메서드에 람다를 인자로 전달 (동일한 Runnable 인스턴스 객체 사용)
void postponeComputatuion(1000) {println(42)}

//Java 메서드에 객체 식을 전달 (메서드 호출할 때마다 Runnable 새로운 객체 생성)
postponeComputation(1000, object : Runnable {
  override fun run() {
    println(42)
  }
})

//Java 메서드에 객체 식을 전달 & 생성자를 통한 전역 선언
val runnable = Runnable{ println(42) }
fun handleComputation(){
	postponeComputation(1000, runnable) //동일한 Runnable 인스턴스 객체 사용
}

//Java 메서드에 람다를 인자로 전달 & 메서드 영역 변수를 포획
fun handleComputation(id:String){
	postponeComputation(1000) {println(id)} //변수 포획..매번 새로운 인스턴스 생성
}

 

SAM 생성자 : 람다를 함수형 인터페이스로 명시적으로 변경

  • SAM 생성자는 람다를 함수형 인터페이스의 인스턴스로 변환할 수 있게 컴파일러가 자동으로 생성한 함수다
  • 컴파일러가 자동으로 람다를 함수형 인터페이스 무명 클래스로 바꾸지 못하는 경우 SAM 생성자를 사용할 수 있다
  • SAM 생성자의 이름은 사용하려는 함수형 인터페이스의 이름과 같다
  • SAM 생성자는 그 함수형 인터페이스의 유일한 추상 메서드의 본문에 사용할 람다만 인자로 받아서, 함수형 인터페이스를 구현하는 클래스의 인스턴스를 반환한다
fun createAllDoneRunnable() : Runnable {
    return Runnable { println("All done!") }
}

createAllDoneRunnable().run()
>>>All done!

 

수신 객체 지정 람다 : with 와 apply

  • 수신 객체를 명시하지 않고 람다의 본문 안에서 다른 객체의 메서드를 호출 할 수 있게 하는 것
  • with
    • 객체 이름을 반복하지 않고 그 객체에 대해 다양한 연산을 가능케 함
    • 람다 결과를 반환
  • apply 
    • 람다의 결과 대신 수신 객체가가 필요한 경우에 사용 (객체 자체를 반환)
    • with와 유사하지만, 항상 자신에게 전달된 객체를 반환한다
    • 객체의 인스턴스를 만들면서 즉시 프로퍼티 중 일부를 초기화하는 경우에 사용
//with 사용하지 않음
fun alphabet() : String{
  val result = StringBuilder()
  for(letter in 'A'..'Z'){
    result.append(letter) // result 중복
  }
  result.append("append!") // result 중복 
  return result.toString() // result 중복
}


// with 사용함 (람다 안에서 사용)
fun alphabet() : String{
  val stringBuilder = StringBuilder()
  return with(stringBuilder){ //람다를 괄호 밖으로 빼냄
    for(letter in 'A'..'Z'){
      this.append(letter) // this 사용하여 메서드 호출
    }
    append("append!") // this 생략, 메서드 호출
    this.toString() // 	this 사용하여 결과 리턴
  }
}

fun alphabet() = with(stringBuilder){ //람다를 괄호 밖으로 빼냄
    for(letter in 'A'..'Z'){
      append(letter) 
    }
    append("append!") 
    toString() //마지막 식의 결과가 결과 값임
  }
}

// apply 사용함
fun alphabet() = StringBuilder().apply { //apply의 수신 객체가 람다식 안에서 수신 객체가 된다
  for(letter in 'A'..'Z'){
    append(letter) // 함수 바로 접근
  }
  append("append!") // 함수 바로 접근
}.toString()

>>> ABCDEFGHIJKLMNOPQRSTUVWXYZ
>>> Now I know the alphabet!
반응형

댓글