今天來介紹點大家不是很常碰到的神奇用法,看看下面這段程式:
data class Path(val content: String, val id: String)
fun copyPath() {
val path = Path("/some/path", "235123")
val copiedPath = path.copy(id = "5566") // Path("/some/path", "5566")
}
只要在 Kotlin 的 class 前面加上 data 變成 data class
,就可以使用 copy
,其功能為複製原有的內容並改變指定的成員變數。最棒的是, copy 拿到的資料是一個全新的實例,也就是說是 immutable 的,在寫 functional code 的時候是很方便的工具。
但是如果現在的情況是要複製並更改巢狀的資料的話,就要寫多一點程式碼來實現了:
data class Image(val uri: Uri, val mask: Mask)
data class Mask(val uri: Uri)
data class Uri(val scheme: String, val path: Path)
data class Path(val content: String, val id: String)
fun copyImageWithId(image: Image, id: String): Image {
return image.copy(uri = image.uri.copy(path = image.uri.path.copy(id = id)))
}
擁有越“巢”的資料,就要呼叫越多次的 copy
。
現在來試試看別種解法:Lenses。Lenses 有兩個 function:一個是 get ,另一個是 set
interface Lenses<T, F> {
fun get(): F
fun set(value: T, field: F): T
}
其中 T 是要改變的類別,F 則是要改變的成員變數,我們以 Path
與 id
為例:
class PathIdLenses: Lenses<Path, String> {
override fun get(value: Path): String {
return value.id
}
override fun set(value: Path, field: String): Path {
return value.copy(id = field)
}
}
fun lensesSample() {
val path = Path("/some/path", "4455")
val pathIdLenses = PathIdLenses()
// "4455"
val id = pathIdLenses.get(path)
// Path("/some/path", "7788")
val newPath = pathIdLenses.set(path, "7788")
}
看起來是一個沒什麼作用的類別,對吧?這些事情即使沒有 Lenses 也可以很簡單的做到,但這介面其實是給我們組合用的,讓我們把剩下的也完成:
class PathIdLenses: Lenses<Path, String> {
override fun get(value: Path): String {
return value.id
}
override fun set(value: Path, field: String): Path {
return value.copy(id = field)
}
}
class UriPathLenses: Lenses<Uri, Path> {
override fun get(value: Uri): Path {
return value.path
}
override fun set(value: Uri, field: Path): Uri {
return value.copy(path = field)
}
}
class MaskUriLenses: Lenses<Mask, Uri> {
override fun get(value: Mask): Uri {
return value.uri
}
override fun set(value: Mask, field: Uri): Mask {
return value.copy(uri = field)
}
}
class ImageMaskLenses: Lenses<Image, Mask> {
override fun get(value: Image): Mask {
return value.mask
}
override fun set(value: Image, field: Mask): Image {
return value.copy(mask = field)
}
}
// 聽說程式碼一個字母就算一個字,這絕對不是在衝鐵人賽的字數XD
有沒有注意到這些 Type 都是頭尾相連的?<Image, Mask> → <Mask, Uri> → <Uri, Path> → <Path, String> ,既然 function 可以組合起來,何不試試看讓類別也組合起來呢?
fun <A, B, C> composeLenses(outer: Lenses<A, B>, inner: Lenses<B, C>): Lenses<A, C> {
return object : Lenses<A, C> {
override fun get(value: A): C {
return inner.get(outer.get(value))
}
override fun set(value: A, field: C): A {
val innerValue = inner.set(outer.get(value), field)
return outer.set(value, innerValue)
}
}
}
如此一來,我們就可以把這四個類別組合起來了,如下:
fun lensesSample(image: Image) {
val imageIdLenses = composeLenses(
ImageMaskLenses(),
composeLenses(
MaskUriLenses(),
composeLenses(
UriPathLenses(),
PathIdLenses()
)
)
)
val newImage = imageIdLenses.set(image, "5566")
}
再稍微修改一下原函式,改成 infix function 就可以這樣使用(留給大家當練習):
val imageIdLenses = ImageMaskLenses() compose MaskUriLenses() compose UriPathLenses() compose PathIdLenses()
相信大家都會覺得上面的做法超麻煩,寧願寫多次的 copy 也不要寫這麼多 boilerplate code(樣板程式),對吧?好家在 Functional programming library - Arrow 有提供另一個替代方案,藉由 Annotation 來自動產生程式碼,如下:
// build.gradle
apply plugin: 'kotlin-kapt'
def arrow_version = "0.11.0"
dependencies {
implementation "io.arrow-kt:arrow-optics:$arrow_version"
implementation "io.arrow-kt:arrow-syntax:$arrow_version"
kapt "io.arrow-kt:arrow-meta:$arrow_version"
}
@optics
data class Image(val uri: Uri, val mask: Mask) {
companion object
}
@optics
data class Mask(val uri: Uri) {
companion object
}
@optics
data class Uri(val scheme: String, val path: Path) {
companion object
}
@optics
data class Path(val content: String, val id: String) {
companion object
}
只要在 class 的前面加上 optics
,就會自動幫你產生所有的 Lenses 給你,還有 Compaion object 也是需要的。build 完 code 之後,自動產生的程式碼將會在 Compaion 底下,例如 Image.Compaion.mask
或是Image.Compaion.uri
,實際的範例如下:
val image = Image(
uri = Uri(scheme = "https", path = Path(id = "34", content = "/hello/world")),
mask = Mask(
uri = Uri(scheme = "https", path = Path(id = "35", content = "/hello/welcome"))
)
)
val imageIdLenses = Image.mask compose Mask.uri compose Uri.path compose Path.id
val newImage = imageIdLenses.set(image, "5566")
println(image)
println(newImage)
// Image(uri=Uri(scheme=https, path=Path(content=/hello/world, id=34)),
// mask=Mask(uri=Uri(scheme=https, path=Path(content=/hello/welcome, id=35))))
//
// Image(uri=Uri(scheme=https, path=Path(content=/hello/world, id=34)),
// mask=Mask(uri=Uri(scheme=https, path=Path(content=/hello/welcome, id=5566))))
不知道大家是否腦洞又更開了呢?原來不只 function 可以組合,Functor 可以組合,現在連 class 都可以組合了!今天留給大家的練習(infix 版本的 composeLenses) 將會在下一篇提供解答。