eclipse-archived / ceylon

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

factory functions for Java UI libraries #6368

Open FroMage opened 8 years ago

FroMage commented 8 years ago

Some UI libs, like Swing or Android, use property setters to build and don't have any builder/constructors for those, which makes them cumbersome to build.

We could propose compiler flags to auto-generate factory functions that would turn:

public class Foo extends View{
 public Foo(View parent){}
 public void setTitle(String title){}
 public void addChild(Child child){}
}

Into:

shared foo(View parent, String= title, {Child*} children){}

Then, title would be passed with the setter and children would be passed with addChild. This is all compile-time magic.

We'd have to be able to configure which packages/classes get this feature activated because it is expensive otherwise. We'd also have to configure which method to generate variadics for (addChild for example). For Android, we'd also have to consider making parent optional and default to this IFF the caller is a subtype of View, or if there's such a subtype in scope. That's very special-casey.

gavinking commented 8 years ago

Perhaps we could use some sort of annotation on the module import to indicate that we want this functionality to be enabled.

lucaswerkmeister commented 8 years ago

Would this automagically pass the correct parent for the panes, labels and button?

value w = Window {
    Pane {
        Label { text = "foo bar"; },
        Button { text = "OK"; }
    },
    Pane { Label { text = "nothing to see here"; } }
};

This looks like a cyclic initialization problem, but since this isn’t actually one huge constructor call, I think it could be done (create the objects in the order Window-Pane-Label, then initialize and add them in reverse order).

FroMage commented 8 years ago

Perhaps we could use some sort of annotation on the module import to indicate that we want this functionality to be enabled.

No, it's per-package, with list of exceptions, and a way to specify things like how variadic maps to ViewGroup.addView or Container.add. It's either a set of flags, or a config file with lots of options.

FroMage commented 8 years ago

Would this automagically pass the correct parent for the panes, labels and button?

I suppose we could hard-code this strategy for Swing. I'd have to take a look. That's even more special-casey than for Android if that's really required.

gavinking commented 8 years ago

No, it's per-package, with list of exceptions, and a way to specify things like how variadic maps to ViewGroup.addView or Container.add. It's either a set of flags, or a config file with lots of options.

It's not clear to me why an annotation can't specify these things.

FroMage commented 8 years ago

I suppose it can be a set of annotations, yes.

lucaswerkmeister commented 8 years ago

But why would we make the user specify all that convoluted logic themselves, in annotations? Shouldn’t we produce something that, at least for the Android case, “just works”? (Or perhaps I’m misunderstanding your suggestion.)

FroMage commented 8 years ago

We can hard-code defaults for Android and Swing, but not for every other similar module.

gavinking commented 8 years ago

We can hard-code defaults for Android and Swing, but not for every other similar module.

Agreed.

quintesse commented 8 years ago

Isn't this something that could be turned into plugins? Then you could enable whatever one you want or create your own (possible defined as extensions to existing ones).

FroMage commented 8 years ago

Isn't this something that could be turned into plugins?

It requires changes in the model loader, model and backend. It's very pervasive. Hard to turn into a plugin :(

FroMage commented 8 years ago

I've stopped working on this proto since to be useful it depends on #1617 or #6189 which are for 1.3.

gavinking commented 8 years ago

Reassigning to 1.3.2, since @FroMage says it won't make it into 1.3.1.

lucono commented 7 years ago

We could propose compiler flags to auto-generate factory functions that would turn:

public class Foo extends View{
 public Foo(View parent){}
 public void setTitle(String title){}
 public void addChild(Child child){}
}

Into:

shared foo(View parent, String= title, {Child*} children){}

I think what might be more useful for addressing the problem (or a significant part of the pain point) in this issue would be a standard language-level feature that allows the setting of multiple properties at once on an object. This could also help to some extent with the stated situation in Android and Swing.

So for instance, any code like this:

value foo = Foo();
foo.propA = "propA";
foo.propB = "propB";
foo.propC = "propC";

could be replaced with something like the following (using a fictional syntax):

value foo = Foo();
foo.{ propA = "propA"; propB = "propB"; propC = "propC"; };

Another interesting extension might be to have this syntax return the object, so as to allow chaining, including with construction:

class Foo(String name) { /* ... */ }

value foo = Foo("bar").{ propA = "propA"; propB = "propB"; propC = "propC"; };

But regarding the Android UI problem, I think something more extensive, such as a focused library, is what would really be needed (which may be something that ends up coming from the community rather than from the language team).

luolong commented 7 years ago

Yeah, on the surface of it, I like @lucono's proposal.

