KotlinでTCAライクな実装

# はじめに
Swift+TCAで作成したアプリをKotlinに移植するため、GPTに作ってもらいました。基本的なところはできたつもりですが、まだ抜けがあるかもしれません。また私はKotlin初心者なので悪しからず。サンプルはこちらです。

https://github.com/kmatsushita1012/KotlinTCA

# 使用例
ホーム画面（親）とカウンター画面（子）の構成の基本的なアプリ構成にしました。UIには`Jetpack`を使ってます。

## MainActivity
```MainActivity.kt
package com.example.kotlin_tca

import HomeView
import com.example.kotlin_tca.core.tca.*
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import com.example.kotlin_tca.core.theme.AppTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val store = Store<HomeFeature.State, HomeFeature.Action>(
            initialState = HomeFeature.State(),
            reducer = HomeFeature,
        )
        setContent {
            AppTheme {
                Surface(
                    color = MaterialTheme.colorScheme.background
                ) {
                    HomeView(store = store)
                }
            }
        }
    }
}
```

## ホーム画面
```Home.kt
import com.example.kotlin_tca.core.tca.*
import com.example.kotlin_tca.counter.CounterFeature
import com.example.kotlin_tca.counter.CounterView
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.*
import androidx.compose.material3.*

object HomeFeature : ReducerOf<HomeFeature.State, HomeFeature.Action> {
    data class State(
        val title: String = "Home",
        val counter: CounterFeature.State? = null
    )

    sealed class Action {
        data class SetTitle(val title: String) : Action()
        class CounterButtonTapped() : Action()
        data class Counter(val action: CounterFeature.Action) : Action()
    }
    //WritableKeyPathの代わり
    val counterLens = Lens<HomeFeature.State, CounterFeature.State?>(
        get = { it.counter },
        set = { parent, child -> parent.copy(counter = child) }
    )
    val counterPrism = Prism<HomeFeature.Action, CounterFeature.Action>(
        extract = { (it as? Action.Counter)?.action },
        embed = { Action.Counter(it) }
    )

    override fun body(): ReducerOf<State, Action> =
        Reduce<State, Action>{ state, action ->
            when (action) {
                is Action.SetTitle -> state.copy(title = action.title) to Effect.none()
                is Action.CounterButtonTapped->{
                    state.copy(counter = CounterFeature.State(1)) to Effect.none()
                }
                is Action.Counter -> {
                    when (val innerAction = action.action) {
                        is CounterFeature.Action.DismissTapped -> {
                            state.copy(counter = null) to Effect.none()
                        }
                        else -> state to Effect.none()
                    }
                }
            }
        } +
        OptionalScope(
            stateLens = counterLens,
            actionPrism = counterPrism,
            reducer = CounterFeature
        )
}
@Composable
fun HomeView(store: StoreOf<HomeFeature.State, HomeFeature.Action>) {
    val state by store.state.collectAsState()
    
    Navigation(
        store = store,
        lens = HomeFeature.counterLens,
        prism = HomeFeature.counterPrism,
        child = { store ->
            CounterView(store)
        }
    ) {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(state.title)
            Button(onClick = {
                store.send(HomeFeature.Action.CounterButtonTapped())
            }) {
                Text("Go to Counter")
            }
            Button(onClick = {
                store.send(HomeFeature.Action.SetTitle(state.title))
            }) {
                Text("Title ")
            }
        }
    }
}
```
WritableKeyPath(`\.counter`)が実装できなかったので`setter`,`getter`のペアを自作する必要があります。`Navigation`でtree-based-navigation（状態のnull/nonnullで遷移）を実現してます。遷移のアニメーションとかはなくただ表示を切り替えてるだけなので今後要改善。

