Today I learn Swift supports a feature that I thin...
# language-proposals
m
Today I learn Swift supports a feature that I think it’d be useful in Kotlin: Result Builders. The feature improves the ability to create declarative DSL-like, by allowing a collection to be written as a common DSL. The usage syntax is very similar to the syntax of Compose’s
Row
,
Column
, and similar view groups but it is heavily customisable. Basically, you can do something like:
Copy code
// Definition, kind of:
@resultBuilder
struct SettingsBuilder {
    static func buildBlock() -> [Setting] { [] }
}

func makeSettings(@SettingsBuilder _ content: () -> [Setting]) -> [Setting] {
    content()
}

// Usage, each line is an item in the array:
makeSettings {
    Setting(name: "Offline mode", value: .bool(false))
    Setting(name: "Search page size", value: .int(25))
}
That would be similar to what we have with Compose groups, I guess:
Copy code
Row {
  Text("Offline mode")
  Second("Search page size")
}
The nice part is that you can create and customize your own “view group”-like DSL to build your APIs. Is there anything similar in the plans for Kotlin?
f
I guess you can already do that with K (there are many guides on how to write your own DSLs) and it will become super-powered with context receivers. Your example can be achieved with:
Copy code
data class Setting(val name: String, val value: Any)

val settings = buildList {
    add(Setting(name = "Offline mode", value = false))
    add(Setting(name = "Search page size", value = 25))
}
or you can add your semantic wrapper if you really like and achieve
Copy code
val settings = makeSettings {
    setting(...)
    setting(...)
}
easily, already
m
And I just made that semantic wrapper to show you 😛
Copy code
data class Setting<T>(val name: String, val value: T)

class SettingsBuilder {
    val list = mutableListOf<Setting<*>>()
    
    fun <T> setting(name: String, value: T) = 
        list.add(Setting(name, value))
}

fun makeSettings(builder: SettingsBuilder.() -> Unit): List<Setting<*>> = 
    SettingsBuilder().apply { builder() }.list

fun main() { 
  val settings = makeSettings {
    setting("Offline mode", false)
    setting("Search page size", 25)
  }
  println(settings)
}
m
@franztesca I’m familiar with writing DSLs in Kotlin, in your example you are just doing a DSL-like on top of a builder but you need to directly call
add
(like in a normal
builder
). If you nest two types inside each other (let’s say, a Row, inside a Column, inside a…), you would end up with a lot of indentation and a confusing syntax IMO.
Copy code
Row {
  add {
    Column {
      add {
        // etc...
      }
    }
  }
}
ResultBuilders
let you write your own “builders like” without this verbosity, which can be extended to any API (I think today Compose does that with a Compiler plugin… but that could be at language level and seems interesting to make more complex DSLs with inner collections). Each expression (line), is added to the collection that later can be traversed.
@marstran my example is just copied and pasted from a simple usage but IIUC, they are way more powerful because: 1. You can easily create generic collections, and leverage that (let’s say, UI components rendering - they use it inside SwiftUI; Compose seems to have built that mechanism using a Compiler Plugin). 2. You don’t need to write one builder function for supported type. The
add { }
, seems better.
Anyway, I was just wondering considering how DSLs are powerful in Kotlin and how Compose UI seems to rely in a similar “style” (I don’t really know how they do it in Compose, but I’m guessing Compiler Plugin). It seems it would be somehow interesting to have supported for that inside the language. If people find useful, I could write a proposal but not sure if it is worth, hence my question.
y
In Kotlin (playground):
Copy code
// This would exist in a library somewhere, just like how it's in Rust's stdlib
// Made it inline because why not!
@JvmInline value class ResultBuilder<T>(private val list: MutableList<T>) {
    operator fun T.unaryPlus() { list.add(this) }
}
// Convenience function
inline fun <T> makeResult(content: ResultBuilder<T>.() -> Unit) = buildList {
    ResultBuilder(this).content()
}

// In some UI library
typealias SettingsBuilder = ResultBuilder<Setting<*>>

fun makeSettings(content: SettingsBuilder.() -> Unit): List<Setting<*>> = makeResult(content)

// Only do this if Setting should only be created inside a `makeSettings` block,
// otherwise use the + operator
data class Setting<out T> private constructor(val name: String, val value: T) {
    companion object {
        context(SettingsBuilder)
        operator fun <T> invoke(name: String, value: T) { +Setting(name, value) }
    }
}

fun main() {
    // Usage, each line adds an item to the list
    println(makeSettings {
        Setting(name = "Offline mode", value = false)
        Setting(name = "Search page size", value = 25)
    })
    //Setting(name = "Offline mode", value = false) // Not allowed
}
Although this naming scheme is definitely not kotlin-y. I would instead go with something like this:
Copy code
// This would exist in a library somewhere, just like how it's in Swift's stdlib
@JvmInline value class Builder<in T>(private val list: MutableList<T>) {
    operator fun T.unaryPlus() { list.add(this) }
}
// Convenience function
inline fun <T> build(@BuilderInference block: Builder<T>.() -> Unit) = buildList {
    Builder(this).block()
}

// In some UI library
data class Setting<out T> constructor(val name: String, val value: T)

context(Builder<Setting<T>>)
fun <T> setting(name: String, value: T) {
    +Setting(name, value) 
}

// Perhaps this is a weird default where you want +"SettingName" to add a setting with a boolean value by default
context(Builder<Setting<Boolean>>)
operator fun String.unaryPlus() { 
    +Setting(this, false) 
}

fun main() {
    // Usage, each line adds an item to the list
	// Type inference does the heavy lifting here
    println(build {
        setting(name = "Offline mode", value = false)
        +Setting(name = "Search page size", value = 25)
    })
	// or you can specify the type yourself, especially when you have such overloads as above
	println(build<Setting<*>> {
        +Setting(name = "Offline mode", value = false)
        +Setting(name = "Search page size", value = 25)
        +"Online visibility on"
	})
}
Looking at the swift proposal, note that none of the "Result Building Methods" are really needed in Kotlin because it's just using the simple idea of receivers and extension methods and combining them to have function calls like
+
and
setting
be resolved automatically to the right thing and require that it's inside a
Builder
. The way Kotlin does it means that it plays nicely with all the other language features, while it looks like that Swift had to disallow a whole slew of built-in functions inside of result builders.
m
I see. Fancy way to use the
unaryPlus
til (I would still prefer without but that seems a valid approach without complicate anything). To be sure everyone is the same page, so the
unaryPlus
would be the “Kotlin idiomatic way” to do generic “result builders”. Is that correct?
y
You don't even need
unaryPlus
as long as you define methods like
setting()
or make the constructors for your data types internal and have factory functions (like a
fun Setting()
), or you can make the constructor private and have a
operator invoke
on the companion object as per my first example. All 3 of those methods would take a
context(Builder<Blah>)
, and so they'll be able to add the values to the array, but they can do so, so much more without ruining the semantics or composability of the language. In fact, compose existed before we had those context receivers, and so it had to get around some of those issues with a compiler plugin (and compose also does some magic to do efficient recomposition, but the basic UI-tree building aspect of it is absolutely possible in pure Kotlin. See also kotlinx.html for an HTML DSL library that might provide some inspiration
112 Views