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

[9주차] 람다로 프로그래밍(3)

by 하동녹초오레오 2022. 1. 9.

5.0 람다식

  • 람다식 (람다) : 다른 함수에 넘길 수 있는 작은 코드 조각
  • 람다의 특징
    • 쉽게 공통 코드 구조를 라이브러리 함수로 뽑아낼 수 있음
    • 코틀린 표준 라이브러리에서 많이 사용함
    • 컬렉션 처리 대치
  • 수신 객체 지정 람다 : 람다 선언을 둘러싸고 있는 환경과는 다른 상황에서 람다 본문을 실행할 수 있음

 

5.1 람다 식과 멤버 참조

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

  • 일련의 동작을 변수에 저장하거나 다른 함수에 넘겨야하는 경우
    • 자바에서는 무명 내부 클래스 이용
      • ⇒ 단점 : 번거로움
      • ⇒ 예제) 무명 내부 클래스로 리스너 구현하기
    • /*자바*/ 
      button.setOnClickListener(new onClickListener(){ 
      	@Override 
          public void onClic(View view){ 
          //클릭시 수행할 동작 
          
          } 
      });
    • 함수형 프로그래밍에서는 함수를 값처럼 다뤄서 해결
      • ⇒ 람다식을 사용하면 코드블록을 직접 함수의 인자로 전달 할 수 있음
      • ⇒ 예제) 람다로 리스너 구현하기
      • button.setOnClickListener{/*클릭시 수행할 동작*/}
    • 메소드가 하나 뿐인 무명 객체 대신 람다를 사용할 수 있음

 

5.1.2 람다와 컬렉션

  • 사람의 이름과 나이를 저장하는 Person 클래스 예제
    • 사람들로 이뤄진 리스트, 그중 연장자 찾기
    • 람다 없이 구현 ⇒ 반복문 사용
    data class Person(val name: String, val age:Int)
    
    fun main()
    {
    	val people = listOf(Person("Alice", 29), Person("Bob", 31))
    	findTheOldest(people)
    }
    fun findTheOldest(people: List<People>)
    {
    	var maxAge = 0;
    	var theOldest: Person? = null
    	for(person in people){
    		if(person.age > maxAge){
    			maxAge = person.age
    			theOldest = person
    		}
    	}
    	println(theOldest)
    }
    
    • 더 좋은 방법 : 라이브러리 함수 활용
      • 모든 컬렉션에 대해 maxBy 함수 호출 가능
      • maxBy : 가장 큰 원소를 찾기 위해, 인자로 비교에 사용할 값을 반환하는 함수를 받음
        • {it.age} : maxBy에서 비교에 사용할 값을 반환하는 함수 (it은 컬렉션의 원소)
        • 예제에서 컬렉션의 원소 : Person 객체 ⇒ 함수가 반환하는 값은 Person 객체의 age 필드에 저장된 나이 정보
    val people = listOf(Person("Alice", 29), Person("Bob", 31))
    println(people.maxBy{it.age})//멤버 참조로 바꾸어 사용할 수 있음 
    //출력 결과 : Person(name=Bob, age = 31)
    
    • 멤버 참조를 사용해 컬렉션 검색하기
    people.maxBy(Person::age)
    

5.1.3 람다식의 문법

  • 람다 : 값처럼 여기저기 전달할 수 있는 동작의 모음
  • 람다식 문법
    • 항상 중괄호 사이에 위치
    • 매개변수 목록 주변에 괄호가 없음
    • → : 매개변수 목록과 본문을 구분해줌
    • {x: Int, y: Int -> x + y}
  • 람다식 변수에 저장
    • val sum = {x: Int, y: Int -> x+y} 
      println(sum(1,2)) 
      
      //람다식 직접 호출 
      //{println(42)}()
  • run : 인자로 받은 람다를 실행해주는 라이브러리 함수
    • run{println(42)}

❗매개변수 (parameter) : 함수 정의에 나열되어있는 변수