## Counter
```Counter.kt
package com.example.kotlin_tca.counter

import com.example.kotlin_tca.core.tca.*
import kotlinx.coroutines.delay
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.*
import androidx.compose.material3.*

object CounterFeature : ReducerOf<CounterFeature.State, CounterFeature.Action> {

    data class State(val count: Int = 0)

    sealed class Action {
        object IncrementTapped : Action()
        object DecrementTapped : Action()
        object DismissTapped : Action()
    }

    override fun body(): ReducerOf<State, Action> =
    Reduce { state, action ->
        when (action) {
            is Action.IncrementTapped -> state.copy(count = state.count + 1) to Effect.none()
            is Action.DecrementTapped -> state.copy(count = state.count - 1) to Effect.none()
            is Action.DismissTapped -> state to Effect.none()
        }
    }
}

@Composable
fun CounterView(store: StoreOf<CounterFeature.State, CounterFeature.Action>) {
    val state by store.state.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Count: ${state.count}")
        Row {
            Button(onClick = { store.send(CounterFeature.Action.DecrementTapped) }) {
                Text("-")
            }
            Button(onClick = { store.send(CounterFeature.Action.IncrementTapped) }) {
                Text("+")
            }
        }
        Button(onClick = { store.send(CounterFeature.Action.DismissTapped) }) {
            Text("Dismiss")
        }
    }
}
```

# ライブラリ
正式なライブラリにはしてないのでコピペして使ってください。
```TCA.kt
interface ReducerOf<State, Action> {
    fun body(): ReducerOf<State, Action>

    fun reduce(state: State, action: Action): Pair<State, Effect<Action>> {
        return body().reduce(state, action)
    }
}


fun <S, A> Reduce(block: (S, A) -> Pair<S, Effect<A>>): ReducerOf<S, A> {
    return object: ReducerOf<S, A> {
        override fun body() = this
        override fun reduce(state: S, action: A) = block(state, action)
    }
}

fun <ParentState, ParentAction, ChildState, ChildAction> Scope(
    stateLens: Lens<ParentState, ChildState>,
    actionPrism: Prism<ParentAction, ChildAction>,
    reducer: ReducerOf<ChildState, ChildAction>
): ReducerOf<ParentState, ParentAction> {
    return object : ReducerOf<ParentState, ParentAction> {
        override fun body() = this

        override fun reduce(parentState: ParentState, parentAction: ParentAction): Pair<ParentState, Effect<ParentAction>> {
            val childAction = actionPrism.extract(parentAction) ?: return parentState to Effect.none()
            val childState = stateLens.get(parentState)

            val (newChildState, childEffect) = reducer.reduce(childState, childAction)
            val newParentState = stateLens.set(parentState, newChildState)
            val newParentEffect = childEffect.map { actionPrism.embed(it) }

            return newParentState to newParentEffect
        }
    }
}

fun <ParentState, ParentAction, ChildState, ChildAction> OptionalScope(
    stateLens: Lens<ParentState, ChildState?>,
    actionPrism: Prism<ParentAction, ChildAction>,
    reducer: ReducerOf<ChildState, ChildAction>
): ReducerOf<ParentState, ParentAction> {
    return object : ReducerOf<ParentState, ParentAction> {
        override fun body() = this

        override fun reduce(parentState: ParentState, parentAction: ParentAction): Pair<ParentState, Effect<ParentAction>> {
            val childAction = actionPrism.extract(parentAction) ?: return parentState to Effect.none()
            val childState = stateLens.get(parentState) ?: return parentState to Effect.none()

            val (newChildState, childEffect) = reducer.reduce(childState, childAction)
            val newParentState = stateLens.set(parentState, newChildState)
            val newParentEffect = childEffect.map { actionPrism.embed(it) }

            return newParentState to newParentEffect
        }
    }
}

operator fun <S, A> ReducerOf<S, A>.plus(
    other: ReducerOf<S, A>
): ReducerOf<S, A> {
    return object : ReducerOf<S, A> {
        override fun body() = this
        override fun reduce(state: S, action: A): Pair<S, Effect<A>> {
            val (s1, e1) = this@plus.reduce(state, action)
            val (s2, e2) = other.reduce(s1, action)
            return s2 to Effect.merge(e1, e2)
        }
    }
}

interface StoreOf<State, Action> {
    val state: StateFlow<State>
    fun send(action: Action)
    fun <ChildState, ChildAction>scope(
        lens: Lens<State, ChildState?>,
        prism: Prism<Action, ChildAction>,
    ): ScopedStore<ChildState, ChildAction>?{

        val current = state.value
        val childState = lens.get(current) ?: return null

        val childStateFlow = state
            .mapNotNull { lens.get(it) }
            .distinctUntilChanged()
            .stateIn(
                CoroutineScope(Dispatchers.Main),
                SharingStarted.Eagerly,
                initialValue = childState
            )
        return ScopedStore<ChildState, ChildAction>(
            state = childStateFlow,
            sendAction = { childAction ->
                this.send(prism.embed(childAction))
            }
        )
    }

}

class Store<State, Action>(
    initialState: State,
    private val reducer: ReducerOf<State, Action>,
) : StoreOf<State, Action> {

    private val _state = MutableStateFlow(initialState)
    override val state: StateFlow<State> = _state
    private val coroutineScope = CoroutineScope(Dispatchers.Main)

    override fun send(action: Action) {
        val (newState, effect) = reducer.reduce(_state.value, action)
        _state.value = newState
        coroutineScope.launch {
            effect.collect { newAction ->
                send(newAction)
            }
        }
    }
}

class ScopedStore<ChildState, ChildAction>(
    override val state: StateFlow<ChildState>,
    private val sendAction: (ChildAction) -> Unit
) : StoreOf<ChildState, ChildAction> {

    override fun send(action: ChildAction) {
        sendAction(action)
    }
}

class Effect<A> private constructor(
    private val flow: Flow<A>
) {
    fun <B> map(transform: (A) -> B): Effect<B> =
        Effect(flow.map(transform))

    suspend fun collect(collector: suspend (A) -> Unit) = flow.collect(collector)

    companion object {
        fun <A> none(): Effect<A> = Effect(emptyFlow())
        fun <A> just(value: A): Effect<A> = Effect(flowOf(value))
        fun <A> merge(vararg effects: Effect<A>): Effect<A> =
            Effect(merge(*effects.map { it.flow }.toTypedArray()))
        fun <A> run(
            dispatcher: CoroutineDispatcher = Dispatchers.Default,
            block: suspend (suspend (A) -> Unit) -> Unit
        ): Effect<A> {
            val flow = flow {
                coroutineScope {
                    block { action -> emit(action) }
                }
            }.flowOn(dispatcher)
            return Effect(flow)
        }
    }
}

class Lens<S, A>(
    val get: (S) -> A,
    val set: (S, A) -> S
) {
    fun <B> compose(other: Lens<A, B>): Lens<S, B> = Lens(
        get = { s -> other.get(this.get(s)) },
        set = { s, b -> this.set(s, other.set(this.get(s), b)) }
    )
}

class Prism<ParentAction, ChildAction>(
    val extract: (ParentAction) -> ChildAction?,
    val embed: (ChildAction) -> ParentAction
)

@Composable
fun <ParentState, ParentAction, ChildState, ChildAction> Navigation(
    store: StoreOf<ParentState, ParentAction>,
    lens: Lens<ParentState, ChildState?>,
    prism: Prism<ParentAction, ChildAction>,
    child: @Composable (StoreOf<ChildState, ChildAction>) -> Unit,
    parent: @Composable () -> Unit
) {
    val parentState by store.state.collectAsState()
    val childState = lens.get(parentState)

    val childStore = remember(childState) {
        store.scope(lens = lens, prism = prism)
    }

    if (childStore != null) {
        child(childStore)
    } else {
        parent()
    }
}
```

# おわりに
進捗があったら追記していきます。単方向データフローはいいぞ。