프로그래밍/Kotlin

Kotlin Scope functions의 쓰임새 (let, run, with, apply, also, takeIf, takeUnless) with skydove's pokedex

Lou Park 2022. 7. 11. 02:35

코틀린에서는 특정한 객체에대해 이름없이 접근할 수 있는 스코프를 형성하는 함수가 존재하는데, 이것이 바로 스코프 함수(Scope function)이다. 5가지로 이루어져있고, 목록은 아래와 같다.

  • let
  • run
  • with
  • apply
  • also

이들을 코틀린 공식 문서와함께, 이 안드로이드 바닥에서 유명한 오픈소스 프로젝트인 Skydove님의 Pokedex 코드와 함께 살펴보려고한다.

 

기본적으로는 이들 모두 하는일은 동일하다. 어떤 객체에 대한 코드블록을 실행시키는거다. 

차이점은 리턴값, 그리고 블록안에서 해당 객체가 어떻게 참조되는지뿐이다. 

함수명 객체참조 리턴값 Extension함수 인지?
let it Lambda result O
run this Lambda result O
run Lambda result X
with this Lambda result X
apply this Context object O
also it Context object O

이들을 활용하는 방법을 간략하게 설명하자면 다음과 같다.

let

Nullable 객체에서 Non-null임을 보장받고 싶을때 이렇게 사용한다. 하지만 중첩해서 사용할경우 it이 shadowing되기때문에 적당히 사용하거나 명시적으로 변수명을 정해주는 것이 좋다.

str?.let {
	println(it)
}

let은 또한 호출체인의 결과에서 추가적으로 함수호출이 필요할때도 사용할 수 있다.

class PokemonAdapter {
	...
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PokemonViewHolder =
        parent.binding<ItemPokemonBinding>(R.layout.item_pokemon).let(::PokemonViewHolder)
    ...
}

 

 

run

객체 설정(configuration), 결과를 계산할때 사용한다. run은 with와 같은일을 하지만 let처럼 람다를 반환한다. 그럼 run은 Nullable을 대상으로 Non-null을 보장받기 위해, with는 태상부터~ Non-nullable을 대상으로 사용하면 좋다.

val result = service.run {
    port = 8080
    query(prepareRequest() + " to port $port")
}

Pokedex 소스코드에서도 설정을 하는데에 쓰인 것을 볼 수 있다.

@JvmStatic
  @BindingAdapter("paginationPokemonList")
  fun paginationPokemonList(view: RecyclerView, viewModel: MainViewModel) {
    RecyclerViewPaginator(
      recyclerView = view,
      isLoading = { viewModel.isLoading },
      loadMore = { viewModel.fetchNextPokemonList() },
      onLast = { false }
    ).run {
      threshold = 8
    }
  }

 

 

with

함수 호출을 그룹화 할때. 이 객체를 가지고, 다음 명령을 수행해라~("with this object, do the following")라는 말이 어울리는 곳에 쓰면된다.

val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}

 

 

apply

객체 설정(configuration)에 사용하는데, apply의 리턴값은 오브젝트이기때문에 곧바로 그 결과가 필요한 부분에 사용하면된다. 

 

아래 코드를보면 리본뷰를 만들고, maxLines와 gravity를 설정한 후 바로 addRibbon에 넘겨주는 모습을 볼 수 있다.

addRibbon(ribbonView(context) {
  setText(type.type.name)
  setTextColor(Color.WHITE)
  setPaddingLeft(84f)
  setPaddingRight(84f)
  setPaddingTop(2f)
  setPaddingBottom(10f)
  setTextSize(16f)
  setRibbonRadius(120f)
  setTextStyle(Typeface.BOLD)
  setRibbonBackgroundColorResource(
    PokemonTypeUtils.getTypeColor(type.type.name)
  )
}.apply {
  maxLines = 1
  gravity = Gravity.CENTER
})

 

also

현재 객체가 아규먼트로 필요한 추가적인 일을 할때, 부수적인일들이 있을때 이를 사용하면된다.

var formattedPrice = ""
val price = 10.plus(5).also {
    formattedPrice = "Price is ${it * 2}"
}

또한 let으로 Nullcheck를 했을때 null일 경우의 코드를 쓰는 블록으로도 사용할 수 있다. 

var value: Int? = 0

value?.let {
	do(it)
} ?: also {
	doWhenNull()
}

 

그외 takeIf, takeUnless

takeIf는 predicate가 있을 경우 predicate와 매치되는 객체를 반환하고, 매치되지 않을경우 Null을 반환한다. 간단히말하자면 takeIf는 단일 객체를 위한 필터(filter) 함수다. takeUnless는 predicate와 매치되지 않으면 객체를 반환하고 매치되면 null을 반환하는 것으로, takeIf의 반대다. "Unless"라는 영어가 나에겐 너무 와닿지 않지만...쓰다보면 되겠지...

val number = Random.nextInt(100)

val evenOrNull = number.takeIf { it % 2 == 0 }
val oddOrNull = number.takeUnless { it % 2 == 0 }
println("even: $evenOrNull, odd: $oddOrNull")

pokedex에서는 RecyclerView 어댑터 포지션을 가져오되, NO_POSITION이라면 클릭 수행을 막기 위해 사용했다.

val position = bindingAdapterPosition.takeIf { it != NO_POSITION }
          ?: return@setOnClickListener

takeIf/Unless는 저렇게 엘비스 오퍼레이터나, 기타 다른 스코프 함수들과 함께 사용하면 더 유용하게 사용할 수 있다. 아래 두 함수중 위의 함수는 takeIf와 Scope function을 같이 사용했고, 아래는 그렇지 않은 경우다. 

fun displaySubstringPosition(input: String, sub: String) {
    input.indexOf(sub).takeIf { it >= 0 }?.let {
        println("The substring $sub is found in $input.")
        println("Its start position is $it.")
    }
}

fun displaySubstringPosition(input: String, sub: String) {
    val index = input.indexOf(sub)
    if (index >= 0) {
        println("The substring $sub is found in $input.")
        println("Its start position is $index.")
    }
}

 

참고자료

https://kotlinlang.org/docs/scope-functions.html#takeif-and-takeunless

https://proandroiddev.com/dont-abuse-kotlin-s-scope-functions-6a7480fc3ce9

https://github.com/skydoves/Pokedex