eclipse-archived / ceylon

The Ceylon compiler, language module, and command line tools
http://ceylon-lang.org
Apache License 2.0
396 stars 62 forks source link

Function literals with receiver #6520

Open ckulenkampff opened 7 years ago

ckulenkampff commented 7 years ago

See Kotlin language reference for an explanation of the concept.

A syntax for specifying such functions could also provide the basis for extension methods (#4252).

Functions with receiver objects could make the declarative object instantiation syntax even more fluent.

arseniiv commented 7 years ago

Allow me to state some observation from outside here. I see two pretty unrelated features together: possibility (or is it necessity?) of extension-method syntax for a function, and a special treatment of one of its arguments in its body. For example, C# feels reasonably good with the first and without the second.

And, as I can deduce, a contribution of the second feature to the declarative object instantiation syntax is based on some other Kotlin syntaxes for one-argument function call and/or a lambda without (ordinary) arguments, which Ceylon doesn’t have: argument-less lambda would be regular () => expr or () { body }, and one-argument function can only be called as any other one, either. Emulating Kotlin syntax thus would produce something with double braces or braces within parentheses.

Plus the first feature doesn’t contribute to the latter syntax at all even in Kotlin—it is used inside the definition of such a “builder function” only.

ckulenkampff commented 7 years ago

Ceylon doesn’t have: argument-less lambda

This is a very good point. As you mentioned short but still not satisfying way to handle this might be like this:

void html(void HTML.build()) {
    build(new HTML());
}

html(HTML.(){
    body();
});

So to make it easier Ceylon would have to deduce the type of the anonymous no-argument function. Either it's a no-argument function literal with receiver or a normal no-argument function literal.

Then we could write it like this:

html((){
    body();
});

Still like you said with many braces. This is certainly not nearly as fluent as in Groovy or Kotlin.

I hoped for a way to emulate Gradle-like configuration with this. The last syntax might work, since at least you know what's happening and it's still relatively short...

gavinking commented 7 years ago

@ckulenkampff you have not explained what it is that you're trying to do that can't already be done in Ceylon.

ckulenkampff commented 7 years ago

I learned to love the syntax of "configuration methods" in Groovy when using Gradle. Kotlin provides a similar feeling with their function literals with receiver. I would like to have similar syntax sugar in Ceylon.

The whole Gradle API works this way and Kotlin works so well as Groovy replacement because it enables writing interactions with the API in a similar way:

Nonsense Gradle Groovy Sample:

plugins {
  id 'application'
}
allprojects {
  group = "org.gradle.script.groovy.sample"
  version = "1.0"
  repositories {
    mavenCentral()
  }
  dependencies {
    testCompile 'junit:junit:4.12'
  }
}
mainClassName = "samples.HelloWorldKt"

Sample gradle-script-kotlin equivalent:

apply {
  plugin("kotlin")
  plugin<ApplicationPlugin>()
}
allprojects {
  group = "org.gradle.script.kotlin.sample"
  version = "1.0"
  repositories {
    mavenCentral()
  }
  dependencies {
    testCompile("junit:junit:4.12")
  }
}
configure<ApplicationPluginConvention> {
    mainClassName = "samples.HelloWorldKt"
}

Without function literals with receiver in Ceylon it could probably look like this:

project.apply((pluggable) {
  pluggable.plugin("ceylon")
  pluggable.plugin(`ApplicationPlugin`);
});
project.allprojects((project) {
  project.group = "org.gradle.script.ceylon.sample";
  project.version = "1.0";
  project.repositories((repositoryHandler) {
    mavenCentral(repositoryHandler);
  });
  project.dependencies((dependencyHandler) {
    dependencyHandler.add("testCompile","junit:junit:4.12");
  });
});
project.configure((ApplicationPluginConvention convention) {
  convention.mainClassName = "samples.HelloWorldCeylon";
});

I think the Ceylon example still somewhat relies on non-existing extension methods, at least the methods repositories and dependencies expect a Groovy closure as parameter instead of an Action. In Kotlin both methods are referring to an extension method, see here. Same is true for the configure method. Without those it might look like this:

project.apply((pluggable) {
  pluggable.plugin("ceylon")
  pluggable.plugin(`ApplicationPlugin`);
});
project.allprojects((project) {
  project.group = "org.gradle.script.ceylon.sample";
  project.version = "1.0";
  repositories(project, (repositoryHandler) {
    mavenCentral(repositoryHandler);
  });
  dependencies(project, (dependencyHandler) {
    dependencyHandler.add("testCompile","junit:junit:4.12");
  });
});
configure(project, (ApplicationPluginConvention convention) {
  convention.mainClassName = "samples.HelloWorldCeylon";
});
gavinking commented 7 years ago

@ckulenkampff AFAICT these examples can already be written in Ceylon using an API based around named argument invocations. It's just a different solution to the same problem. I'm not seeing anything here that we can't already do.

ckulenkampff commented 7 years ago

@gavinking I am not sure how to create such a fluent API so easy with named argument invocations. One would always have to repeat all public members of a class as arguments within the declaration of the "configuration function". With function literals with receiver all possible "arguments" are defined by the type that receives the call. But on the other hand, similar to with-blocks, changing the receiver of a code block can be very confusing - so maybe it's good that Celyon passes on this feature.

gavinking commented 7 years ago

One would always have to repeat all public members of a class as arguments within the declaration of the "configuration function".

Or just instantiate the class directly, without the intermediate "configuration function".

changing the receiver of a code block can be very confusing - so maybe it's good that Celyon passes on this feature.

To be clear, I think it's a very confusing language feature.

ckulenkampff commented 7 years ago

Or just instantiate the class directly, without the intermediate "configuration function".

Yes. Still this is only possible when you create the object and when you don't want to apply the same configuration on other objects. For example set the property member of all objects a, b, c to "x" (Groovy, Gradle):

configure([a, b, c]) { member="x" }

In Groovy it is

[a, b, c].each { it.member="x" }

To be clear, I think it's a very confusing language feature.

I agree, as a Java programmer this was the aspect of Groovy/Gradle that took me the longest to grasp.