[Kotlin] 람다와 고차함수

uuranus·2024년 4월 19일
post-thumbnail

람다

다른 함수에게 넘길 수 있는 코드 조각

  • 자바 8 버전에서 람다가 도입되기 전에는 무명클래스를 통해 필요한 동작코드를 전달
  • 하지만 람다를 통해서 훨씬 간단하게 코드조각을 전달해줄 수 있다.
{ x: Int, y: Int -> x + y }
  • 위처럼 중괄호와 -> 로 람다 식을 표현
  • 람다가 인자의 마지막인 경우는 () 밖으로 뺄 수 있다.
  • 람다 파라미터가 하나인 경우에는 it 디폴트 이름으로 사용 가능

변수 포획 (capture)

  • 람다는 람다 외부에 있는 변수를 접근해 변경할 수 있다.
class Ref<T>(var value: T)
>> val counter = Ref(0)
>> val inc = {counter.value++}

var count = 0
val inc = { counter++ } // 내부적으로 Ref클래스를 통해 포획
  • 다음과 같이 변경할 수 있는 변수를 가진 final한 클래스를 포획해서 나중에 변경할 수 있게 된다.

수신객체 지정 람다 (lambda with receiver)

  • 람다는 확장함수처럼 본문에서 다른 객체를 명시하지 않고 this로 참조할 수 있는 람다다.
  • 객체를 반복해서 호출하지 않아도 되기 때문에 코드가 간결해진다.
  • with, apply, run, let, also 등등이 있는데 아래 scope function에 자세히 적어놓았다.

함수형 인터페이스

  • 자바의 함수형 인터페이스를 제공해야 하는 API를 사용하는 경우 람다를 대신 넘길 수 있다.

SAM (Single Abstract Method)

  • 추상 메서드가 하나만 존재하는 인터페이스이다.
  • OnClickListener가 대표적
