(번역)[Kotlin pearls 1]Scope Functions

(번역)[Kotlin pearls 1]Scope Functions

다이어그램으로 스코프 함수들에 대한 이해도를 높이자

db's photo
db
·Aug 13, 2022·

4 min read

Table of contents

  • Scope Functions
  • 2 Questions
  • signatures

원문 : [Kotlin pearls 1]Scope Function]
원문과 내용은 동일하고, 문단 스타일에만 차이가 있습니다

Scope Functions

우리가 코틀린의 스코프 함수들에 대한 글을 계속 찾아 다닐 필요가 정말로 있을까요?

아마 그렇다고 생각하시는 분들이 많을 것 같습니다. 하지만 스코프 함수들에 대한 설명을 적어놓은 포스트들을 많지만, 정작 저는 상황에 맞는 스코프 함수를 고르는 방법이나 스코프 함수들 중 하나를 넣어서 리팩토링을 하기 위해 고려해야하는 점에 대한 글을 찾지 못했습니다.

그래서 이를 해소하기 위해 다음과 같이 다이어그램으로 정리해 보았습니다.

1_i-Uo4RQDd6tNi2Mj8WSUoA.jpeg

Use this는 우리가 single object에 method를 (주로)호출한다는 의미입니다. 즉, object에 대한 참조는 this 인 것이죠.

Pass it은 우리가 object를 다른 methods/funtions로 넘겨 그 object에 대한 참조로 it을 사용합니다.

Result는 block에서 나온 결과값이 있을 것이고, 우리는 그것을 반환해줘야 할 필요가 있다는 뜻입니다.

Side-effects는 결과가 필요없지만, Unit(a.k.a. void)를 반환하는 methods를 호출하고 싶다는 의미입니다. 그리고 Fluent Interface style로 결합시킬 수 있습니다.

2 Questions

object(the target object)가 여러번 사용된 곳에서 코드를 단순화 시키는 방법이 궁금하다면, 다음과 같이 2가지 질문을 스스로에게 던져야 합니다.

1. 이 object에 methods를 호출해야할까? 아니면, 다른 methods/functions에 arguments로 넘겨야할까?

2. 마지막에 결과값이 필요한가? 아니면, 상태(state)만 바꿔도 되는가?


apply

만약 setter처럼 반환을 신경쓰지 않고, 여러개의 methods를 object에 호출해야 한다면, apply가 유용합니다. 다른 세 확장함수들과 같이 target object가 null인 경우 block 코드가 호출되지 않고, block 내부의 object는 항상 non-nullable type입니다. 예를 들어,

fun updateItem(itemId: String, newName: String, newPrice: Double) = itemsMap.get(itemId)?.apply { 
    enabled = true
    desc = newName
    price = newPrice
}

with

apply와 거의 비슷한데, object가 nullable type일 때에도 넘겨서 block을 호출한다는 차이점이 있습니다. 그래서 저는 오로지 특정 object에 호출하는 몇 method들이 긴 block 코드와 있을 때만 사용합니다. 예를 들어,

// ... long method
val total = with(ridicurioslyLongNameOfFrameworkSingleton){ 
   setSomething(a)
   setSomethingElse(b)
// do other things...
   getTotal()
}
// ...

let

let은 아마 가장 많이 사용되는 함수일 것이라고 생각합니다(적어도 저는 그렇습니다). 특히 value가 null이 아닐 때, 그 value를 얻기 위해서 question mark와 함께 쓸 때, 유용합니다. 즉, if (obj == null) {...}와 같이 생긴 코드를 지울 때, 고려해 볼 수 있는 함수입니다. (혹은 다른 스코프 함수를 써도 됩니다.) 예를 들어,

fun fullName(firstName: String, middleName: String?, familyName: String) = middleName?.let{ 
   "$firstName ${it.first()}. $familyName"
} ?: "$firstName $familyName"

run

run은 마지막에 object에 몇 method들을 호출하고 다른 무언가를 반환하고 싶을 때 유용합니다. 예를 들어,

fun cartTotal(cart: Cart, items: List<CartItem>) = cart.run{ 
   items.foreach{addItem(it)}
   calcTotal()
}

also

also는 value를 반환해야 하고, 반환하기 전에 "겸사겸사"(번역자: 원문글에서 also라고 적혀 있는데, "또한"보다 자연스러운 것 같아 의역했습니다) value를 사용하여 다른 무언가를 해야할 때 유용합니다. 예를 들어,

fun createUser(userName: Stirng): Int =
    myAtomicInt.getAndIncrement().also { users.put(it, userName) }

signatures

참고로 시그니처는 다음과 같습니다.

public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
public inline fun <T, R> T.run(block: T.() -> R): R = block()
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }

제가 스코프 함수들을 사용한 코드를 코틀린에 이 함수들을 사용하지 않는 사람들에게 보여주면 흔히 "저걸 굳이 왜 사용해야 하는지 모르겠어요, 오히려 엉망으로 만들어요"라며 반박합니다.

스코프 함수를 사용했을 때, 좀 작은 이점은 코드 줄 수를 줄일 수 있다는 것이고, 더 큰 이점은 잠깐 사용하고 버리는 변수들(임시 변수)의 사용을 줄일 수 있다는 것입니다.

임시 변수들은 함수 지향 프로그래밍에서 별로 좋은 생각이 아니며, 무조건 그렇다는 것은 아니지만(원문글:(물론 안 그러겠지만!)) , 그것들을 혼동하거나, 코드 일부분을 복사-붙여넣기 했을 때, 작은 버그들의 원인이 됩니다.

또, 함수형 프로그래밍 방식에서는 (좀 더 정확한 의도를 전달한다는 의미에서) 함수를 쓸 때, single expression declaration form을 사용하는 것이 더 좋다. (이처럼 생긴.. fun f = ...) 그리고 스코프 함수는 이렇게 사용하기 용이하게 해줍니다.