코틀린 입문 스터디 (16) Lambda with Receiver

mook2_y2
12 min readMar 10, 2019

스터디파이 코틀린 입문 스터디 (https://studypie.co/ko/course/kotlin_beginner) 관련 자료입니다.

코틀린 입문반은 Kotlin을 직접 개발한 개발자가 진행하는 Coursera 강좌인 “Kotlin for Java Developers” (https://www.coursera.org/learn/kotlin-for-java-developers) 를 기반으로 진행되며 아래는 본 강좌 요약 및 관련 추가 자료 정리입니다.

목차

(1) Introduction

(2) From Java to Kotlin

(3) Basics

(4) Control Structures

(5) Extensions

(6) 실습 : Mastermind game

(7) Nullability

(8) Functional Programming

(9) 실습 : Mastermind in a functional style, Nice String, Taxi Park

(10) Properties

(11) Object-oriented Programming

(12) Conventions

(13) 실습 : Rationals, Board

(14) Inline functions

(15) Sequences

(16) Lambda with Receiver

(17) Types

(18) 실습 : Game 2048 & Game of Fifteen

1. Lambda with receiver

  • Lambda with receiver (extension lambda 라고도 함)는 확장함수와 lambda가 결합된 개념으로 일반 lambda에 대한 일반 함수 -> 확장 함수의 관점으로 볼 수 있고, 확장 함수에 대한 일반 함수 -> 일반 lambda의 관점으로 볼 수 있습니다.
  • 일반 lambda에 대한 일반 함수 -> 확장 함수의 관점 : 일반 함수는 호출시에 객체를 Argument로 받지만 확장 함수는 호출시에 객체를 Receiver로 받습니다. 마찬가지로 일반 lambda는 호출시에 객체를 Argument로 받지만 (block : (T) -> R), Lambda with Receiver는 호출시에 객체를 Receiver로 받습니다. (block : T.() -> R)
  • 확장 함수에 대한 일반 함수 -> 일반 lambda의 관점 : 일반 함수는 특정한 로직을 재사용한 단위의 목적으로 사용된다면, 일반 lambda는 함수를 인자로 사용하기 위한 목적으로 주로 사용됩니다. (ex: filter의 경우 predicate, map의 경우 transform 로직을 인자로 사용하기 위해 lambda 사용) 마찬가지로 확장 함수는 Receiver에 대한 특정한 로직을 재사용하기 위해 사용하고 (ex : fun String.lastChar() = this.get(this.length — 1) ), lambda with receiver는 함수를 인자로 사용하기 위해 사용됩니다. (ex: val str = buildString { this.append("...") } 에서 lambda with receiver는 특정한 로직을 거친 후 String을 반환하는 buildString() 함수에 대한 인자로 사용됨)
  • 또한 확장 함수를 정의할 때 Receiver를 의미하는 this를 생략하고 멤버를 호출해도 무방했는데, 마찬가지로 lambda with receiver 역시 정의시에 this 를 생략하고 멤버를 호출해도 무방합니다.
inline fun buildString(builderAction: StringBuilder.() -> Unit): String {
val stringBuilder = StringBuilder()
stringBuilder.builderAction()
return stringBuilder.toString()
}
fun generateAlphabet():String{
return buildString {
appendln("Alphabet: ")
for (c in 'a'..'z'){
append(c)
}
}
}
  • buildString() 함수의 경우 위와 같이 lambda with receiver 1개를 인자로 받는 형태로 구현되어 있으며, 내부에서 StringBuilder 객체를 생성한 뒤에 이 객체에 대해 lambda with receiver를 builder Action으로 사용하여 조작하고, 이 객체를 반환하는 역할을 수행합니다. 이 경우 위 코드의 generateAlphabet() 함수 처럼 lambda with receiver 부분을 정의하여 원하는 형태의 조작을 수행한 후 그 String을 반환하기 위한 목적으로 사용할 수 있습니다.
  • 또한 살펴볼 점은 앞서 inline functions 파트에서 배운 것처럼 lambda를 인자로 받는 경우이므로 inline 으로 정의하는 것이 퍼포먼스 개선에 좋으며, 확장함수처럼 receiver에 대한 멤버를 this를 생략하고 사용하여 코드 간결성을 개선할 수 있다는 점입니다. (ex: this.appendln("Alphabet: ") -> appendln("Alphabet: ") , this.appendln(c) -> append(c) )
  • 즉, lambda with receiver는 일반 lambda 처럼 인자로서 함수를 사용하기 위해 사용될 수 있는데, 일반 lambda와 달리 Receiver에 대한 lambda 형태로 정의할 수 있어서 “특정한 객체에 대한 멤버를 빈번하게 사용하는 로직 (Receiver this를 생략하고 사용 가능하므로써)”을 인자로 받는 함수를 정의할 때 유용하게 쓰일 수 있습니다. 이러한 예시가 with, run, let, apply, also 함수들입니다.

2. More useful library functions

inline fun <T, R> with(receiver: T, block: T.() -> R): R { return receiver.block() }
inline fun <T, R> T.run(block: T.() -> R): R {return this.block()
}
inline fun <T, R> T.let(block: (T) -> R): R {return block(this)}
inline fun <T> T.apply(block: T.() -> Unit): T {this.block(); return this;}
inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this;}
  • with, run, let, apply, also 함수들은 1) 함수가 객체를 receiver로 받는지, argument로 받는지, 2) block이 일반 lambda로 정의되는지, lambda with receiver로 정의되는지, 3) 함수의 반환값이 객체인지, lambda인지의 3가지 항목에 있어 다르며 이에 따라 사용 목적이 달라집니다.
  • 1) 함수가 객체를 receiver로 받는지, argument로 받는지 여부 : receiver로 받을 경우 safe access (?)를 통해 null 체크가 용이하다. 즉, non-null 타입 객체를 다룰 때는 객체를 argument로 받는 방식 (ex: with)을 사용하나, nullable 타입을 처리하기 위해서는 객체를 receiver로 받는 방식 (ex: run, let, apply, also)이 유용합니다.
  • 2) block이 일반 lambda로 정의되는지, lambda with receiver로 정의되는지 : lambda로 정의 되는 경우 객체가 block(this) 형태로 구현되어 lambda로 정의하는 커스텀 로직 상에서 객체에 접근할 때 it 을 통해 접근하며 이를 생략할 수 없습니다. 한편 lambda with receiver로 정의 되는 경우 객체가 this.block() 형태로 구현되어 lambda로 정의하는 커스텀 로직 상에서 객체에 접근할 때 this 를 통해 접근하며 이를 생략할 수 있습니다. 즉, 커스텀 로직 상에서 객체가 Argument로 사용되는 경우 lambda로 정의된 것 (ex: let, also )이 유용하며, 커스텀 로직 상에서 객체가 Receiver로 사용되는 경우 lambda with receiver로 정의된 것 (ex: with, run, apply)이 유용합니다.
  • 3) 함수의 반환값이 객체인지, lambda인지 : 반환값이 객체인 함수의 경우 객체에 대해 lambda를 통해 정의된 커스텀 로직을 수행한 후에 그 객체를 다른 변수에 저장하거나, 추가적인 연산을 chain call 형태로 연달아 수행하기에 용이합니다. 한편 반환값이 lambda인 함수의 경우 lambda의 가장 마지막 expression이 반환값이 되므로 반환할 것을 직접 설정할 수 있습니다. 즉, 커스텀 로직이 수행된 객체를 다른 변수에 저장하거나 연달아 연산을 수행할 경우 객체를 반환하는 것 (ex: apply, also )가 유용하며, 그렇지 않은 경우 lambda를 반환하는 것 (ex: run, let )이 유용합니다.
  • 이에 따른 with, run, let, apply, also 함수 사용 사례는 아래와 같습니다.
