Skip to content

Concise and statically-typed Property<T> assignment #9268

Closed
@eskatos

Description

@eskatos
Member

Status Quo

Extension

public interface Extension {
    Property<String> getDescription();
}
public interface OtherExtension {
    Property<String> getDescription();
}

Groovy DSL

extension {
    description = "My $project.description"
    description = otherExtension.description
    description = provider {
        // compute stuff lazily
    }
}

Kotlin DSL (build script or precompiled script)

extension {
    description.set("My $project.description")
    description.set(otherExtension.description)
    description.set(provider {
        // compute stuff lazily
    })
}

Potential solutions

Option 1: Kotlin supports equals = operator overloading

Implementation

operator fun <T> Property<T>.EQUALS(value: T): Unit = set(value)
operator fun <T> Property<T>.EQUALS(provider: Provider<T>): Unit = set(provider)

Kotlin DSL (build script or precompiled script)

extension {
    description = "My $project.description"
    description = otherExtension.description
    description = provider {
        // compute stuff lazily
    }
}

Groovy DSL
Unchanged

Pros/Cons

  • (+) Simple, just add two kotlin extensions
  • (+) Groovy and Kotlin DSL look the same
  • (-) Kotlin support not likely to happen

Option 2: Rename Property.set/get to setValue/getValue

Just the for the plain value, do not rename set(Provider<T>).

Kotlin DSL (build script or precompiled script)

extension {
    description.set("My $project.description") // DEPRECATED
    description.value = "My $project.description"
    description.set(otherExtension.description)
    description.set(provider {
        // compute stuff lazily
    })
}

Groovy DSL

extension {
    description = "My $project.description" // DEPRECATED
    description.value = "My $project.description"
    description = otherExtension.description
}

Pros/Cons

  • (+) Simple, just add two methods, deprecate two old ones
  • (+) Explicit difference between using the plain value or connecting to other properties
  • (-/+) Will not look the same in Groovy and Kotlin DSL unless we remove sugar from Groovy DSL
  • (-) Looks nice for eager assignment only

Alternative

Also rename set(Provider<T>) to setValue(Provider<T>).

extension {
    description.set("My $project.description") // DEPRECATED
    description.value = "My $project.description"
    description.setValue(otherExtension.description)
    description.setValue(provider {
        // compute stuff lazily
    })
}

This makes it worse at first until Kotlin supports overloaded property setters gradle/kotlin-dsl-samples#380

BUT it becomes semantically wrong, value = provider doesn't make much sense.

Option 3: Kotlin invoke operator overloading

Implementation

operator fun <T> Property<T>.invoke(value: T?): Unit = set(value)
operator fun <T> Property<T>.invoke(provider: Provider<T>): Unit = set(provider)

Kotlin DSL (build script or precompiled script)

extension {
    description("foo")
    description(otherExtension.description)
    description(provider {
        // compute stuff lazily
    })
}

Pros/Cons

  • (+) Simple, just add two kotlin extensions
  • (-) Still not assignments

Activity

changed the title [-]Concise and statically-typed `Property` assignment[/-] [+]Concise and statically-typed `Property<T>` assignment[/+] on Sep 7, 2018
JLLeitschuh

JLLeitschuh commented on Sep 7, 2018

@JLLeitschuh
Contributor

Option 3 might also collide with plugin author's extensions where they've defined

fun description(a: String) {
    // ...
}

This might be an non-issue though.

jnizet

jnizet commented on Sep 7, 2018

@jnizet
Contributor

And simply not exposing properties or type Property directly, but exposing plain old String/whatever properties delegating to private Property fields isn't an option, even for incubating classes?

eskatos

eskatos commented on Sep 7, 2018

@eskatos
MemberAuthor

@JLLeitschuh kotlin prefers original members over extensions

@jnizet types need to expose Property<T> JavaBean properties otherwise it means giving up on lazy configuration

mkobit

mkobit commented on Sep 7, 2018

@mkobit
Contributor

I wonder if setter overloading (KT-4075) or setter-only properties (KT-6519) would allow something like this to work? Seems like there might be a few missing Kotlin feautre requests (like how do you have setters for a val?), but maybe something that looks like this?

open class MyTask : DefaultTask() {
  val myProp: Property<String> = project.objects.property()
  // ignore that this is val for sake of example
    set(value: String) {
      myProp.set(value)
    }
    set(value: Provider<out String) {
      myProp.set(value)
    }
}
task<MyTask>("myTask") {
  myProp = "value"
  myProp = provider { "value" } 
}

Maybe accessor generation could generate these setters?

Just brainstorming if there are other options.

JLLeitschuh

JLLeitschuh commented on Sep 7, 2018

@JLLeitschuh
Contributor

@mkobit That's a lot of overhead to put on plugin authors for ever field on a task they write.

StefMa

StefMa commented on Sep 9, 2018

@StefMa
Contributor

To be honest I don't get the current "issue" here.
I mean - when a Kotlin developer take a look to the current javadoc they see that they have to call set and don't "assign".

The only thing why something should changed here is that the Groovy DSL and the Kotlin DSL should look similar, right?
But none of your options can solve that.
From my point view nothing should be changed here. Kotlin is not Groovy (for good reasons). So the API is slightly different. No need to adjust Gradle/Kotlin here to be similar to Groovy.

