KotlinでScopeクラスを作ってその中だけで呼べる拡張関数を定義する

今日ご紹介させていただくのは、Kotlinの拡張関数を特定の条件下のみで使える様にする。
といった実装の話です。

通常の拡張関数ではできないことが、特定の条件下ではできるようになることが今回のポイントになります。
これによって、ちょっと面倒な処理で、同じコードの繰り返しが多い場合に非常に楽になると思います。
ただし便利な反面設計が重要になります。より発展的なことをしようとすると隠蔽された実装のなかで、特に状態管理やAPI設計が非常に重要になるので、諸刃の剣になると言うことを十分に理解した上で適所のみに使うことが重要です。


ではさっそく段階を踏んで紹介していきましょう。


## レベル１

まず最初は特定のクラス内でのみ使える拡張関数からです。

```kotlin
class SquareScope {
  fun Int.square(): Int = this * this
}
```
上記のように`SquareScope`クラスの中に拡張関数を定義した場合、SquareScopeのインスタンスを`this`で参照できる場合のみ拡張関数の`square`を利用することができます。

```kotlin
class SquareScope {
    // SquareScopeがthisの時だけ使える拡張関数
    fun Int.square(): Int = this * this

    fun square_test() {
        println(22.square())
    }
}
fun square_test2() {
    // ちょっと応用、withの中でも使える
    with(SquareScope()) {
        println(22.square())
    }
}
fun square_test3() {
    println(22.square()) // コンパイルエラー
}
```

## レベル２

Kotlinが好きな人であれば、`square_test2()`でwithを利用している点をみれば、これがどう言う面白さを持っているのか見えてきたかもしれません。
次は特定のラムダ関数引数でのみ使える拡張関数の定義のしかたです。とくにレベル２にするほどのことではないかもしれませんが、レベル１よりは発展的な内容であるので、段階をあえて踏んでいます。

```kotlin
class SquareScope {
    // SquareScopeがthisの時だけ使える拡張関数
    fun Int.square(): Int = this * this
}

fun squareScope(block: SquareScope.()-> Unit) = SquareScope().block()
```

さて新たに`squareScope()`という関数を用意しました。
これを用意することで、先ほどの`square_test2()`で行ったことが次のように実装できます。

```kotlin
fun square_test2_1() {
    // ラムダを引数に使った関数で応用
    squareScope {
      println(22.square())
    }
}
```

先ほどよりスッキリ書ける様になりました。


## レベル３

さていよいよここから紹介したい内容になっていくのですが、これまでは通常の拡張関数が特定の条件下でのみ使える様になると言ったものではありましたが、通常の拡張関数にも置き換え可能な点がここまでの話での限界でした。
次は冒頭でも言った様に、通常の拡張関数では置き換えができないと言う点がポイントになります。

```kotlin
class DateFormatScope(private val format: String) {
    fun Date.simpleFormat(): String {
        return SimpleDateFormat(format, Locale.getDefault()).format(this)
    }
}
fun withFormat(format: String, block: DateFormatScope.() -> Unit) =
    DateFormatScope(format).block()
```

さて今回は`SquareScope`クラスではなく`DateFormatScope`とそれを使う`withFormat()`という関数を定義しました。
これは以下のようにつかうことができます。

```kotlin
fun withFormat_test() {
    withFormat("yyyy.MM.dd") {
        println(Date().simpleFormat()) // ex. 2020.11.04
    }
    withFormat("yyyy.MM.dd-HH:mm") {
        println(Date().simpleFormat()) // ex. 2020.11.04-11:36
    }
}
```
このように`Date.simpleFormat（）`という拡張関数の振る舞いが提供するScopeクラスのインスタンスが変わることで変更されたことになり、通常の拡張関数では考えられない動作をしていることになるとおもいます。

これは特に使っていて便利だから紹介したと言うものではないですが、たとえばたくさん日付の処理を書かなければいけなくて、フォーマットも複数扱わなければいけない時に便利になるかもしれません。
また、たとえば`offset`を考慮した座標計算をしなくてはいけないときに、毎回offsetを差し引きする実装だと読むのが辛い（プログラミング作業のほどんどはコードを読むことだと言う話もあります）というパターンでもつかえるのではないでしょうか？`ex. withOffset(x=5, y=20){}`





## レベル４

さらに応用です。
これも、さほど先ほどとは変わらないのですが、一度にやってしまうと混乱してしまう可能性があるので段階を踏んでいます。
せっかくなので実践的な内容にしていきたいとおもいます。Androidの`ComponentActivity`での利用を想定した例です。

```kotlin
val Activity.contentView: View get() = findViewById<ViewGroup>(android.R.id.content)[0]

fun <T : ViewDataBinding> ComponentActivity.requireBinding(): T =
    DataBindingUtil.bind(contentView) ?: error("please setup data binding")

fun  <T : ViewDataBinding> ComponentActivity.bindingScope(block: BindingScope<T>.()->Unit): T = 
    requireBinding<T>().apply { 
        BindingScope(this, this@bindingScope).block() 
    }

class BindingScope<T : ViewDataBinding>(val binding: T, private val activity: ComponentActivity) {
    init {
        binding.lifecycleOwner = activity
    }
    fun Int.spanGridLayoutManager(): GridLayoutManager = GridLayoutManager(activity, this)
    fun RecyclerView.decorationSpace(value: Int) {
        addItemDecoration(SpacesItemDecoration(value))
    }
}
```
上記のようにDataBindingに関する拡張関数を用意してみました。
どこら変がレベル４なのかというと、ジェネリック関数をつかっているところがメインではあるのですが、追加するならより実践的な内容に踏み込んでいるというところもポイントではあるとおもいます。

今回ご紹介したコードが実際に使うことがあるかどうかは別にしろ、特定のボイラープレートを緩和することも可能です。
以下が実際に使ってみたコードです。
コンストラクタにレイアウトを渡している上でのDataBindingのセットアップをより簡易的に書ける様にしているのがポイントです。
今回の例については、Androidの開発では毎回煩わしい部分ではあるので、複数のテクニックを積み上げてのボイラープレートの削減と利便性の向上がここでは見込めると思います。

```kotlin
class MainActivity : AppCompatActivity(R.layout.activity_main) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mainBinding = bindingScope<ActivityMainBinding> {
            binding.list.apply {
                layoutManager = 3.spanGridLayoutManager()
                decorationSpace(4)
            }
        }
    }
}
```


Activityのリークが怖いと言われる可能性がありますが、`bindingScope()`の中だけで`BindingScope`のインスタンスが消費されていることと、用途的には`binding.apply{}`を使っているのとほとんど変わらない事に注意してください。
またこれは単にこんな感じのことができるという可能性を示すもので、これが良いのでご紹介と言う話ではない事にも注意していただければと思います。

## おわりに

さて、いかがだったでしょうか？僕はこう言うことができると知ったのはJetpack Composeに定義されている`DrawScope`などのいくつかのスコープクラスからでした。なのでJetpack Composeで色々やられている方はすでに知っている内容だったかもしれません。
しかし今回初めて知った方は`Kotlin`という言語がどれだけボイラープレート省略にすぐれている言語であるかをある程度実感していただけたのではないかと思いますし、そうであれば本当に嬉しいことです。

ではまた何か、便利になる様なものがあれば紹介させていただきたいと思います。

enjoy!