Kotlin in Action - 5장

김정욱·2021년 10월 4일
1

Kotlin

목록 보기
4/5
post-thumbnail

해당 글은 Kotlin in Action 도서를 읽으며 정리한 내용입니다

[5장] 람다로 프로그래밍

람다 식과 멤버 참조

[ 람다 & 람다식 ]

  • 람다
    • 다른 함수에 넘길 수 있는 작은 코드 조각
    • 함수를 값처럼 다루는 접근방법 (함수는 직접 다른 함수에 전달 가능)
    • 무명 내부 클래스로 일련의 동작을 변수에 저장하거나, 넘겼던 작업을 대신함
    • 대부분 람다는 함수에 인자로 넘기면서 바로 정의해서 사용하는 경우가 많다
  • 람다 식의 문법
    • 파라미터와 본문으로 구성
    • -> 를 통해서 인자 목록과 람다 본문을 구분
    • 항상 중괄호 {} 를 통해 둘러쌓여 있음
    • 실행 시점에 코들린 람다 호출은 아무 부가 비용이 들지 않는다
/* 람다식을 변수에 저장 */
val sum = { x: Int, y: Int -> x+y }

/* 사용 */
println(sum(1,2))

/* 생성 후 즉시 사용 방법 (1) */
{ println(42) }()

/* 생성 후 즉시 사용 방법 (2)
  -> run을 통한 즉시 사용이 더 보기 좋다!
*/
run { println(42) }
  • run은 인자로 받은 람다를 실행해주는 라이브러리
  • 람다의 변형
    • 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면
      => 괄호 밖으로 람다를 뺄 수 있는 관습이 존재
      => 람다가 어떤 함수의 유일한 인자이고, 괄호 뒤에 람다가 있다면 빈 괄호를 없앨 수 있다
    • 컴파일러가 문맥으로 유추할 수 있는 인자 타입을 생략해서 간단하게 표기할 수 있음
      => 파라미터가 여러개인 경우, 일부 타입만 지정해도 가능
    • 인자가 단 하나뿐인 경우 굳이 인자에 이름을 붙이지 않아도 된다
      => default name인 it 로 사용 가능
      => 람다가 중첩되는 경우에는 겹치기 때문에 명시적으로 선언하는 것이 좋다
/* 원래 호출 */
people.maxBy({p: Person -> p.age})

/* 람다가 유일한 인자라서 괄호 밖으로 추출 */
people.maxBy(){p: Person -> p.age}

/* 유일한 인자라서 빈 괄호 삭제 */
people.maxBy{p: Person -> p.age}

/* 컴파일러가 문맥으로 타입을 추론할 수 있어서 타입 지정 X */
people.maxBy{p -> p.age}

/* 인자가 하나뿐이라서, 이름 생략 후 default값이 it로 사용 */
people.maxBy{it.age}


/* 변수에 람다를 저장할 때에는, 타입 추론 문맥이 없으므로 타입 명시 필요 */
val = getAge = { p: Person -> p.age }

/* 본문이 여러 줄인 람다는 마지막 식이 결과(return)값을 의미 */
val sum = { x: Int, y: Int ->
  ...
  ...
  x + y // result값을 의미
}

[ 변수 포획 ]

  • 변수 포획
    • 람다 함수 안에서 외부 변수를 사용하는 것
    • Java와 다르게 코틀린 람다는 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++ // 람다 내부에서 외부 필드를 접근 & 변경
    }
  }
}
  • 변수 포획과 생명주기
    • 함수 내부의 일반적인 로컬 변수의 생명 주기는 함수가 반환되면서 끝난다
    • 하지만, 해당 로컬 변수를 람다가 포획하여 반환하거나, 다른 변수에 저장하면
      => 함수의 생명주기와 달라질 수 있다
      => 클로저(closure)
    • 생명 주기(life cycle)가 달라질 수 있는 원리
      • final 변수를 포획
        => 람다 코드를 변수 값과 함께 저장
      • final이 아닌 변수를 포획
        => 특별한 래퍼(wrapper)로 감싼 뒤, 나중에 변경하거나 읽을 수 있게 한다
        => 감싼 래퍼의 참조를 람다 코드와 함께 저장