❗전달 인자, 인자 (argument) : 함수 호출시 전달되는 값

  • 연장자 예제 정식으로 람다식 작성해보기
    • p라는 Person 객체를 파라미터로 받아서 p의 age를 반환해줌
    • val people = listOf(Person("Alice", 29), Person("Bob", 31)) 
      //println(people.maxBy{it.age}) 
      people.maxBy({p:Person->p.age}) 
      //출력 결과 : Person(name=Bob, age = 31)
  • 위의 코드 개선시키기
    • 코틀린에서 함수 호출 시 맨 뒤에 있는 인자가 람다식이라면 그 람다를 괄호 밖으로 빼낼 수 있다. (문법 관습)
    • 람다가 어떤 함수의 유일한 인자이고 괄호 뒤에 람다를 썼다면 호출시 빈 괄호를 없앨 수 있음
    • 둘 이상의 람다를 인자로 받는 함수라도 인자 목록의 맨 마지막 람다만 밖으로 빼낼 수 있음
    • 컴파일러가 람다 파라미터의 타입 추론 가능함 ⇒ 파라미터 타입 생략 가능
    • 람다를 변수에 저장할 때는 타입 추론 불가능 ⇒ 파라미터 타입 명시
    • val getAge = {p: People -> p.age} 
      people.maxBy(getAge)
    • 람다의 파라미터가 하나뿐이고 그 타입을 컴파일러가 추론할 수 있는 경우 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} //디폴트 파라미터 이름 it 사용
  • 무조건 간단해진다고 가독성이 좋아지고 효율적인 코드가 되는 것은 아님
    • 아래 코드에서 윗줄이 더 간결하지만 람다의 용도를 알기 어려움
    • 상황에 따라 적절하게 람다를 괄호 밖으로 빼내야 함
    people.joinToString(" "){p: Person -> p.name}
    //people.joinToString("", {p:Person->p.name})

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

  • 람다를 함수 안에서 정의하면 함수의 파라미터와 로컬 변수를 람다에서 모두 사용 가능
  • 함수 파라미터를 람다 안에서 사용하기
    • fun printMessagesWithPrefix(messages:Collection<String>, prefix: String){ 
      	message.forEach{ // 각 원소에 대해 수행할 작업을 람다로 받음 
          	println("$prefix $it") // 람다 안에서 함수의 파라미터 사용 
          } 
      }
  • 람다 안에서 바깥 함수의 로컬 변수 변경하기
    • fun printProblemCounts(responses: Collection<String>){ 
      	var clientErrors = 0 
          var serverErrors = 0 
          responses.forEach { 
          	if(it.startsWith("4")){ 
              	clientErrors++ // 람다 안에서 람다 밖의 변수 변경 
              } 
              else if(it.startWith("5")){ 
              	serverErrors++ // 람다 안에서 람다 밖의 변수 변경 
              } 
          } 
          println("$clientErrors client errors, $serverErros server errors") 
      }

5.1.5. 멤버 참조

  • 이중 콜론(::)을 사용하는 식
  • 프로퍼티나 메소드를 단 하나만 호출하는 함수값을 만들어줌
  • 이중 콜론(::)은 클래스 이름과 참조하려는 멤버 이름 사이에 위치함
  • 멤버 참조 뒤에는 괄호를 넣으면 안된다. (참조대상이 함수인지 프로퍼티인지와 관계 x )
Person::age
//Person은 클래스 , age는 참조하려는 멤버
//val getAge = {person: Person -> person.age} //멤버 참조와 같은 역할을 하는 코드

 

5.2 컬렉션 함수형 API

5.2.1. 필수 적인 함수: filter와 map

  • Person 클래스
    • data class Person(val name: String, val age: Int)
  • filter 함수
    • 컬렉션을 이터레이션하면서 주어진 람다에 각 원소를 넘겨서 람다가 true를 반환하는 원소만 모은다.
    • 컬렉션에서 원치않는 원소를 제거함
    val list = listOf(1,2,3,4)
    println(list.filter{it%2 == 0})
    
  • map 함수
    • 주어진 람다를 컬렉션의 각 원소에 적용한 결과를 모아서 새 컬렉션을 만듦
    • 원소를 변환할 수 있음
    val list = listOf(1,2,3,4)
    println(list.map{it*it})
    

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

  • all 함수 : 모든 원소가 해당 조건에 만족하면 true, 아니면 false를 반환함
  • any 함수 : 조건에 만족하는 원소가 하나라도 있다면 true 반환 아니라면 false 반환함
  • count 함수 : 조건을 만족하는 원소의 개수를 반환함
  • find 함수 : 조건을 만족하는 첫 번째 원소를 반환함

