buildfoundation / ketolang

Future of general purpose Сonfiguration languages. Side-effect-less dialect of Kotlin.
Apache License 2.0
81 stars 1 forks source link

[Feature Request/Question] Higher-Order Function with Receivers #18

Open Sintrastes opened 2 years ago

Sintrastes commented 2 years ago

Hi.

First off, this seems like a great project, and I am looking forward to keeping track of it's development.

I was wondering, will this project support higher-order functions with receivers? This seems like it would be very useful for allowing for common types of Kotlin-style DSLs for being defined in Ketolang, is (in my opinion) one of the most distinctive features of Kotlin, and I believe is still compatible with the goals of Ketolang.

One issue I see with this is that in order to be useful, Kotolang would probably have to also support interfaces to allow for useful receivers for builder DSLs. But then again, maybe defining data classes in a "record of functions" manner would be sufficient, e.x.

data class BuilderScope(
    val add: (Int) -> Unit
)

fun sillyBuilder(handler: BuilderScope.() -> Unit): List<Int> {
    // Internal mutable var -- allowed in Ketolang
    var ints = listOf<Int>()
    val scope = BuilderScope { ints = listOf(it) + ints }
    scope.handler()
    return ints
}

val example = sillyBuilder {
    add(1)
    add(2)
    add(3)
}
artem-zinnatullin commented 2 years ago

Hi @Sintrastes, I'll be having a conversation with JB people hopefully soon and will definitely be touching Kotlin DSL syntax support.

Looking at your example what bugs me is that data class has a property of functional type, meaning that it stores not value but a behaviour.

One of the goals of the Ketolang language is to split data from functions, so a functional type should not be stored in data. Benefit that it gives is that Ketolang data types should be serializable w/o additional work from the developer, which is particularly helpful for caching Ketolang execution: function must produce same output for given input, meaning that parameters and return value can be cached — need to be serialized.

For now I'd like to point to Starlark examples (which is the language that inspires Ketolang):

As you can find in these examples there are just function invocations with data types, I do understand that Kotlin DSL is something many people got used to, especially with Gradle build.gradle.kts files, hopefully we'll figure out some consensus on that.


To add to this, there are few issues with Kotlin DSLs that I'm not sure we'll be able to solve:

DSL function can be called multiple times

(Example refers to kotlinx.html HTML)

html {
  body {

  }

  body {

  }
}

Here I've called body twice, that doesn't make much sense and not all DSLs will have meaningful way of handling multiple invocations of DSL builder blocks. And what makes it even weirder is that body could've been a property (see below, so it's an inconsistent design choice up-to developer).

As opposed to this where these are simple function calls with parameters that can be passed once:

html(
  body = body(children = listOf(h1(text = "abc"))),
)

Writing DSL in Kotlin is quite confusing

DSL requires writing functions with receivers A.(B) -> C

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}

Even for people with years of production Kotlin use this looks confusing. Ketolang aims to be generic configuration language and many of the Kotlin features I've limited so far are limited for sake of simplicity for people w/o Kotlin experience.

Kotlin DSL allows both properties and functions

Property probably means that a value is stored once, function probably means that value can be stored once or multiple times I guess, but that is all up to DSL developer, one can override property setter and accept multiple property assignments into a combined data type. It's all around confusing I'd say. In HTML example above body could've property, but it is a function, why…? Ketolang can limit DSL syntax, but yeah.

// See https://proandroiddev.com/writing-dsls-in-kotlin-part-2-cd9dcd0c4715
val person = person {
    name = "John"
    dateOfBirth = "1980-12-01"
    address {
        street = "Main Street"
        number = 12
        city = "London"
    }
    address {
        street = "Dev Avenue"
        number = 42
        city = "Paris"
    }
}

Ketolang aims to gently suggest different users to write similar code in similar way, this is why for now interfaces and other OOP features are disabled.

Kotlin DSL requires mutable state

// See https://proandroiddev.com/writing-dsls-in-kotlin-part-2-cd9dcd0c4715
val person = person {
    name = "John"
    address {}
}

class PersonBuilder {
    var name: String = ""
    private val addresses = mutableListOf<Address>()
}

This perhaps can be generically solved by declaring that mutable state in a function, rather than a class, as you cleverly did in your example which I find honestly better than having mutable builder classes. However, I'm not sure this is reusable when you need to share builder like you do for HTML where many tags are compatible with each other and you kind of need to expose builders to each other?


These are my current thoughts on Kotlin DSLs in Ketolang, please post your thoughts and ideas, thanks!