Kotlin let, also, run, apply, with 분석

Jungwook Park
kjcoop
Published in
8 min readMay 23, 2018

Higher-order functions 를 읽으면 도움이 됩니다.

Standard.kt 에는 유용한 함수들이 있는데 그 중 자주 사용하는 함수는 let, also, run, apply, with 등을 들 수 있다.

contract block 은 동작에 영향이 없으므로, 이외의 부분을 살펴보면

  • let
public inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}

함수 signature 를 보면 <T, R> T.let(block: (T) -> R) : R 인데 이 의미는 다음과 같이 생각할 수 있다.

<T, R> 은 generics type parameter 이며, 어떤 type이라도 올 수 있다.

T.let(block : (T) -> R) 은 Type T 에 대해 let extension 함수를 정의하며 인자로 (T) -> R lambda 식을 받고 이름을 block 으로 정의한다.

: RR 은 최종 반환 값이다.

따라서 let 은 (T) -> R 형태의 lambda 식을 인자로 받고 lambda 식을 수행하여 결과를 반환한다.

인자로 사용하는 lambda 식이 (T) -> R 이기 때문에 block 에서 T 를 인자로서 접근할 수 있다. block(this) 에서 this 는 let 을 호출하는 T 를 의미한다.

예를 들어

val stringfied = 3.let ({ input ->
(input * 2).toString()
}) // "6"

TInt 이고 block 에 해당하는 (T) -> R{input -> (input * 2).toString()} 이며 해당 block 을 수행하여 “6”을 반환하며 반환하는 Type은 String 이다.

let 의 인자가 (T) -> R 이기 때문에 lambda 안에서 T 에 해당하는 input 을 사용할 수 있다.

kotlin 은 single parameter에 대해 it keyword 를 지원하며

val stringfied = 3.let ({
(it * 2).toString()
}) // "6"

맨 마지막 인자가 lambda 인 경우 괄호 밖으로 뺄 수 있기 때문에

val stringfied = 3.let {
(it * 2).toString()
} // "6"

모두 동일하다.

  • also
public inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}

T.also(block: (T) -> Unit) : T 이므로

Type T 에 대해 lambda 식 (T) -> Unitblock 을 수행하고 다시 T 를 반환한다. let은 결과를 반환한다면 also는 T를 반환하는데 차이가 있다. 마찬가지로 block(T) -> Unit 이므로 lambda 식 안에서 T 를 사용할 수 있다.

예를 들어 아래와 같은 코드에서

"/sdcard/sample.file"
.let { File(it) }
.also { it.mkdir() }
.also { println(it.absolutePath) }

.let { File(it) }itString 이고 첫번째 alsoitFile 이며 두번째 alsoitalsoT 의 type 을 그대로 반환하기 때문에 File 이다.

  • run #1

run 은 두가지 정의가 있고, 순서대로 살펴보면

public inline fun <R> run(block: () -> R): R {
return block()
}

run(block: () -> R): R 이기 때문에 lambda 식 block() 을 수행하고 그 값을 반환한다. let 과는 다르게 lambda 식의 인자로 () 가 쓰였으며 따라서 T 를 lambda 식 안에서 사용할 수 없다.

사용 예는 아래와 같다.

val block = {
"Hello"
}
run(block).also { println(it) }

block 은 인자가 없고 String 을 반환하는 lambda 식이고 위 식을 수행하면 also 에서 문자열을 출력한다.

run { "Hello" }.also { println(it) }

block 을 inline 하고 마지막 인자가 lambda 이므로 괄호를 제거하면 위와 같다.

  • run #2
public inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}

run #1 과 조금 차이가 있는데 block: T.() -> R 에서 T.()Function literals with receiver 이며 T 의 멤버 함수를 의미한다. 함수의 이름을 미리 알 수 있다면 T.memberFunction() 등의 형태로 사용하겠지만, 멤버 함수의 이름을 미리 알 수 없기 때문에 T.() 형태로 표현한다고 생각하면 편하다.