[ 멤버 참조 ]

  • 멤버 참조
    • 이중 콜론(::)을 통해서 프로퍼티나 메소드를 단 하나만 호출하는 함수 값을 만들어 준다
    • 클래스 이름 :: 프로퍼티 or 클래스 이름 :: 메소드 형식으로 사용
    • 최상위에 선언된 함수 / 프로퍼티를 참조할 수 있음
    • 확장 함수에도 적용 가능
    • Java 8의 메소드 레퍼런스와 유사
/* 람다 표기 */
people.maxBy{p -> p.age}
people.maxBy{it.age}

/* 멤버 참조 표기 */
people.maxBy(Person::age)


/* 최상위 함수 참조 */
fun salute() = println("Salute!")
run(::salute) // 최상위 함수를 참조


/* 확장 함수에 적용 */
fun Person.isAdult() = age >= 21
val predicate = Person:: isAdult
  • 생성자 참조
    • 클래스 생성 작업 내용을 값으로 저장 하는 것
    • 클래스 생성을 연기하거나 저장해둘 수 있다
/* 생성자 참조 */
data class Person(val name: String, val age: Int)

val createPerson = ::Person // Person 인스턴스 생성 작업을 값으로 저장
val p - createPerson("hue", 26) // 생성자 참조를 통해 Person 인스턴스 생성

컬렉션 함수형 API

[ 필수적인 함수 : filter와 map ]

  • filter 함수
    • 컬렉션을 이터레이션하면서, 람다에 각 원소를 넘기고 true를 반환하는 원소만 모은다
    • 결과는 조건에 true를 만족하는 원소로 이루어진 새로운 컬렉션(Collection)
    • 원치 않는 원소를 제거하는 목적으로 사용
val list = listOf(1,2,3,4)
val newList = list.filter {it%2 == 0} // 짝수인 원소들로 이루어진 컬렉션을 반환
  • map 함수
    • 주어진 람다를 각 원소에 적용한 결과를 모아서 새 컬렉션으로 반환
/* map함수로 새로운 컬렉션 반환 */
val people = listOf(Person("Alice", 29), Person("Bob", 31))
val nameList = people.map{it.name}

/* 멤버 참조를 이용한 같은 표현 */
val nameList = people.map(Person::name)


/* filter + map */
people.filter{ it.age > 30 }.map(Person::name)
  • map 컬렉션과 filter / map
    • 맵의 경우 키와 값을 처리하는 filter / map함수가 따로 존재
    • key 처리
      • filterKeys()
      • mapKeys()
    • value 처리
      • filterValues()
      • mapValues()
val numbers = mapOf(0 to "zero", 1 to "one")
/* value값을 대문자로 변환한 후 새로운 컬렉션으로 반환 */
val valuesMap = numbers.mapValues{ it.value.toUpperCase() }

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

  • 술어
    • 참 / 거짓을 반환하는 함수
  • 다양한 함수
    • all : 모든 원소가 술어를 만족하는지 여부를 true / false로 반환
    • any : 술어를 만족하는 원소가 하나라도 있는지 여부를 true / false로 반환
    • count : 술어를 만족하는 원소의 개수를 반환
    • find : 술어를 만족하는 가장 첫 원소를 찾아서 반환
/* 술어 */
val canBeInClub27 = { p: Person -> p.age <= 27 }
/* Person List */
val people = listOf(Person("Alice", 29), Person("Bob", 31))

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

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

/* 개수 구하기 ( filter + size vs count ) */
/* filter 후 size로 개수를 가져오는 방식
   => 조건을 만족하는 중간 컬렉션이 생김 
   => 비효율적 */
people.filter(canBeInClub27).size // 1
/* count
   => 조건을 만족하는 원소의 개수만 추적
   => 따로 중간 컬렉션이 없어서 filter + size보다 효율적!
*/
people.count(canBeInClub27) // 1


/* find
   => 술어에 해당하는 것이 없다면 null 반환 */
people.find(canBeInClub27) // 조건에 맞는 첫번째 객체 반환

[ group : 리스트를 여러 그룹으로 이루어진 맵으로 변경 ]

  • group 함수
    • 컬렉션의 모든 원소를 어떤 특성에 따라 그룹으로 분리
    • 구분하고 싶은 특성을 파라미터로 전달
/* 특정 특성을 통해 그룹화 */
val list = listOf("a", "ab", "b")
println(list.groupBy(String::first))
// {a=[a, ab], b=[b]}

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

  • flatMap
    • 인자로 주어진 람다를 컬렉션의 모든 객체에 적용 후, 여러 리스트를 한 리스트로 모은다
    • 람다 적용 + 평평하게