button.setOnClickListener(new OnClickListener() {
	@Override
	public void onClick(View v){
		...
	}
}
  • 자바에서는 이렇게 무명객체를 만들어서 전달해줘야했지만
button.setOnClickListener { view -> ... }
  • 코틀린에서는 이렇게 바로 람다만 전달해줄 수 있다.

  • 반대로 자바 8이전에서 코틀린 람다를 전달하는 API를 사용하고 싶은 경우

processTheAnswer(
	new Function1<Integer,Integer>() {
			@Override
			public Integer invoke(Integer number) {
				System.out.println(number);
				return number + 1;
			}
	}
);
  • 이런식으로 코틀린에서 제공하는 FunctionX 인터페이스를 통해서 구현할 수 있다.

무명객체 vs 람다

  • 무명객체는 메서드를 호출할 때마다 새로운 객체를 생성한다.
  • 람다는 하나의 인스턴스를 반복 사용한다.
    • 그러나 외부 변수를 포획한 경우에는 외부 변수 값이 변경되기 때문에 호출할 때마다 새로운 인스턴스를 생성한다.
  • 그리고 람다를 그냥 함수타입으로 사용하는 경우는 SAM 방식으로 호출하지 않는다.

고차함수

함수를 인자로 전달하거나 반환할 수 있는 함수

함수 타입

  • 함수를 전달하려면 함수 타입이 있어야 한다.
(p1: Int, p2: String) -> R: Int

이런 식으로 표현한다.

inline

  • 람다가 외부변수를 포획하면 매번 새로운 무명 클래스를 생성한다.
  • 그럼 람다를 자주 사용하면 성능이 떨어지는 거 아닌가?
    • 이 때 inline을 통해서 함수에 전달되는 람다를 같이 호출된 함수의 본문에 inline 시킨다.
  • 그러나 inline 함수가 너무 크면 좋지 않다.
    • 대부분 컬렉션 API 함수들은 inline이다.

넌로컬 리턴

  • inline으로 람다가 인라이닝되면 람다가 전달되는 함수를 호출하는 함수를 알 수 있게 되므로 외부 함수를 람다에서 리턴할 수 있게 된다.
fun lookForAlice(people: List<Person>) {
	people.forEach {
    	if( it.name == "Alice") {
        		println("Found!")
                return //lookForAlice가 반환됨
        }
    }
    println("Alice is not found!")
}

함수형 컬렉션 API

  • 컬렉션에서 for 문 대신에 함수형 컬렉션 API를 통해서 컬렉션 요소들을 편하게 iterate할 수 있다.
  • 람다 대신에 :: 멤버 참조를 통해서 이미 만들어진 메서드, 프로퍼티 등의 참조를 전달해줄 수 있다.
  • filter, map, all, any, count, find, groupBy, flatMap 등등 다양하게 존재

scope function

  • let
    public inline fun <T,R> T.let(block: (T) -> R): R
    • null이 될 수 있는 식을 다룰 때 사용
      • taskId?.let{}으로 ?뒤에 사용하면 let안에서는 taskId가 null이 아님을 보장할 수 있다.
      • 만약 taskId가 null이 될 수 있으면 let{} ?: 으로 elvis 연산자를 통해 null처리도 해줄 수 있다.
      • if(taskId==null) {} else {}와 동일
  • run
    • 반환타입이 없는 경우
      public inline fun <R> run(block: () -> R): R
      • 람다에 넘어오는 값이 it으로 넘어옴
      • 클래스의 run 함수와 매개변수와 이름이 동일해 구분되지 않으므로 kotlin.run{}으로 사용해야 한다.
    • 반환타입이 있는 경우
      public inline fun<T,R> T.run(block: T.() -> R): R
      • 람다에 넘어오는 값이 this로 넘어옴
  • with
    public inline fun <T,R> with(receiver: T, block: T.() -> R): R 
    • 거의 사용하지 않음
      • with에 들어오는 값의 null check를 해주지 않기 때문
      • nullable한 값이라면 내부에서 ?.let을 또 해줘야 하는데 그럴바에 그냥 처음부터 let을 쓰는 게 더 이득
      • this로 받는 게 let과의 차이점인데 null이 절대 아니면 사용할 만 하지만 null이 아닌 경우에 run이랑 동일 기능을 제공하기 때문에 run을 대신 써도 됨
  • apply
    public inline fun <T> T.apply(block: T.() -> Unit): T
    • this로 값이 전달 됨
    • 전달받은 수신객체를 리턴
      • 주로 객체 생성 시 초기화을 동시에 해줄 때 사용
  • also
    public inline fun <T> T.also(block: (T) -> Unit): T
    • apply와 비슷하지만 it으로 전달되기 때문에 이미 생성한 객체의 세부 속성을 변경하고자 할 때 사용
    • 마찬가지로 수신객체를 리턴하기에 내부 속성 변경용으로 사용

T.() vs (T)

  • scope function 보니 T.() 도 있고 (T)도 있는데 차이점은 무엇일까

확장함수

어떤 클래스의 메소드인 것처럼 호출할 수 있는 함수

  • 즉, 진짜 그 클래스 내 메소드가 아니고 외부에서 선언되었지만 그 클래스 메소드인것 처럼 클래스.메소드이름() 이렇게 부를 수 있는 것
fun String.lastChar(): Char = this[this.length-1]
  • 위 식이 확장함수이며
  • 앞의 String. 수신 객체의 타입 (receiver type)
    • 확장할 클래스의 타입,이름
  • 뒤의 this가 수신 객체 (receiver object)
    • 확장 함수가 호출되는 대상이 되는 객체
    • 만약 “hello,world!”.lastChar()를 호출했다면 this는 “hello, world!” 라는 String 객체 인스턴스를 가리킴
  • 확장함수는 정적메소드이므로 오버라이딩 할 수 없다.

(T)

fun String.lastChar(block: (String) -> Unit): Char {
	block(this)
	return this[this.length-1]
}
  • 그럼 (T) 로 전달을 하게 되면 수신객체의 값이 it으로 전달이 된다!

T.()

  • T.()은 확장함수처럼 보인다.
  • block이라는 람다식의 파라미터 ()가 T의 확장함수라는 의미이다.
  • 그럼 파라미터라는 새로운 확장함수 내부에서 T를 this로 사용할 수 있다
    • lastChar를 생각해라! (String.lastChar() = this 로 접근가능했다)
  • 람다이름()으로 호출만 해도 T의 확장함수로 호출되는 효과가 생겨서 내부적으로 this를 사용할 수 있게 된다.
fun String.lastChar(block: String.() -> Unit): Char {
	block()
	return this[this.length-1]
}
  • this로 변경된 것 빼고는 값은 동일하게 나온다!

시퀀스 (Sequence)

  • 함수형 컬레션은 함수결과 리스트를 바로바로 생성
    • 중간결과를 담는 임시 컬렉션이 생성된다.
    • 컬레션 원소가 많아지면 메모리를 많이 사용
    • 시퀀스는 중간리스트가 없기 때문에 원소가 많은 경우 유용

중간연산, 최종연산

  • 중간연산 (intermediate)
    • 시퀀스를 반환
    • 최종연산이 호출되면 그제서야 각 원소에 대해 필요한 중간연산과정들을 계산한다.
    • 중간에 원하는 결과값을 찾으면 그 이후 원소는 계산안할 수도 있다.
    • 최종연산을 호출하지 않으면 아무것도 출력되지 않는다.
  • 최종연산 (terminal)
    • 결과를 얻을 수 있는 연산
    • toList() 가 대표적
      • 꼭 컬렉션으로 변경해야 하는가?
      • Sequence로 남겨둬서 iterator를 사용할 거라면 변경안해도 됨
      • 그러나 인덱스를 통해 컬렉션 API 쓰고 싶으면 컬렉션으로 변경해야

스트림

  • 시퀀스를 자바의 스트림과 동일한 개념
  • 그럼 왜 또 만들었냐?
    • 코틀린 액션에서는 "안드로이드 등에서 예전 버전 자바를 사용하는 경우 자바 8에 있는 스트림이 없기 때문이다." 라고 적혀있다.
    • 내 생각으로는 자바 8 이전버전으로 구현한 안드로이드에서 스트림을 사용하고 싶을 때 코틀린의 시퀀스를 사용할 수 있도록 만든게 아닌가? 싶다.

출처

Kotlin in Action 5장, 8장

profile
Frontend Developer

0개의 댓글