Support concise and statically-typed configuration blocks #54

Open
bamboo opened this Issue May 30, 2016 · 3 comments

Projects

None yet

4 participants

@bamboo
Member
bamboo commented May 30, 2016 edited

The configuration block is a very common pattern throughout the Gradle API.

A good example is Project#copy which thanks to its (groovy.lang.Closure) overload can be used from Groovy in a very concise way:

copy {
    from configurations.runtime
    into 'build/deploy/lib'
}

And because the Gradle API must be useable from languages other than Groovy, a companion org.gradle.api.Action overload is usually provided.

Kotlin lambda expressions can be readily used with such Action overloads yielding a statically-typed alternative which is almost as concise even if slightly clumsy:

copy {
    it.from(configurations.runtime)
    it.into("build/deploy/lib")
}

Kotlin does supply the necessary mechanisms to allow for the same level of conciseness as the original Groovy version, extension functions and function literals with receiver:

fun Project.copy_(configuration: CopySpec.() -> Unit) = copy({ it.configuration() })

copy_ {
    from(configurations.runtime)
    into("build/deploy/lib")
}

In the snippet above Project.copy_ is an extension function that takes a configuration function parameter (declared with a CopySpec receiver, the CopySpec. part of the parameter type) and acts as a façade to the original Project#copy(Action<CopySpec>) overload. It must be named copy_ instead of simply copy here because interface members take precedence over extension members and currently there's no mechanism to instruct Kotlin otherwise.

By giving the Kotlin compiler a carefully crafted API jar it would be possible to achieve the desired level of conciseness without having to resort to different names.


Tasks that should be mapped to cards when this Epic is ready to be worked on

  • Discuss with the Kotlin team the idea of having lazy implement Callable; possibly issue a PR for it
@bamboo bamboo added this to the 1.0.0 (GA) milestone May 30, 2016
@bamboo bamboo added the Epic label May 30, 2016
@cbeams cbeams modified the milestone: 1.0.0-M2, 1.0.0 (GA), 1.0.0-M3 May 31, 2016
@cbeams cbeams added the a:feature label May 31, 2016
@oehme
Member
oehme commented May 31, 2016

Do we know why this is not the default in Kotlin? Why is the single argument to the lambda not automatically the implicit receiver? That would make so many Java APIs easier to use with Kotlin without writing a bunch of overloads.

@melix
Member
melix commented May 31, 2016

This is an interesting discussion. I had the same issue when implementing the static Groovy DSL. Groovy does the same as Kotlin here: by default, you have to write it.copy. However, I'm generating extension methods (at build time) that create an overloaded Closure parameter and set the delegate appropriately. It works as a charm.

So either:

  • Kotlin needs to support the same pattern, and allow the extension method to be picked up first
  • Kotlin needs to allow the receiver to be the "delegate" implicitly

We have so many methods taking Action<...> which is both Java and Groovy friendly that it would be a pity to have to introduce a new one for Kotlin, especially if the methods have to be named differently.

@cbeams
Member
cbeams commented Jun 2, 2016

I've just updated the description with a section containing tasks that should be mapped to cards once we begin working on this epic. Namely, @bamboo had the idea of having Kotlin's lazy implement Callable so that deferred configuration can happen in Kotlin-friendly way, e.g.:

some.configuration(lazy {
    // expensive logic
})
@bamboo bamboo modified the milestone: 0.3.0, 1.0.0 Jul 6, 2016
@bamboo bamboo modified the milestone: 0.3.2, 1.0.0 Aug 5, 2016
@bamboo bamboo added a commit that referenced this issue Aug 25, 2016
@bamboo bamboo Compile scripts against generated Kotlin API jar
We would like Kotlin build scripts to behave as if all the `Action<T>`
parameters in the Gradle API had been declared as `T.() -> Unit` to
avoid the need for explicitly qualifying the single argument to the
given lambda expressions with `it`.

In other words, we would like users to be writing code like:

    copySpec {
       from("src")
       into("out")
    }

Instead of:

    copySpec {
        it.from("src")
        it.into("out")
    }

Where `copySpec` is declared in the Gradle Java API as:

    CopySpec copySpec(Action<? super CopySpec> configuration)

So far we have been able to avoid the qualifying `it` in some situations
via mindful use of inheritance and Kotlin extensions but a comprehensive
solution was still lacking. The underlying issue is that while Kotlin
does provide a type extension mechanism, type members still take
precedence over extensions and currently there's no mechanism to
instruct Kotlin otherwise.

In the future we might be able to implement a different solution to this
particular issue via a new Kotlin language feature still in discussion:

 -  https://youtrack.jetbrains.com/issue/KT-12848

In the meantime, by giving the Kotlin compiler a carefully crafted API
jar with all members that could potentially conflict with our provided
extensions removed we can work around the fact that interface members
take precedence over extension members and expose all the extensions we
want.

And that is the solution implemented in this commit:

  - Remove all API methods that take a last `Action<T>` parameter
  - Generate shim extensions that take a last `T.() -> Unit`

Proper treatment for generic types will be implemented in a future
commit.

Resolves: #52
See also: #54, #117
7b3890e
@bamboo bamboo modified the milestone: 0.4.0, 0.3.2, 0.5.0 Sep 7, 2016
@bamboo bamboo modified the milestone: 0.6.0, 0.5.0 Oct 21, 2016
@bamboo bamboo modified the milestone: 0.6.0, 1.0.0 Nov 24, 2016
@bamboo bamboo modified the milestone: 0.7.0, 1.0.0 Jan 6, 2017
@bamboo bamboo modified the milestone: 0.8.0, 0.7.0 Jan 16, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment