Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arrow Optics ❤️ Compose #3299

Merged
merged 27 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
84d64c0
First draft of Arrow Optics + Compose
serras Nov 15, 2023
2270eb2
Use Java 17 in GitHub Actions
serras Nov 15, 2023
607dc71
Use Java 17 in GitHub Actions, take 2
serras Nov 15, 2023
ce0f9c6
Fix choice of compiler in Compose for -dev Kotlin versions
serras Nov 15, 2023
9bc65ae
Fix choice of compiler in Compose for -dev Kotlin versions, take 2
serras Nov 15, 2023
4b24494
Keep atomicity of update
serras Nov 17, 2023
a424188
Merge branch 'main' into serras/optics-compose
serras Nov 30, 2023
eee3786
Merge branch 'main' into serras/optics-compose
serras Nov 30, 2023
2721856
Update libs.versions.toml
serras Nov 30, 2023
0947c3a
Update build.gradle.kts
serras Nov 30, 2023
24f9d79
Merge branch 'main' into serras/optics-compose
serras Dec 11, 2023
3ac8587
Update pull_request.yml
serras Dec 11, 2023
aeadb48
Update pull_request.yml
serras Dec 11, 2023
7505214
Merge branch 'main' into serras/optics-compose
serras Dec 20, 2023
132bce0
Merge branch 'main' into serras/optics-compose
serras Dec 21, 2023
b5f037f
Update pull_request.yml
serras Dec 21, 2023
0e99a6d
Merge branch 'main' into serras/optics-compose
nomisRev Jan 16, 2024
1f1a0ff
Merge branch 'main' into serras/optics-compose
serras Jan 16, 2024
f2d5428
Merge branch 'main' into serras/optics-compose
nomisRev Jan 17, 2024
162bddc
Merge branch 'main' into serras/optics-compose
serras Jan 19, 2024
16b23ae
Update versions
serras Jan 19, 2024
93810a6
Merge branch 'main' into serras/optics-compose
nomisRev Jan 19, 2024
bfaf2c2
Merge branch 'main' into serras/optics-compose
serras Jan 22, 2024
d700c77
Do not run Compose tests with K2
serras Jan 22, 2024
f474b39
Merge branch 'main' into serras/optics-compose
serras Jan 23, 2024
452aa99
Apply suggestion
serras Jan 23, 2024
047b905
Merge branch 'main' into serras/optics-compose
serras Jan 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
public final class arrow/optics/CopyKt {
public static final fun update (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V
public static final fun updateCopy (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V
public static final fun updateCopy (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlin/jvm/functions/Function1;)V
}

public final class arrow/optics/FlowKt {
public static final fun optic (Lkotlinx/coroutines/flow/MutableStateFlow;Larrow/optics/PLens;)Lkotlinx/coroutines/flow/MutableStateFlow;
public static final fun optic (Lkotlinx/coroutines/flow/SharedFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/SharedFlow;
public static final fun optic (Lkotlinx/coroutines/flow/StateFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/StateFlow;
}

public final class arrow/optics/StateKt {
public static final fun optic (Landroidx/compose/runtime/MutableState;Larrow/optics/PLens;)Landroidx/compose/runtime/MutableState;
public static final fun optic (Landroidx/compose/runtime/State;Larrow/optics/Getter;)Landroidx/compose/runtime/State;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
public final class arrow/optics/CopyKt {
public static final fun update (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V
public static final fun updateCopy (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V
public static final fun updateCopy (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlin/jvm/functions/Function1;)V
}

public final class arrow/optics/FlowKt {
public static final fun optic (Lkotlinx/coroutines/flow/MutableStateFlow;Larrow/optics/PLens;)Lkotlinx/coroutines/flow/MutableStateFlow;
public static final fun optic (Lkotlinx/coroutines/flow/SharedFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/SharedFlow;
public static final fun optic (Lkotlinx/coroutines/flow/StateFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/StateFlow;
}

public final class arrow/optics/StateKt {
public static final fun optic (Landroidx/compose/runtime/MutableState;Larrow/optics/PLens;)Landroidx/compose/runtime/MutableState;
public static final fun optic (Landroidx/compose/runtime/State;Larrow/optics/Getter;)Landroidx/compose/runtime/State;
}

95 changes: 95 additions & 0 deletions arrow-libs/optics/arrow-optics-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
@file:Suppress("DSL_SCOPE_VIOLATION")

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile


repositories {
google()
mavenCentral()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}

plugins {
id(libs.plugins.kotlin.multiplatform.get().pluginId)
// alias(libs.plugins.arrowGradleConfig.kotlin)
alias(libs.plugins.arrowGradleConfig.publish)
alias(libs.plugins.spotless)
alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.android.library)
}

apply(from = property("ANIMALSNIFFER_MPP"))

kotlin {
explicitApi()

jvm {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Off-topic: @serras how do you feel about doing this explicitly in Arrow instead of relying on the arrowGradleConfig?

Arrow Gradle Config came to live because of publishing, and I've in favor of replacing Arrow Gradle Config Nexus/Publish with https://github.com/vanniktech/gradle-maven-publish-plugin. I've tried it on a couple of my projects and it works better as what we have whilst supporting the same style of configuration we currently use.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds great (although having to define all the targets on each project seems tiresome). Maybe another thing to do in the arrow-2 branch?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe another thing to do in the arrow-2 branch?

Was wondering if we could do something interesting in buildSrc or something where we can define some top-level functions to avoid all the repetitive boilerplate. arrow-2 sounds good! Definitely not for 1.2.2 😅

jvmToolchain(8)
}
js(IR) {
browser()
nodejs()
}
androidTarget()
// Native: https://kotlinlang.org/docs/native-target-support.html
// -- Tier 1 --
linuxX64()
macosX64()
macosArm64()
iosSimulatorArm64()
iosX64()
// -- Tier 2 --
// linuxArm64()
watchosSimulatorArm64()
watchosX64()
watchosArm64()
tvosSimulatorArm64()
tvosX64()
tvosArm64()
iosArm64()
// -- Tier 3 --
mingwX64()

sourceSets {
commonMain {
dependencies {
api(projects.arrowOptics)
api(libs.coroutines.core)
api(compose.runtime)
implementation(libs.kotlin.stdlibCommon)
}
}

commonTest {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotest.assertionsCore)
implementation(libs.kotest.property)
}
}
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}

compose {
// override the choice of Compose if we use a Kotlin -dev version
val kotlinVersion = project.rootProject.properties["kotlin_version"] as? String
if (kotlinVersion != null && kotlinVersion.contains("-dev-")) {
kotlinCompilerPlugin.set(dependencies.compiler.forKotlin("2.0.0-Beta1"))
kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=$kotlinVersion")
}
}

android {
namespace = "arrow.optics.compose"
compileSdk = libs.versions.android.compileSdk.get().toInt()
}

tasks.named<Jar>("jvmJar").configure {
manifest {
attributes["Automatic-Module-Name"] = "arrow.optics.compose"
}
}
4 changes: 4 additions & 0 deletions arrow-libs/optics/arrow-optics-compose/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Maven publishing configuration
pom.name=Arrow Optics for Compose
# Build configuration
kapt.incremental.apt=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package arrow.optics

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.snapshots.Snapshot
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update

/**
* Modifies the value in this [MutableState]
* by applying the function [block] to the current value.
*/
public inline fun <T> MutableState<T>.update(crossinline block: (T) -> T) {
Snapshot.withMutableSnapshot {
serras marked this conversation as resolved.
Show resolved Hide resolved
value = block(value)
}
}

/**
* Modifies the value in this [MutableState]
* by performing the operations in the [Copy] [block].
*/
public fun <T> MutableState<T>.updateCopy(block: Copy<T>.() -> Unit) {
update { it.copy(block) }
}

/**
* Updates the value in this [MutableStateFlow]
* by performing the operations in the [Copy] [block].
*/
public fun <T> MutableStateFlow<T>.updateCopy(block: Copy<T>.() -> Unit) {
update { it.copy(block) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package arrow.optics

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

/**
* Exposes the values of [this] through the optic.
*/
public fun <T, A> SharedFlow<T>.optic(g: Getter<T, A>): SharedFlow<A> = object : SharedFlow<A> {
override suspend fun collect(collector: FlowCollector<A>): Nothing =
this@optic.collect { collector.emit(g.get(it)) }

override val replayCache: List<A>
get() = this@optic.replayCache.map { g.get(it) }
}

/**
* Exposes the values of [this] through the optic.
*/
public fun <T, A> StateFlow<T>.optic(g: Getter<T, A>): StateFlow<A> = object : StateFlow<A> {
override val value: A
get() = g.get(this@optic.value)

override suspend fun collect(collector: FlowCollector<A>): Nothing =
this@optic.collect { collector.emit(g.get(it)) }

override val replayCache: List<A>
get() = this@optic.replayCache.map { g.get(it) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always contains single element.

Suggested change
get() = this@optic.replayCache.map { g.get(it) }
get() = listOf(value)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep this implementation, since it makes it more clear that we are just reusing the replayCache from the nested version.

}

/**
* Exposes the values of [this] through the optic.
* Any change made to [value] is reflected in the original [MutableStateFlow].
*/
public fun <T, A> MutableStateFlow<T>.optic(lens: Lens<T, A>): MutableStateFlow<A> = object : MutableStateFlow<A> {
override var value: A
get() = lens.get(this@optic.value)
set(newValue) {
this@optic.value = lens.set(this@optic.value, newValue)
}

override suspend fun collect(collector: FlowCollector<A>): Nothing =
this@optic.collect { collector.emit(lens.get(it)) }

override fun compareAndSet(expect: A, update: A): Boolean {
val expectT = lens.set(this@optic.value, expect)
val updateT = lens.set(this@optic.value, update)
return compareAndSet(expectT, updateT)
}

override fun tryEmit(value: A): Boolean =
this@optic.tryEmit(lens.set(this@optic.value, value))

override suspend fun emit(value: A): Unit =
this@optic.emit(lens.set(this@optic.value, value))

override val subscriptionCount: StateFlow<Int>
get() = this@optic.subscriptionCount

override val replayCache: List<A>
get() = this@optic.replayCache.map { lens.get(it) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above 🙏


@ExperimentalCoroutinesApi
override fun resetReplayCache() {
this@optic.resetReplayCache()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package arrow.optics

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State

/**
* Exposes the value of [this] through the optic.
*/
public fun <T, A> State<T>.optic(g: Getter<T, A>): State<A> = object : State<A> {
override val value: A
get() = g.get(this@optic.value)
}

/**
* Exposes the value of [this] through the optic.
* Any change made to [value] is reflected in the original [MutableState].
*/
public fun <T, A> MutableState<T>.optic(lens: Lens<T, A>): MutableState<A> = object : MutableState<A> {
override var value: A
get() = lens.get(this@optic.value)
set(newValue) {
this@optic.value = lens.set(this@optic.value, newValue)
}

override fun component1(): A = value
override fun component2(): (A) -> Unit = { value = it }
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ allprojects {

plugins {
base
alias(libs.plugins.android.library) apply false
alias(libs.plugins.dokka)
alias(libs.plugins.animalSniffer) apply false
alias(libs.plugins.kotest.multiplatform) apply false
Expand All @@ -46,6 +47,7 @@ plugins {
alias(libs.plugins.kotlin.binaryCompatibilityValidator)
alias(libs.plugins.arrowGradleConfig.nexus)
alias(libs.plugins.spotless) apply false
alias(libs.plugins.jetbrainsCompose) apply false
}

apply(plugin = libs.plugins.kotlinx.knit.get().pluginId)
Expand Down
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ kotlin.mpp.stability.nowarn=true
# https://youtrack.jetbrains.com/issue/KT-32476
kotlin.native.ignoreIncorrectDependencies=true
kotlin.native.ignoreDisabledTargets=true
kotlin.native.cacheKind.linuxX64=none
# kotlin.native.cacheKind.linuxX64=none
# https://youtrack.jetbrains.com/issue/KT-45545#focus=Comments-27-4773544.0-0
kapt.use.worker.api=false
kotlin.mpp.applyDefaultHierarchyTemplate=false
Expand All @@ -37,3 +37,5 @@ ANIMALSNIFFER=../../gradle/animalsniffer.gradle
ANIMALSNIFFER_MPP=../../gradle/animalsniffer-mpp.gradle

dokkaEnabled=false

android.useAndroidX=true
7 changes: 7 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ mockWebServer = "4.12.0"
retrofit = "2.9.0"
retrofitKotlinxSerialization = "1.0.0"
spotlessVersion = "6.25.0"
compose = "1.6.0-rc01"
composePlugin = "1.6.0-dev1369"
agp = "8.2.0"
android-compileSdk = "34"
cache4k = "0.12.0"

[libraries]
Expand Down Expand Up @@ -52,6 +56,7 @@ ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref =
kspGradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "kspVersion" }
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
classgraph = { module = "io.github.classgraph:classgraph", version.ref = "classgraph" }
compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" }
kotlinCompileTesting = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" }
kotlinCompileTestingKsp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kotlinCompileTesting" }
cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" }
Expand All @@ -73,3 +78,5 @@ kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerializationPlugin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "kspVersion" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotlessVersion" }
jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "composePlugin" }
android-library = { id = "com.android.library", version.ref = "agp" }
22 changes: 18 additions & 4 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("LocalVariableName")

enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

rootProject.name = "arrow"
Expand All @@ -9,6 +11,8 @@ pluginManagement {
mavenCentral()
mavenLocal()
kotlin_repo_url?.also { maven(it) }
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
}

Expand All @@ -17,11 +21,12 @@ plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0")
}

dependencyResolutionManagement {
@Suppress("LocalVariableName") val kotlin_repo_url: String? by settings
@Suppress("LocalVariableName") val kotlin_version: String? by settings
@Suppress("LocalVariableName") val ksp_version: String? by settings
val kotlin_repo_url: String? by settings
val kotlin_version: String? by settings
val ksp_version: String? by settings
val compose_version: String? by settings

dependencyResolutionManagement {
repositories {
mavenCentral()
gradlePluginPortal()
Expand All @@ -38,6 +43,10 @@ dependencyResolutionManagement {
println("Overriding KSP version with $ksp_version")
version("kspVersion", ksp_version!!)
}
if (!compose_version.isNullOrBlank()) {
println("Overriding Compose version with $compose_version")
version("composePlugin", compose_version!!)
}
}
}
}
Expand Down Expand Up @@ -87,6 +96,11 @@ project(":arrow-optics").projectDir = file("arrow-libs/optics/arrow-optics")
include("arrow-optics-reflect")
project(":arrow-optics-reflect").projectDir = file("arrow-libs/optics/arrow-optics-reflect")

if (kotlin_version.isNullOrBlank() || "2.0" !in kotlin_version!!) {
include("arrow-optics-compose")
project(":arrow-optics-compose").projectDir = file("arrow-libs/optics/arrow-optics-compose")
}

include("arrow-optics-ksp-plugin")
project(":arrow-optics-ksp-plugin").projectDir = file("arrow-libs/optics/arrow-optics-ksp-plugin")

Expand Down