data class Window(var width:Int, var height:Int, var isVisible:Boolean)
fun cleanWindow(window: Window):Unit = TODO()
fun openWindow(window: Window):Unit = TODO()
// 1. with
// Non-nullable이 보장된 객체의 경우
// 객체의 멤버에 여러번 접근하는 로직 (객체 명시적 선언없이 멤버 접근하여 간결성 개선)
// 반환값을 사용하지 않거나 원하는 임의의 반환값 정의 가능
fun initializeWindow(window:Window):Unit{
with(window){
width = 300
height = 200
isVisible = true
}
}
// 2. run
// Nullable 객체에 대해 safe access(객체가 null인 경우 아무 연산 수행하지 않음)
// 객체의 멤버에 여러번 접근하는 로직 (객체 명시적 선언없이 멤버 접근하여 간결성 개선)
// 반환값을 사용하지 않거나 원하는 임의의 반환값 정의 가능
fun initializeWindowOrNull(windowOrNull:Window?):Unit{
windowOrNull?.run{
width = 300
height = 200
isVisible = true
}
}
// 3. apply
// Nullable 객체에 대해 safe access(객체가 null인 경우 연산 X & null 반환)
// 객체의 멤버에 여러번 접근하는 로직 (객체 명시적 선언없이 멤버 접근하여 간결성 개선)
// 해당 객체를 반환 (조작한 객체를 다른 변수에 저장하거나 chain call)
fun getInitializedWindow(windowOrNull:Window?):Window?{
return windowOrNull?.apply{
width = 300
height = 200
isVisible = true
}
}
// 4. let
// Nullable 객체에 대해 safe access(객체가 null인 경우 아무 연산 수행하지 않음)
// 객체를 인자로 여러번 사용하는 로직
// 반환값을 사용하지 않거나 원하는 임의의 반환값 정의 가능
fun useWindow(windowOrNull: Window?):Unit{
windowOrNull?.let{
cleanWindow(it)
openWindow(it)
}
}
// 5. also
// Nullable 객체에 대해 safe access(객체가 null인 경우 연산 X & null 반환)
// 객체를 인자로 여러번 사용하는 로직
// 해당 객체를 반환 (조작한 객체를 다른 변수에 저장하거나 chain call)
fun getUsedWindow(windowOrNull: Window?):Window?{
return windowOrNull?.also{
cleanWindow(it)
openWindow(it)
}
}

3. Working with auxiliary functions 실습 코드 작성시 고려할 점

  • lambda with receiver로 정의 되어 객체를 this 로 받는 경우 생략이 가능하지만, 일반 lambda로 정의 되어 객체를 it 으로 받는 경우 생략이 불가능합니다.
  • 객체를 함수 인자로 사용하는 경우 명시가 꼭 필요하지만, 객체 멤버에 접근하는 경우 생략하는 것이 코드 간결성을 개선해줍니다.
  • 여러개의 auxiliary functions을 중첩하여 사용하는 것은 안티 패턴이며 한개의 함수로 대체할 수 있습니다.

4. Member extensions 실습 코드 작성시 고려할 점

  • 클래스 내부에 멤버 함수 형태로 다른 클래스 타입에 대한 확장함수를 정의한 경우, 해당 확장함수는 다음 3가지 경우에만 접근 가능한 가시성이 제어된 확장함수로 사용할 수 있습니다. 1) 클래스 내부 다른 멤버가 사용할 수 있는 확장함수, 2) 클래스에 대한 다른 확장함수 정의시에 사용할 수 있는 확장함수, 3) with 함수 등과 함께 사용시 접근 가능합니다.

--

--