5.2.3. groupBy : 리스틀르 여러그룹으로 이뤄진 맵으로 변경

  • groupBy : 특성을 파라미터로 전달하면 컬렉션을 구분해주는 함수
    • val people = listOf(Person("Alice", 31), ... Person("Bob", 29), Person("Carol", 31)) 
      println(people.groupBy{it.age})
  • groupBy 연산 결과 : 원소를 구분하는 특성이 key, key 값에 따른 각 그룹이 값인 맵

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

  • flatMap : 모든 원소로 이루어진 단일 리스트를 반환, 내용의 변환이 필요할 때 사용
  • flatten : 모든 원소로 이루어진 단일 리스트를 반환, 내용의 변환이 필요없을 때 사용

 

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

  • 앞서 배운 컬렉션 함수들은 리스트를 반환함 == 연산할 때 새로운 리스트가 생겨남
    • people.map(Person::name).filter{ it.startsWith("A") } 
      //각 연산이 컬렉션을 직접 사용 
      //즉, 두개의 리스트(임시 컬렉션)가 새로 생겨남
  • 시퀀스를 사용하면 계산 중간에 임시 컬렉션을 사용하지 않고 컬렉션 연산을 여러번 이어 할 수 있음
    • 결과는 같지만 성능이 더 좋아짐
    people.asSequence() //원본 컬렉션을 시퀀스로 변환
    	.map(Person::name)
    	.filter{ it.startsWith("A")} // 시퀀스도 컬렉션과 같은 API 제공함
    	.toList() //시퀀스를 다시 리스트로 변환
    
  • 시퀀스 : 순차적인 컬렉션으로 요소의 크기를 특정하지 않고 나중에 결정할 수 있는 특수한 컬렉션, iterator를 통해 원소에 접근해야함
  • asSequence 확장함수를 호출해서 어떤 컬렉션이든 시퀀스로 바꿀 수 있음
  • toList() : 시퀀스를 리스트로 만들 때 사용하는 함수
  • 지연 계산 후 Sequence를 List로 다시 바꾸어 주는 이유 : List는 인덱스로 원소 접근하는 등 다른 api 메소드를 활용할 수 있어서
  • Sequence는 iterator로 원소 접근 vs List는 인덱스로 원소 접근 가능
  • ⇒ 원소를 차례로 이터레이션 해야 할 경우 : Sequence 그대로 사용해도 됨

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

  • 중간 연산
    • 다른 시퀀스를 반환
    • 항상 지연 계산됨
  • 최종 연산
    • 결과를 반환
    • 결과는 최초 컬렉션에 대해 변환을 적용한 시퀀스로부터 일련의 계산을 수행해 얻을 수 있는 컬렉션이나 원소, 숫자 또는 객체다.
sequence.map{...}.filter{...}.toList()
//증간 연산 : map{...} filter{...}
//최종 연산 : toList()
  • 최종 연산이 없는 예제
    • listOf(1,2,3,4).asSequence() 
      	.map{print("map($it)"); it * it} 
          .filter{print("filter($it)"); it%2 == 0} 
      //실행시키면 아무것도 출력되지 않음​
       
    • map과 filter 변환이 늦춰져서 결과를 얻을 필요가 있을 때 (즉 최종 연산이 호출될 때) 적용됨
  • 최종 연산 있는 예제
    fun main(){
        
       val nums = listOf(1,2,3,4).asSequence()
            .map{print("map($it)"); it * it}
            .filter{print("filter($it)"); it%2 == 0}
            .toList()
        println()  
        print(nums)
    }
    
  • 연산 순서
    • 컬렉션에 대한 연산 : map 함수를 각원소에 대해 먼저 수행해 새 컬렉션을 얻고 그 컬렉션에 대해 다시 필터를 수행함
    • 시퀀스에 대한 연산 : 모든 연산은 각 원소에 대해 순차적으로 적용됨. 첫번째 원소가 변환된 다음에 걸러지면서 처리되고 다시 두번째 원소가 처리되며 이런 처리가 모든 원소에 적용
    • /* 
      첫번째 원소 1 : map(1) 출력 → 1*1 저장됨 → filter(1) 출력 → 1%2 ≠0 이라 걸러짐 
      두번째 원소 2 : map(2) 출력→ 2*2 저장됨 → filter(4) 출력 → 4%2 == 0 이라 안걸러짐 
      세번째 원소 3: map(3) 출력 → 3*3 저장됨 → filter(9) 출력 → 9%2 ≠0 이라 걸러짐 
      네번째 원소 4: map(4) 출력 → 4*4 저장됨 → filter(16) 출력 → 16%2 == 0 이라 안걸러짐 
      ⇒ 4, 16만 남음 */

댓글