Just my 2 cents 🙃

passsy

passsy commented on Nov 2, 2018

@passsy

I'm one of the developers running into this trap when I used the new MavenPom DSL using Property<T>. I first implemented it in Groovy and then tried to use it in Kotlin in a different project and failed hard. It took me hours to find the .set(T). Using it is a dissatisfying experience.

I don't see a problem here for Plugin authors. When I develop a DSL I use var name: String? = null and it can be used the same way from kotlin and groovy.

The problem is that DSLs in gradle core uses the Property API which makes DSL inconsistent. Sometimes = can be used (i.e. in android DSL), sometimes .set(T) is required.

Except for Option 1: Kotlin supports equals = operator overloading which would restore a consistent DSL experience Option 2 and Option 3 also break consistency.

The Property<T> API was introduced because properties can now be lazily evaluated. That's a cool feature but often not required. I'm really unhappy that this changes how DSLs are used. As a plugin author, I'd much rather provide an alternative API for lazy properties.

// non-lazy property, can be used with `=` in kotlin DSL 
var name: String? = null

// lazy property via closure
fun name(block: () -> String?)
JLLeitschuh

JLLeitschuh commented on Nov 2, 2018

@JLLeitschuh
Contributor

The Property API was introduced because properties can now be lazily evaluated. That's a cool feature but often not required.

Actually, it is required to fix a lot of issues with task evaluation ordering that has plagued Gradle for a long time. The goal is to provide a mechanism where plugin authors do not have to utilize the Project::afterEvaluate method.

eskatos

eskatos commented on Nov 6, 2018

@eskatos
MemberAuthor

The position of the @gradle/kotlin-dsl team is to advocate for Option 2 with minor changes.

We think that it's important to distinguish Property<T> plain value usage and provider wiring because the latter implies later side effects.

For that reason and increased readability we suggest the following:

  • Introduce a value: T? property i.e. @Nullable T getValue() / void setValue(@Nullable T)
  • Deprecate @Nullable T getOrNull() in favor of @Nullable T getValue()
  • Deprecate void set(@Nullable T) in favor of void setValue(@Nullable T)
  • Introduce from(Provider<T>) for wiring
  • Deprecate void set(Provider<T>) in favor of void from(Provider<T>)

Kotlin DSL

extension {
    description.value = "My $project.description"
    description.from(otherExtension.description)
    description.from(provider {
        // compute stuff lazily
    })
}

Groovy DSL

extension {
    description.value = "My $project.description"
    description.from otherExtension.description
    description.from provider {
        // compute stuff lazily
    }

    // Additional existing Groovy DSL sugar
    description = "My $project.description"
    description = otherExtension.description
}

For trying it out

Put the following extensions in scope:

var <T> Property<T>.value: T?
    get() = getOrNull()
    set(value) = set(value)

fun <T> Property<T>.from(provider: Provider<T>) =
    set(provider)

Pros/Cons

  • (+) Simple, just add a few methods, deprecate old ones (doesn't require any compiler plugins, bytecode munging etc..)
  • (+) Explicit difference between using the plain value or connecting to other properties
  • (-/+) Will not look the same in Groovy and Kotlin DSL unless we remove sugar from Groovy DSL
  • (-) Deprecations might affect the perception of the Gradle API, changing too fast
lacasseio

lacasseio commented on Nov 6, 2018

@lacasseio
Contributor

I'm not sure I agree with removing the difference between getOrNull() and get() as it adds some intention behind the querying of the property/provider.

Could option 4 be a viable solution as well. I understand it doesn't feel property-ish in Java and Kotlin. I fear that option 2 will create 3 ways to understand the Provider API:

extension {
    // Valid in Groovy
    description = "some-value"

    // Valid in Groovy and Kotlin
    description.value = "some-value"

    // Valid in Java, Groovy and Kotlin
    description.setValue("some-value")
}

I think I would prefer to have the Groovy syntax sugar print a deprecation warning informing users how to properly use the Property object and pointing them to use set until Gradle 6.0 or further (if we don't convert the entire code base to use provider API by then). This way, we would have a single way of setting values for Property in all language:

extension {
    // Valid in Java, Groovy, and Kotlin
    description.set("some-value")
    //  Note: in Java is would be getDescription().set(...)

    // Normal groovy syntax sugar
    description.set "some-value"
}

What is your feeling on just removing (informing users) the Groovy syntax sugar and keeping everything as-is?

bamboo

bamboo commented on Nov 6, 2018

@bamboo
Member

I'm not sure I agree with removing the difference between getOrNull() and get()

That's not what's in the proposal. We propose to deprecate getOrNull() in favor of getValue() with the same signature and semantics.

get() remains unaffected by this proposal.

bamboo

bamboo commented on Nov 6, 2018

@bamboo
Member

What is your feeling on just removing (informing users) the Groovy syntax sugar and keeping everything as-is?

As stated, we think that it's important to distinguish Property plain value usage and provider wiring because the latter implies later side effects.

68 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

    Development

    Participants

    @bamboo@jamesward@adammurdoch@eskatos@ljacomet

    Issue actions

      Concise and statically-typed `Property<T>` assignment · Issue #9268 · gradle/gradle