즉, block: T.() -> R 은 lambda 식 안에서 T 의 멤버 함수를 T. 없이 사용할 수 있다는 의미이다.

따라서 run #2 의

T.run(block: T.() -> R) : R 은 lambda 식 안에서 T 의 멤버 함수를 T. 를 생략하여 쓸 수 있으며 lambda 식의 결과를 R type으로 반환한다는 의미이다.

예를 들어 아래와 같은 코드에서

class Outer(var inner: Inner? = null)

class Inner(var value1: Int? = null, var value2: Int? = null) {
fun method1() = Unit
fun method2() = Unit
}
fun caller() {
val outer: Outer? = null
outer?.inner?.run {
value1 = 1
value2 = 2
method1()
method2()
"Hello" // return
}.let { println(it) }
}

위 예는 outernull 이므로 수행되지 않지만, Outer.Inner 의 여러 멤버들을 한 번의 null 검사로 쉽게 이용할 수 있다는 것을 알 수 있다. 마지막의 "Hello”가 lambda의 반환 값이며 block: () -> R 이므로 block 에 인자가 없어 it 을 사용할 수 없다. 또 마지막의 “Hello” 가 없는 경우 method2() 의 반환 type인 Unit 이 반환된다.

  • apply
public inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}

T.apply(block: T.() -> Unit) : Tblock 안에서 T. 없이 멤버를 사용할 수 있으며 block 을 수행한 후 T 를 반환한다는 의미이다.

block 을 수행한 후 this 를 반환하며 block 안에서 T 의 멤버 함수를 호출할 수 있기 때문에 생성과 동시에 값을 설정하는데 쓰인다.

예로, 아래와 같이 사용할 수 있다.

val intent = Intent()
.apply { putExtra("Sample", 1) }
.apply { action = "io.github.genju83.SAMPLE_ACTION" }
  • with
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}

with 는 receiver T 를 받고 T. 없이 멤버를 사용할 수 있는 lambda 식 block 을 인자로 받는다. apply 와 유사하지만 T 없이 with 로 시작한다. receiverT 이고 blockT.() 이기 때문에 receiver.block() 으로 사용하는게 포인트.

즉, T 를 receiver 로 T 의 멤버인 lambda 식 block 을 수행하고 block 의 결과를 반환한다.

apply 는 T 를 반환하는데 반해 with 는 block 결과를 반환하는 차이가 있다.

아래와 같이 사용할 수 있다.

val intent = with(Intent()) {
putExtra("Sample", 1)
action = "io.github.genju83.SAMPLE_ACTION"
}

표로 정리해보면 다음과 같다.

T 의 확장 함수로 사용가능

어느 곳에서나 사용 가능

  • 연습

Int 의 확장 함수로 짝수, 홀수 각각 실행 lambda block 을 인자로 받고 짝수일 경우 그에 해당하는 lambda block, 홀수일 경우 그에 해당하는 lambda block 을 수행하고 각각의 반환에 따라 다른 동작을 하며 편의상 it 을 쓰는 함수를 만들어야 하는 경우 아래와 같은 함수 signature 를 정의할 수 있다.

fun <R> Int.evenOrOdd(evenBlock:(Int) -> R, oddBlock:(Int) -> R) : R

간단히 홀수인지 짝수인지 판단하고 해당 block(this) 를 수행하면 되기 때문에 아래와 같이 구현할 수 있다.

fun <R> Int.evenOrOdd(evenBlock:(Int) -> R, oddBlock:(Int) -> R): R {
return when {
this % 2 == 0 -> evenBlock(this)
else -> oddBlock(this)
}
}

위 함수는 아래와 같이 사용 가능하다.

4.evenOrOdd(
{ "Even block $it" },
{ "Odd block $it" }
).let { println(it) } // Even block 4

3.evenOrOdd(
{ "Even block $it" },
{ "Odd block $it" }
).let { println(it) } // Odd block 3

--

--