/* 저자의 정보를 뺀 후, set으로 변환해서 중복 값을 제거 */
class Book(val title: String, val author: List<String>)
books.flatMap{ it.author }.toSet()

/* String.toList() : String 문자열을 모든 문자로 분리 */
val strings = listOf("abc", "def")
println(strings.flatMap( it.toList() ))
// [a, b, c, d, e, f] 
  • flatten
    • 중첩된 컬렉션의 원소를 한 리스트로 모으기만 해주는 역할
    • 굳이 반환할 것이 없는 중첩 리스트에서 유용하게 사용
    • 평평하게 만 해줌

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

[ 컬렉션 연산 비교 ]

  • 즉시 계산(eager) 컬렉션 연산
    • 앞서 살펴본 map, filter와 같은 연산은 결과 컬렉션을 즉시 생성
    • 즉시라는 말은, 매 단계마다 계산의 중간 결과를 새로운 컬렉션이 임시로 담게 된다
  • 지연 계산(lazy) 컬렉션 연산
    • 시퀀스(sequence)를 통해서 중간 임시 컬렉션 없이 컬렉션 연산을 수행
      => 효율적 계산 수행 가능
/* 즉시 계산 컬렉션 연산
   => 2개의 임시 컬렉션이 생성 (map, filter) */
people.map(Person::name).filter{ it.startsWith("A") }


/* 지연 계산 컬렉션 연산
   => 중간 임시 컬렉션 없이 최종 결과 컬렉션만 생성
   => 하나의 원소가 차례대로 각 단계를 수행하는 방식이기 때문 */
people.asSequence()
  .map(Person::name)
  .filter{ it.startsWith("A") }
  .toList()
  • 컬렉션 => 시퀀스 변환
    • Sequence 인터페이스의 asSequence()를 통해 어떤 컬렉션이든 sequence로 변환 가능
  • 시퀀스 => 컬렉션 변환
    • 시퀀스를 이용한 결과는 시퀀스 형태이다
    • 시퀀스를 통해 인덱스로 접근하는 다른 API가 필요하다면 결국 리스트로 변환해야 한다
    • .toList() 등등

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

  • 시퀀스 연산 종류
    • 중간(intermediate) 연산
      • 결과(return) : 또 다른 시퀀스를 반환
      • 항상 지연 계산이 수행됨 => 즉, 최종 연산이 있을 때에 수행
    • 최종(terminal) 연산
      • 결과(return) : 결과를 반환
      • 앞의 중간 연산들을 실제로 수행하게 한다
  • 시퀀스 연산의 동작 수행
    • 최종 연산이 수행 될 때, 앞에 정의된 모든 중간 연산이 수행
    • 모든 원소들에 대해, 하나의 원소가 한 단계의 연산을 거쳐 최종 결과까지 수행 됨
      (원소를 하나씩 끝까지 처리하는 방식)
      => 모든 원소가 하나의 연산을 통해 중간 결과를 만드는 non-sequence 방식 보다 효율적!
/* 최종 연산이 없어서 아무런 동작도 수행하지 않는다 */
listOf(1,2,3,4).asSequence()
  .map{ it * it }
  .filter{ it % 2 == 0 }


/* .toList()라는 최종 연산이 있어서 비로소 이 때 map, filter동작도 수행 */
listOf(1,2,3,4).asSequence()
  .map{ it * it }
  .filter{ it % 2 == 0 }
  .toList() // 최종 연산
  • Java 8의 Stream vs Kotlin의 Sequence
    • Java 8의 Stream과 매우 유사하게 동작한다
    • Java 8에서는 스트림 연산을 CPU에서 병렬적으로 실행하는 기능이 존재
      => 필요시 Sequence가 아니라, Stream을 선택적으로 이용해도 좋다!

[ 시퀀스(Sequence) 만들기 ]

  • Collection.asSequence() 함수
    • Collection => Sequence로 변환
  • generateSeqence() 함수
    • 이 전의 원소를 인자로 받아서 다음 원소를 계산하는 시퀀스를 생성
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo100 = naturalNumbers.takeWhile { it <= 100 }
/* sum() 이라는 최종 연산을 통해 모든 원소를 더한결과를 반환 */
println(numbersTo100.sum())

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

  • Java에서 특정 로직을 수행하기 위해서는 무명 클래스를 이용했다
    => Kotlin에서는 람다를 통해서 이를 대신할 수 있다
    => 왜냐하면, SAM이기 때문!
  • SAM 인터페이스
    • Single Abstract Method 의 약자
    • 추상 메소드가 단 하나만 있는 인터페이스
    • ex) Runnable / Callable