Regarding special-case'ing Swing and Android, maybe instead of hacking this into the compiler, have a separate Ceylon modules with generated content from the source of the UI libraries instead of hacking up something that ends up complicating the compiler only to satisfy couple of usecases...

someth2say commented 7 years ago

@lucono's proposal somehow reminds me to chaining setters. In Java (if setters are chaining), you can write something like

Foo foo = new Foo("bar").setPropA("propA").setPropB("propB").setPropC("propC");

Like in Lombok, we can create an annotation/compiler flag that create those setters. I also thought about modifying (on demand) default behavior that returns the rhs when setting an attribute. But the resulting code looks ugly, so not the best approach:

val foo = (((Foo("bar").propA="propA").probB="propB").propC="propC");

@FroMage's proposal makes code cleaner, IMHO, but it raises a question: which fields should be included in the parameter list? Just attributes defined in the class? All of them? Attributes defined in the class and its parents? Only IFF on same module? Which ones may have default values? Like String= title Should all attributes be variable?

I don't think this can be easily resolved by the compiler.

gavinking commented 7 years ago

I think what might be more useful for addressing the problem (or a significant part of the pain point) in this issue would be a standard language-level feature that allows the setting of multiple properties at once on an object.

The reason I've argued against going down this path is that "setting of multiple properties at once on an object" isn't a useful feature in idiomatic ceylon code. It assumes that you have APIs made of objects with lots of mutable properties which is something we're trying to get away with in Ceylon. The equivalent idiom in Ceylon is a named argument invocation of an initializer or constructor.

FTR, very, very early versions of the Ceylon spec allowed you to do something like this with a named arguments-like syntax, but that facility was removed before we started implementing the language.

lucono commented 7 years ago

The reason I've argued against going down this path is that "setting of multiple properties at once on an object" isn't a useful feature in idiomatic ceylon code. It assumes that you have APIs made of objects with lots of mutable properties which is something we're trying to get away with in Ceylon. The equivalent idiom in Ceylon is a named argument invocation of an initializer or constructor.

I think this would be fine if Ceylon applications never have to depend on non-Ceylon libraries written in other JVM languages, especially Java where the use of property getter/setters is a common pattern for bean/model/data and many other types of classes.

But that's not the case (the Android and Swing UI problem in this issue being a good real-world example), and I'd argue that having a nice language-level construct that simplifies this would be a valuable addition for smooth, less painful interop, particularly with Java. Using compiler flags to accomplish that would feel hacky and a lot less natural.

Also, for generated factory functions, it would become very difficult for someone reading code that calls these functions (say reading on github, outside of an IDE) to easily associate the function call back to the actual underlying library methods (and their documentation) without first knowing about the compiler flag and the rules around the generation of the factory functions.

On the other hand, with a language construct that allows setting of several methods at once (or other language-level solution to this problem), then from reading Ceylon code that uses that construct (and as someone who is knowledgeable in the Ceylon language) I can immediately associate them with the setter methods documented in the Android docs. But if I'm reading Ceylon code that calls generated factory functions, my first intuition would be to look for the factory method in the Android docs, which would be non-existent. On the other hand, if there was instead a Ceylon library for Android, then the library would have real methods that are documented as part of the library.

Also, if we use the compiler flag, and decide in the future to change when/which methods or other rules surrounding the generation of the factory functions, could there be a backward compatibility concern at that point? Due to those factory functions being generated code, would any code that relies on them be hard-bound to the compiler flag always being available and always working the same way?

lucono commented 6 years ago

Dart's cascade notation provides a concise syntax for easily setting multiple properties on an object at once, also returning the original object at the end of the chain. This is very similar to the proposal above for being able to represent the following code:

value foo = Foo("bar");
foo.propA = "propA";
foo.propB = "propB";
foo.propC = "propC";

In this fictional proposed form:

value foo = Foo("bar").{
    propA = "propA";
    propB = "propB";
    propC = "propC";
};

One difference in Dart's implementation is that in addition to setting properties, they also allow invoking other (non-setter) methods of the object.

I don't particularly like Dart's use of a double-dot (..) for their implementation of it, as it looks a little noisy. But regardless of syntax, I think this feature would be a really great addition to Ceylon.

xkr47 commented 6 years ago

I think @lucono has valid points with regards to readabilty. His proposal also allows for smooth updating of already instantiated objects. I would find that beneficial in many situations.

However, this also brings to memory the with keyword in the Javascript language which didn't fare so well (see e.g. https://stackoverflow.com/questions/1931186/with-keyword-in-javascript#1931195). But I think the static typing in Ceylon (or Java for that matter) would probably avoid most problems that the with keyword had,