Share
Embed
ネストしたdata classの面倒な更新にさようなら!Lensを作って理解するArrowのOpticsの世界
Kotlin Fest 2025で発表したスライドです。
https://2025.kotlinfest.dev/
shiita0903
November 01, 2025
Transcript
49 slides
-
ෳࡶͳEBUBDMBTTมߋͷਏ͞ data class User(val id: Int, val profile: Profile) data
class Profile(val name: String, val statusMessage: StatusMessage) data class StatusMessage(val text: String, val updatedTime: Long) fun capitalizeStatusMessage(user: User): User = user.copy( profile = user.profile.copy( statusMessage = user.profile.statusMessage.copy( text = user.profile.statusMessage.text.uppercase() ) ) ) -
-FOTʹΑΔਏ͞ͷܰݮ fun capitalizeStatusMessage(user: User): User = user.copy( profile = user.profile.copy(
statusMessage = user.profile.statusMessage.copy( text = user.profile.statusMessage.text.uppercase() ) ) ) fun capitalizeStatusMessageWithLens(user: User): User = User.profile.statusMessage.text.modify(user) { it.uppercase() } -
-FOTΛ͏ // Get fun getStatusMessage(user: User): String = User.profile.statusMessage.text.get(user) //
Set fun updateStatusMessage(user: User, newMessage: String): User = User.profile.statusMessage.text.set(user, newMessage) // Modify(Getter + Setter) fun capitalizeStatusMessage(user: User): User = User.profile.statusMessage.text.modify(user) { it.uppercase() } -
-FOTͷΈΛཧղ͢Δ interface PLens<S, T, A, B> : POptional<S, T, A,
B> { fun get(source: S): A override fun set(source: S, focus: B): T . . . } w"SSPXLUͰͷ-FOTͷ࣮ -
(FUUFSͷ࡞ fun interface Getter<S, A> { fun get(source: S): A
} data class User(val id: Int, val profile: Profile) fun getProfile(user: User): Profile { val profileGetter = Getter<User, Profile> { source -> source.profile } return profileGetter.get(user) } w4(FUUFSͷιʔεͷܕ 6TFS w"(FUUFSͷϑΥʔΧεͷܕ 1SP fi MF -
(FUUFSͷ߹ fun interface Getter<S, A> { fun get(source: S): A
operator fun <B> plus(other: Getter<in A, out B>): Getter<S, B> = TODO() } data class User(val id: Int, val profile: Profile) data class Profile(val name: String, val statusMessage: StatusMessage) fun getStatusMessage(user: User): StatusMessage { val profileGetter = Getter<User, Profile> { source -> source.profile } val statusMessageGetter = Getter<Profile, StatusMessage> { source - > source.statusMessage } val composedGetter: Getter<User, StatusMessage> = profileGetter + statusMessageGetter return composedGetter.get(user) } -
(FUUFSͷ߹ fun interface Getter<S, A> { fun get(source: S): A
operator fun <B> plus(other: Getter<in A, out B>): Getter<S, B> = Getter { source: S -> val a: A = get(source) other.get(a) } } -
4FUUFSͷ࡞ fun interface Setter<S, A> { fun modify(source: S, map:
(A) -> A): S fun set(source: S, focus: A): S = modify(source) { _: A -> focus } } data class User(val id: Int, val profile: Profile) fun setProfile(user: User, newProfile: Profile): User { val profileSetter = Setter<User, Profile> { source, map -> val modifiedProfile = map(source.profile) source.copy(profile = modifiedProfile) } return profileSetter.set(user, newProfile) } -
4FUUFSͷ߹ fun interface Setter<S, A> { fun modify(source: S, map:
(A) -> A): S fun set(source: S, focus: A): S = modify(source) { _: A -> focus } operator fun <B> plus(other: Setter<A, B>): Setter<S, B> = Setter { source: S, map: (B) -> B -> modify(source) { focus: A - > other.modify(focus, map) } } } -
4FUUFSͷ߹ data class User(val id: Int, val profile: Profile) data
class Profile(val name: String, val statusMessage: StatusMessage) fun setStatusMessage(user: User, newStatusMessage: StatusMessage): User { val profileSetter = Setter<User, Profile> { source, map -> val modifiedProfile = map(source.profile) source.copy(profile = modifiedProfile) } val statusMessageSetter = Setter<Profile, StatusMessage> { source, map -> val modifiedStatusMessage = map(source.statusMessage) source.copy(statusMessage = modifiedStatusMessage) } val composedSetter: Setter<User, StatusMessage> = profileSetter + statusMessageSetter return composedSetter.set(user, newStatusMessage) } -
-FOTͷ࡞ class Lens<S, A>( private val getter: Getter<S, A>, private
val setter: Setter<S, A> ) : Getter<S, A> by getter, Setter<S, A> by setter { operator fun <B> plus(other: Lens<A, B>): Lens<S, B> = Lens( getter = this.getter + other.getter, setter = this.setter + other.setter ) } -
࡞ͨ͠-FOTͷར༻ data class User(val id: Int, val profile: Profile) data
class Profile(val name: String, val statusMessage: StatusMessage) data class StatusMessage(val text: String, val updatedTime: Long) fun createStatusMessageTextLens(): Lens<User, String> { val profileLens = Lens<User, Profile>( getter = { source -> source.profile }, setter = { source, map -> source.copy(profile = map(source.profile)) } ) val statusMessageLens = Lens<Profile, StatusMessage>( getter = { source -> source.statusMessage }, setter = { source, map -> source.copy(statusMessage = map(source.statusMessage)) } ) val textLens = Lens<StatusMessage, String>( getter = { source -> source.text }, setter = { source, map -> source.copy(text = map(source.text)) } ) return profileLens + statusMessageLens + textLens } -
࡞ͨ͠-FOTͷར༻ data class User(val id: Int, val profile: Profile) data
class Profile(val name: String, val statusMessage: StatusMessage) data class StatusMessage(val text: String, val updatedTime: Long) fun createStatusMessageTextLens(): Lens<User, String> { … } fun myLens(user: User, newMessage: String) { val lens = createStatusMessageTextLens() val text: String = lens.get(user) val newUser1: User = lens.set(user, newMessage) val newUser2: User = lens.modify(user) { it.uppercase() } } -
࡞ͨ͠-FOTͷܽ fun createStatusMessageTextLens(): Lens<User, String> { val profileLens = Lens<User,
Profile>( getter = { source -> source.profile }, setter = { source, map -> source.copy(profile = map(source.profile)) } ) val statusMessageLens = Lens<Profile, StatusMessage>( getter = { source -> source.statusMessage }, setter = { source, map -> source.copy(statusMessage = map(source.statusMessage)) } ) val textLens = Lens<StatusMessage, String>( getter = { source -> source.text }, setter = { source, map -> source.copy(text = map(source.text)) } ) return profileLens + statusMessageLens + textLens } -
࡞ͨ͠-FOTͷܽ fun createStatusMessageTextLens(): Lens<User, String> { val profileLens = Lens<User,
Profile>( getter = { source -> source.profile }, setter = { source, map -> source.copy(profile = map(source.profile)) } ) val statusMessageLens = Lens<Profile, StatusMessage>( getter = { source -> source.statusMessage }, setter = { source, map -> source.copy(statusMessage = map(source.statusMessage)) } ) val textLens = Lens<StatusMessage, String>( getter = { source -> source.text }, setter = { source, map -> source.copy(text = map(source.text)) } ) return profileLens + statusMessageLens + textLens } fun createStatusMessageTextLensByArrow(): Lens<User, String> = User.profile.statusMessage.text -
-FOTͷͨΊͷίʔυੜ @optics data class Profile(val name: String, val statusMessage: StatusMessage)
{ companion object } w!PQUJDTΛEBUBDMBTTʹ͚ͭΔ wۭͷDPNQBOJPOPCKFDUΛఆٛ͢Δ -
-FOTͷͨΊͷίʔυੜ @optics data class Profile(val name: String, val statusMessage: StatusMessage)
{ companion object } / / Generated code val Profile.Companion.name: Lens<Profile, String> get() = Lens( get = { profile: Profile -> profile.name }, set = { profile: Profile, value: String -> profile.copy(name = value) } ) val Profile.Companion.statusMessage: Lens<Profile, StatusMessage> get() = Lens( get = { profile: Profile -> profile.statusMessage }, set = { profile: Profile, value: StatusMessage - > profile.copy(statusMessage = value) } ) wDPNQBOJPOPCKFDUͷ֦ுϓϩύςΟܦ༝Ͱɺੜ͞Εͨ -FOT͕ར༻Մೳ -
-FOTͷͨΊͷίʔυੜ val composedLens: Lens<User, StatusMessage> = User.profile.statusMessage // Generated code
val <S> Lens<S, Profile>.statusMessage: Lens<S, StatusMessage> inline get() = this + Profile.statusMessage val User.Companion.profile: Lens<User, Profile> inline get() = Lens( get = { user: User -> user.profile }, set = { user: User, value: Profile -> user.copy(profile = value) } ) val Profile.Companion.statusMessage: Lens<Profile, StatusMessage> inline get() = Lens( get = { profile: Profile -> profile.statusMessage }, set = { profile: Profile, value: StatusMessage - > profile.copy(statusMessage = value) } ) -
-FOTͷͨΊͷϢʔςΟϦςΟؔ val modifiedUser1 = User.profile.name.modify(user) { it.uppercase() } val modifiedUser2
= User.profile.statusMessage.text .modify(modifiedUser1) { it.uppercase() } val result = User.profile.statusMessage.updatedTime .modify(modifiedUser2) { System.currentTimeMillis() } wෳͷϓϩύςΟͷҰׅมߋۤख -
-FOTͷͨΊͷϢʔςΟϦςΟؔ val result = user.copy { User.profile.name transform { it.uppercase()
} inside(User.profile.statusMessage) { StatusMessage.text transform { it.uppercase() } StatusMessage.updatedTime set System.currentTimeMillis() } } wDPQZ Ͱ-FOTΛΑΓศརʹ͑Δ wJOTJEF ಛఆͷϑΥʔΧεͷγϣʔτΧοτ -
-FOTͷͨΊͷϢʔςΟϦςΟؔ val modifiedUser1 = User.profile.name.modify(user) { it.uppercase() } val modifiedUser2
= User.profile.statusMessage.text .modify(modifiedUser1) { it.uppercase() } val result = User.profile.statusMessage.updatedTime .modify(modifiedUser2) { System.currentTimeMillis() } val result = user.copy { User.profile.name transform { it.uppercase() } inside(User.profile.statusMessage) { StatusMessage.text transform { it.uppercase() } StatusMessage.updatedTime set System.currentTimeMillis() } } -
ଞϥΠϒϥϦͱͷ࿈ܞ val messageState = remember { mutableStateOf(message) } val messageStateFlow
= MutableStateFlow(message) messageState.updateCopy { StatusMessage.text transform { it.uppercase() } } messageStateFlow.updateCopy { StatusMessage.text set "newText" } wDPQZ .VUBCMF4UBUF.VTUBCMF4UBUF'MPXͰར༻Մೳ -
ଞϥΠϒϥϦͱͷ࿈ܞ val messageState = remember { mutableStateOf(message) } val messageStateFlow
= MutableStateFlow(message) val messageSharedFlow = MutableSharedFlow<StatusMessage>() val textState: State<String> = messageState.optic(StatusMessage.text) val textStateFlow: StateFlow<String> = messageStateFlow.optic(StatusMessage.text) val textSharedFlow: SharedFlow<String> = messageSharedFlow.optic(StatusMessage.text) wPQUJD -FOTΛͬͯ৽͍͠4UBUF'MPXΛੜ -
-FOTͷݶք data class User( val id: Int, val profile: Profile,
val friends: Map<Int, User> ) fun getUserFriendProfile(user: User, friendUserId: Int): Profile? = User.friends. ??? .get(user) w-FOTEBUBDMBTTΛѻ͏ۜͷؙͰͳ͍ -
-FOTͷݶք data class User(val id: Int, val profile: Profile) sealed
class Profile { abstract val name: String abstract val statusMessage: StatusMessage data class BasicProfile( override val name: String, override val statusMessage: StatusMessage) : Profile() data class PremiumProfile( override val name: String, override val statusMessage: StatusMessage, val nameDecorationMetadata: String) : Profile() } fun maybeSetNameDecorationMetadata(user: User, newMetadata: String): User = User.profile. ??? .set(user, newMetadata) w-FOTEBUBDMBTTΛѻ͏ۜͷؙͰͳ͍ -
0QUJDT w"SSPXLUͷ0QUJDTҎԼͷͭͰߏ͞Ε͍ͯΔ w 5SBWFSTBM w 0QUJPOBM w -FOT w 1SJTN
w *TP wԼʹߦ͘΄Ͳೳྗ͕૿͑Δ͕ ద༻Մೳൣғڱ·Δ IUUQTBSSPXLUJPMFBSOJNNVUBCMFEBUBJOUSPNBOZPQUJDTUPSVMFUIFNBMM -
5SBWFSTBM interface Traversal<S, A> { fun modify(source: S, map: (focus:
A) -> A): S } w-JTUͳͲɺෳݸͷͷߋ৽͕Ͱ͖Δ wLPUMJODPMMFDUJPOTNBQΛҰൠԽͨ͠ɺNPEJGZ ͕ར༻Մೳ -
5SBWFSTBM data class UserGroup(val name: String, val users: List<User>) data
class Profile(val name: String, val statusMessage: StatusMessage) data class StatusMessage(val text: String, val updatedTime: Long) fun updateAllUpdatedTime(group: UserGroup): UserGroup = UserGroup.users.modify(group) { user - > user.map { User.profile.statusMessage.updatedTime.set(it, System.currentTimeMillis()) } } fun updateAllUpdatedTimeWithTraversal(group: UserGroup): UserGroup = UserGroup.users.every.profile.statusMessage.updatedTime .set(group, System.currentTimeMillis()) wNBQ Λ5SBWFSTBMͰஔ͖͑Δ -
0QUJPOBM interface Optional<S, A> : Traversal<S, A> { override fun
modify(source: S, map: (focus: A) -> A): S fun getOrNull(source: S): A? } w-JTU.BQͳͲͷίϨΫγϣϯ͔ΒҰͭͷΛऔΓग़ͤΔ wHFU0S/VMM Ͱࣦഊ͢ΔՄೳੑͷ͋ΔॲཧΛදݱՄೳ -
0QUJPOBM data class User( val id: Int, val profile: Profile,
val friends: Map<Int, User> ) fun getUserFriendProfile(user: User, friendUserId: Int): Profile? { val friendOptional: Optional<User, User> = User.friends.index(friendUserId) return friendOptional.profile.getOrNull(user) } w.BQ, 7Λ0QUJPOBMͰѻ͏ -
1SJTN interface Prism<S, A> : Optional<S, A> { override fun
modify(source: S, map: (focus: A) -> A): S override fun getOrNull(source: S): A? fun reverseGet(focus: A): S } wTFBMFEDMBTTͷΫϥε֊ͷදݱʹΘΕΔ wSFWFSTF(FU ͰϑΥʔΧε͔Βιʔε͕ߏஙՄೳ -
1SJTN sealed class Profile { data class BasicProfile() : Profile()
data class PremiumProfile() : Profile() } val basicProfilePrism: Prism<Profile, Profile.BasicProfile> = Profile.basicProfile val premiumProfilePrism: Prism<Profile, Profile.PremiumProfile> = Profile.premiumProfile wαϒΫϥεͷΫϥε໊ͷ1SJTN͕ੜ͞ΕΔ -
1SJTN data class User(val id: Int, val profile: Profile) sealed
class Profile { abstract val name: String abstract val statusMessage: StatusMessage data class BasicProfile( override val name: String, override val statusMessage: StatusMessage) : Profile() data class PremiumProfile( override val name: String, override val statusMessage: StatusMessage, val nameDecorationMetadata: String) : Profile() } fun maybeSetNameDecorationMetadata(user: User, newMetadata: String): User = User.profile.premiumProfile.nameDecorationMetadata.set(user, newMetadata) wTFBMFEDMBTTΛ1SJTNͰѻ͏ -
-FOT interface Lens<S, A> : Optional<S, A> { override fun
modify(source: S, map: (focus: A) -> A): S fun get(source: S): A } w(FUUFSͱ4FUUFS͕ར༻Ͱ͖Δ wHFU ͰϑΥʔΧεΛ࣮֬ʹऔಘՄೳ -
*TP interface Iso<S, A> : Prism<S, A>, Lens<S, A> {
override fun modify(source: S, map: (focus: A) -> A): S override fun reverseGet(focus: A): S override fun get(source: S): A } wͭͷܕͷؒͰɺใͷଛࣦແ͠ͷ૬ޓม͕Մೳ w-FOTͱ1SJTNͷ྆ํͷೳྗΛ࣋ͭ -
*TP data class StatusMessage(val text: String, val updatedTime: Long) val
statusMessageToPairIso: Iso<StatusMessage, Pair<String, Long >> = Iso( get = { it.text to it.updatedTime }, reverseGet = { StatusMessage(it.first, it.second) } ) @JvmInline value class UserId(val id: Int) fun UserId.getValue(): Int = UserId.id.get(this) fun Int.toUserId(): UserId = UserId.id.reverseGet(this) w*TPʹΑΔܕม -
0QUJDT·ͱΊ 0QUJDTaؔ NPEJGZ HFU0S/VMM SFWFSTF(FU HFU 5SBWFSTBM ̋ ✕ ✕
✕ 0QUJPOBM ̋ ̋ ✕ ✕ 1SJTN ̋ ̋ ̋ ✕ -FOT ̋ ̋ ✕ ̋ *TP ̋ ̋ ̋ ̋