[ Java 메소드에 람다를 인자로 전달 ]

  • 람다를 인자로 넘길 때, 람다가 주변 외부 변수를 포획하면
    => 포획한 변수를 저장하는 필드가 생긴다
    => 즉, 호출 시 마다 새로운 인스턴스를 생성하게 된다
  • 람다가 주변 외부 변수를 포획하지 않으면
    => 호출 시 마다 같은 인스턴스를 사용
/* Java 메소드 */
void postponeComputation(int delay, Runnable computation)

/* 람다를 인자로 전달
   => 객체를 명시적으로 선언하지 X + 변수 포획 X
   => 호출시마다 같은 객체를 공유 */
postponeComputation(1000) {println(42)}

/* 람다를 인자로 전달
   => 객체를 명시적으로 선언 O
   => 호출시마다 새로운 객체를 생성 */
postponeComputation(1000, object : Runnable {
  override fun run() {
    println(42)
  }
})

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

[ 공통 ]

  • 수신 객체 지정 람다
    • 수신 객체를 명시하지 않고, 람다의 본문 안에서 다른 객체의 메소드를 호출할 수 있는 것
    • with와 apply 를 통해 구현

[ with 함수 ]

  • 개념
    • 객체의 이름을 반복하지 않고, 객체에 대해 다양한 연산을 수행하도록 도와주는 라이브러리
  • 원리
    • 첫 번째 인자로 받은 객체를 두 번째 인자로 받은 람다의 수신 객체로 지정하는 원리
  • 람다에서 사용
    • this를 통해서 접근 가능
    • 프로퍼티나 메소드 이름만으로도 접근 가능
  • 반환 값
    • 람다 코드를 실행한 결과
    • 람다 식의 본문에서 마지막 식의 값
    • 만약, 수신 객체 자체를 return하려면 => apply 함수를 사용!
/* with 사용 X */
fun alphabet() : String{
  val result = StringBuilder()
  for(letter in 'A'..'Z'){
    result.append(letter) // result 중복
  }
  result.append("append!") // result 중복 
  return result.toString() // result 중복
}


/* with 사용 O */
fun alphabet() : String{
  val stringBuilder = StringBuilder()
  return with(stringBuilder){
    for(letter in 'A'..'Z'){
      this.append(letter) // this를 통해서 접근
    }
    append("append!") // this 없이도 함수에 바로 접근 가능
    this.toString() // return 값
  }
}

[ apply 함수 ]

  • apply
    • with와 유사하지만, 항상 자신에게 전달된 객체를 반환한다는 차이점이 존재
    • with는 람다의 결과를 반환 / apply는 객체 자체를 반환
/* apply 사용 O */
fun alphabet() = StringBuilder().apply {
  for(letter in 'A'..'Z'){
    append(letter) // 함수에 바로 접근해서 사용
  }
  append("append!") // 함수에 바로 접근해서 사용
}.toString()

[ 요약 ]

  • 람다를 사용하면 코드 조각다른 함수에게 인자로 넘길 수 있다
  • Kotlin에서 람다함수 인자인 경우, 괄호 밖으로 빼낼 수 있다
  • 람다의 인자단 하나뿐인 경우, 이름을 지정하지 않고 기본 값it로 사용할 수 있다
  • Kotlin의 람다 안에 있는 코드외부 변수읽거나 쓸 수 있다
  • 메소드, 생성자, 프로퍼티의 이름 앞에 ::을 붙이면 각각에 대한 참조를 만들 수 있다
  • filter, map, all, any 등의 함수로 컬렉션의 대부분의 연산을 간편하게 할 수 있다
  • 시퀀스(sequence)를 사용하면, 중간 결과를 담는 컬렉션 생성 없이연산을 조합할 수 있다
  • 수신 객체 지정 람다를 사용하면 람다 안에서 미리 정해둔 수신 객체메소드직접 호출할 수 있다
  • with 함수를 통해 특정 객체의 참조를 반복해서 언급하지 않고 메소드를 호출할 수 있다
  • apply를 통해서 어떤 객체라도 빌더 스타일의 API를 사용해서 생성하고 초기화 할 수 있다
profile
Developer & PhotoGrapher

0개의 댓글