번역-Kotlin pearls 1) Scope Functions

dbdb
3 min read

원문 : [Kotlin pearls 1]Scope Function]


우리한테 Kotlin scope 함수 관련 글들이 필요할까요?

사실 전 그렇다고 생각합니다. scope 함수에 대한 글들이 정말 많지만, 이 함수들을 활용해서 리팩토링을 하려고 할 때, 어떤 scope 함수가 적합한지 고르는 법에 대한 내용은 찾지 못 했거든요.

아래 그림과 같이 깔끔하게 정리해봤습니다:

1_i-Uo4RQDd6tNi2Mj8WSUoA.jpeg

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

Pass it은 object를 다른 methods/functions로 넘긴다는 것으로, 해당 object에 대한 참조라 it을 사용합니다.

Result는 block에서 나온 결과값을 반환해줘야 한다는 뜻입니다.

Side-effects는 결과를 기대하지 않고, Unit(즉, void)를 반환, 그리고 이를 Fluent Interface style로 결합할 것입니다.

2 Questions

그래서 object(대상 object)가 수회 사용될 때, 코드를 단순화 시키는 방법이 궁금하다면, 다음과 같은 질문을 스스로에게 해보면 됩니다:

1. 해당 object에 method를 호출해야 하는가? 혹은 다른 method/function의 arguments로 넘겨야 하는가?

2. 결과값이 필요한가? 혹은 상태(state)만 변경해도 되는가?


How to choose a correct Scope Function

apply

한 object에 여러 번 method 호출해야 하고, 반환값이 필요없을 때 유용합니다(setter 같이).

아래 코드에서 ?를 사용했는데, 나머지 확장 함수들과 마찬가지로, 대상 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일 때에도 object를 nullable인 상태로 block을 호출합니다.

저는 보통 특정 object를 갖고 호출하는 method들의 양이 너무 많아서 코드가 길어지는 경우에만 사용합니다. 특히 변수명이 길 때요. 예를 들어:

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

let

let은 가장 많이 사용되는 함수일 것 같은데(적어도 저는 그렇습니다), 특히 ?와 함께 사용될 때 유용합니다. value가 null이 아닌 경우 결과를 얻을 수 있죠. 코드 상 null 체크하는 코드(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

반환해야 하는 값이 있는데, 그 전에 “다른 작업에도” 필요한 경우 유용합니다: 예를 들어:

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

Scope Function 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 }

코틀린에 익숙하지 않은 사람들에게 scope 함수를 사용한 코드를 보여주면, “그걸 써도 별 이점도 없고, 그냥 복잡해”라는 반론을 흔히 듣습니다.

scope 함수 사용 시, 작은 이점은 코드 몇 줄 절약하는 것이고, 더 큰 이점은 임시 변수(temporary variables)의 사용을 피하게 해준다는 것입니다.

임시 변수들은 보통 FPFunctional Programming 스타일에서 별로 좋지 않은 방식으로 여겨지며, 변수명을 헷갈리거나 코드 복사-붙여넣기 시, 사소한 버그의 원인이 됩니다.(물론 그런 실수를 안 하겠지만요!)

또한 FP 방식에서 (좀 더 정확한 의도 전달한다는 의미에서) 함수 사용 시, single expression declaration form(이처럼 생긴.. fun f = ...)을 사용하는 것이 더 좋은데, scope 함수가 이를 도와줍니다.

0
Subscribe to my newsletter

Read articles from db directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

db
db