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

properties or attribute setter references #3791

Open CeylonMigrationBot opened 11 years ago

CeylonMigrationBot commented 11 years ago

[@gavinking] We've discussed this many times before. Do we need a way to get a reference to an attribute that you can invoke to get/set it. This is very useful for UI code, where you would be able to just easily attach a textbox to a String attribute, for example.

Proposed solution:

//for instance members
String(String=) nameAtt = person.@name;
String name = nameAtt();   //get the name
nameAtt("New name");   //set the name

//also for static refs
String(String=)(Person) nameAtt = Person.@name;
String name = nameAtt(person)();   //get the name
nameAtt(person)("New name");   //set the name

Thoughts?

P.S. Note if name were non-variable, then the type of the function would be just String().

[Migrated from ceylon/ceylon-spec#685]

CeylonMigrationBot commented 11 years ago

[@gavinking] Now, here's the reason we maybe don't need this. We can already write:

Anything(String)(Person) setName = `Person.name`.set;
String()(Person) getName = `Person.name`.get;

However, I'm inclined to think it would be a bad idea to make this functionality depend on the metamodel stuff.

CeylonMigrationBot commented 11 years ago

[@FroMage] We can't use String= for attributes of type String?, we will need to use something like String|DoNotSet= or something.

CeylonMigrationBot commented 11 years ago

[@FroMage] Note that if Declaration does not work out for declaration literals, I reserve the right to claim @ for declaration literals, with @Declaration, Declaration@member and @toplevel. Though I'd prefer something closer to the syntax for type literals if possible.

CeylonMigrationBot commented 11 years ago

[@gavinking]

We can't use String= for attributes of type String?, we will need to use something like String|DoNotSet= or something.

Why? Sure we can...

CeylonMigrationBot commented 11 years ago

[@tombentley] I think @FroMage's point is "what's the value of the defaulted argument?" For a String value we'd like to say the function type is String(String?=) with a null argument meaning get the value, and a non-null argument meaning set the value. But that's ambiguous in the case of a String?-typed value.

CeylonMigrationBot commented 11 years ago

[@gavinking] Yeah but I can hack this. You don't actually need to be able to write a function that does what this function does ;-)

CeylonMigrationBot commented 11 years ago

[@gavinking] It would be like flatten(). You can't implement flatten() in Ceylon, but you can write down its signature.

Sent from my iPhone

On 14/08/2013, at 2:59 PM, Tom Bentley notifications@github.com wrote:

I think @FroMage's point is "what's the value of the defaulted argument?" For a String value we'd like to say the function type is String(String?=) with a null argument meaning get the value, and a non-null argument meaning set the value. But that's ambiguous in the case of a String?-typed value.

— Reply to this email directly or view it on GitHub.

CeylonMigrationBot commented 11 years ago

[@gavinking] Rm, that's not quite right. In this case you can only write down its type as a value, not its signature as a function. Slight difference.

Sent from my iPhone

On 14/08/2013, at 2:59 PM, Tom Bentley notifications@github.com wrote:

I think @FroMage's point is "what's the value of the defaulted argument?" For a String value we'd like to say the function type is String(String?=) with a null argument meaning get the value, and a non-null argument meaning set the value. But that's ambiguous in the case of a String?-typed value.

— Reply to this email directly or view it on GitHub.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] Perhaps I can interject here (much later) with the notions of Lenses and Lens Families? They seem to be exactly what you're looking for in this discussion.

CeylonMigrationBot commented 10 years ago

[@gavinking] So this idiom is not too bad, better than I expected:

variable value prop = true;

value checkBox = CheckBox { 
    () => prop;
    (Boolean checked) => prop=checked; 
};

checkBox.click();
print(prop);
checkBox.click();
print(prop);
CeylonMigrationBot commented 10 years ago

[@gavinking]

Perhaps I can interject here (much later) with the notions of Lenses and Lens Families? They seem to be exactly what you're looking for in this discussion.

@pthariensflame Well lenses give you a way to compose and abstract over attributes. All this issue is about is having a syntax to quickly reference an attribute. So not really the same issue.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking Ok, but what if the syntax discussed was implemented around lenses? By that I mean there would be two pieces to the syntax: one that would automatically produce a lens from an attribute (the "attribute literal" syntax), and one that would allow you to use lenses as attributes (the "attribute variable" syntax). This way, you have a way to use arbitrary lenses, including custom ones, as attributes, which would be a huge syntactic boon.

CeylonMigrationBot commented 10 years ago

[@gavinking] @pthariensflame I have not researched lenses in detail, but I assume there's no problem representing them within our type system. Or is there a requirement for higher-order generics in there somewhere?

one that would automatically produce a lens from an attribute (the "attribute literal" syntax)

Well we could I suppose make @foo produce an instance of Lens. OTOH, I it's almost as easy to write Lens(@foo) and that way avoid having to build lens machinery into the language module.

one that would allow you to use lenses as attributes (the "attribute variable" syntax).

This is something that's we would need to be careful about. It goes against the grain of the language to do things that are effectively implicit type conversions.

But I suppose something like the following syntax could be reasonable:

variable String personName;
@personName = @person.name;

Or:

class TextField(@text) {
    shared variable String text;
}

TextField(@person.name)

Again, that would not need to be a feature of Lens. Instead, a library could provide Lenses and you could write TextField(lens.attribute).

CeylonMigrationBot commented 10 years ago

[@gavinking] Actually, @pthariensflame is there really even a need for a special Lens type? Can't the interesting operations of lenses be defined to work against whatever type the language module supplies for representing attribute references?

For example, couldn't the "lens" type just be Type(Type=) or am I missing something?

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking The lens type itself could be called whatever, and if your attribute reference type supports all the lens operations (and follows the laws), then it's a lens type, even if it's not called that. There wouldn't need to be anything special; you'd just need a type with set and get abstract (with an appropriate contract for implementing them), and them compose and all its relatives could be implemented in terms of them. The idea is really just for whatever type you use to support the lens operations (and laws) in the first place, because then you get everything else for free.

So, yes, what I'm suggesting is close to what you were already wanting to do. I just wanted it to be recognized for what it is (and have that recognition maybe drive some additional thought about what is possible with this syntax). This is, already, a syntax for (specialized) lenses. All I'm suggesting is that the syntax be split in half, so to speak, so that users can use the attribute references directly as the lenses that they already actually are.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking Oh, and there's no problem representing them in your type system. A Lens<Outer,Inner> is just a [Inner, Outer(Inner)](Outer) (with an appropriate contract).

CeylonMigrationBot commented 10 years ago

[@gavinking] After writing the above, I realized how stupid it was: a lens isn't a member reference like person.@name, it's a static reference like Person.@name, so the type would be, for example, String(String=)(Person). Given that, "lens" composition is pretty trivial:

X(X=) composeAttributes<X,Y,Z>(X(X=)(Y) x, Y(Y=)(Z) y)(Z z) 
        => compose(x,y(z))();

I don't know enough about lenses to know what other operations are considered fundamental here.

CeylonMigrationBot commented 10 years ago

[@gavinking] @pthariensflame Good, this is more or less what I figured. (Our posts crossed.)

CeylonMigrationBot commented 10 years ago

[@gavinking] P.S. If it were a function returning a pair, as you suggest, it would have to be [Inner(), Anything(Inner)](Outer) because our tuples are immutable.

But I'm still inclined to simplify that to Inner(Inner=)(Outer).

CeylonMigrationBot commented 10 years ago

[@gavinking] Oh, and on a tangent, in response to the objection raised by @FroMage and @quintesse above, it turns out I can indeed define @ as a syntax sugar for constructs we already have. In particular, @name means:

flatten(([String=] arg) { 
    switch (arg) 
    case (is []) { 
        return name;
    } 
    case (is [String]) { 
        return name=arg[0]; 
    } 
})

Which is rather satisfying, I think.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking Great! I keep forgetting that the lenses I'm used to are for copy-and-modifying immutable objects, rather than directly changing mutable objects. Either way, though, they are easily composable, as you demonstrated. :)

CeylonMigrationBot commented 10 years ago

[@gavinking] Ooh ohh, I can even write composeAttributes() in pointfree form:

X(X=)(Z) composeAttributes<X,Y,Z>(X(X=)(Y) x,Y(Y=)(Z) y) 
        => compose(x,shuffle(y)());

Much better ;-)

CeylonMigrationBot commented 10 years ago

[@gavinking]

I keep forgetting that the lenses I'm used to are for copy-and-modifying immutable objects, rather than directly changing mutable objects.

Aaaah, of course, that's why you wrote Outer(Inner). Hrrrrm, ok let me stew on that for a bit. I wonder if it's worth trying to accommodate copyonwrite here.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking Regarding copy-on-write support, I think you could easily just have two different "attribute reference" types, perhaps implementing a (partially) common interface, that did either mutable or immutable lenses. The potential issue here is, of course, how to distinguish which one the user wanted when they write an "attribute literal".

Regarding other fundamental operations, they are all easily definable in terms of the core lens representation, like compose is:

There are others, but they tend to require more specialized circumstances, or else type-constructor polymorphism, and they are all much less used than the ones I've given. As always, renaming does nothing to their core form, and you may wish to for readability's sake. :)

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking The diagram here might help you get a sense of how deep the rabbit hole can go, but only if you let it.

CeylonMigrationBot commented 10 years ago

[@gavinking] @pthariensflame great, thanks, so if we make the alias:

alias Lens<Outer,Inner> => Inner(Inner=)(Outer); //or whatever we eventually decide

Then we would have:

Lens<Outer1|Outer2,Inner> merge<Outer1,Outer2,Inner>(Lens<Outer1,Inner> left, Lens<Outer2,Inner> right);
Lens<Outer,[Inner1,Inner2]> product<Outer,Inner1,Inner2>(Lens<Outer,Inner1> left, Lens<Outer,Inner2> right);

That's just exactly what you wrote, but I replaced that nasty Either with a union. Both those functions are trivial to implement. The purpose of these functions appears obvious.

I don't completely grok modify(). Is it just:

void modify(Lens<Outer,Inner> lens, Inner(Inner) fun, Outer instance)
        => lens(instance)(fun(lens(instance)());

If that's all it is it looks sorta trivial. I don't quite see what it buys me.

CeylonMigrationBot commented 10 years ago

[@gavinking]

If that's all it is it looks sorta trivial. I don't quite see what it buys me.

Oh, is it because it should be curried?

void modify(Lens<Outer,Inner> lens, Inner(Inner) fun)(Outer instance)
        => lens(instance)(fun(lens(instance)());

That looks more useful...

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking Yeah it should be curried, sorry. modify can be very useful for quickly and easily defining state-transition methods operating on a lens's target.

CeylonMigrationBot commented 10 years ago

[@gavinking] OK, thanks, then I think I get that much.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] If you have type-constructor polymorphism (and a notion of Functor), you can add an operation to that core list: modF. It's mod within an arbitrary user-chosen Functor, and it's actually general enough for every other possible operation on lenses to be able to be reimplemented solely in terms of modF (this is called the van Laarhoven or functorial representation, as opposed to the (immutable) one that I gave you earlier, which is the store-comonad representation):

// going with the immutable signature here, as I don't know what the mutable version might look like
// also, please excuse the hypothetical syntax here
Fn<Outer> modF<Fn<out>,Outer,Inner>(Lens<Outer,Inner> lens, Fn<Inner>(Inner) fun)(Outer instance) given is Functor Fn
        => fun(lens(instance)(1)).map(lens(instance)(2))

But, while incredibly powerful, modF isn't used much directly; more derived operations are often preferred. So it's no great loss if this isn't worth the type-level complexity. I can't think of any more actually fundamental operations; all the others are pretty specific regarding what the lens's source and target types need to be in order to work. Well, there is zoom (and it's cousins magnify and retract), but I don't think you'll find the State, Reader, or Writer monads critically useful in Ceylon any time soon. :)

CeylonMigrationBot commented 10 years ago

[@gavinking]

If you have type-constructor polymorphism (and a notion of Functor)

I implemented type constructor parameterization in a branch of the typechecker, mainly as a proof-of-concept, but we have not made a decision yet as to whether it really belongs in the language. I keep changing my mind about it.

I can't think of any more actually fundamental operations

OK, great, I really appreciate you walking me through this, it definitely gave me some insight I didn't have before.

Cheers!

but I don't think you'll find the State, Reader, or Writer monads critically useful in Ceylon any time soon. :)

Hehe. Agreed :)

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @gavinking Thanks! It's always a nice feeling to be told that your contributions were useful.

I can give you some of the most common "specialized" lenses, i.e., the ones that apply only to specific instantiations of Outer and Inner, but it might be better if that discussion were saved for dedicated issues regarding the specific type(s) in question, e.g., _Correspondence should have Lens<Correspondence<Key,Value>,Value> valueAt<Key,Value>(Key index)_ and things like that.

Of course, this is assuming that the design here doesn't change significantly after this. :)

CeylonMigrationBot commented 10 years ago

[@gavinking] What I'm wrestling with right now is how to accommodate non-variable attributes into this. I guess that's where copy-on-write really comes into the picture but honestly it's not like we have any machinery to make copyonwrite easy. Values in Ceylon aren't records; they have invariants which are supposed to be enforced by the initializer of the class. Copyonwrite is easy for stuff like tuples and I guess maps and lists, but for classes it's tricky.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] What if you could annotate specific attributes as copyable, the way you can annotate them as variable? We're still left with the problem of how the syntax varies for copy-on-write, if it does at all.

CeylonMigrationBot commented 10 years ago

[@gavinking] So after talking over with Julien, and considering what is needed for databinding in the JavaScript client side, I believe we do need something more than what is contemplated by the issue description. We'll also need the ability to register of set events on an attribute reference. Given that, it would be better to introduce a class or interface ValueBinding<Type> or whatever, with get(), set(), addListener(), and removeListener() functions.

So you would need to annotate an attribute or class observable, like so:

class Person(observable variable String name) {}

And then you could pass a reference to name, as an instance of ValueBinding<String>, like this:

Text { label="Name"; @text=person.@name; }

Where the Text input field is defined as so:

class Text(@text) satisfies Input {
    shared String text;
    @text.addListener(onUpdate);
    void onUpdateText(String text) {
        //redraw
        ...
    }
    ...
}

I'm quite concerned about the potential for memory leaks with this stuff. It might make it easier if wiring listeners was done declaratively with annotations:

class Text(@text) satisfies Input {
    shared String text;
    listening(`text`)
    void onUpdateText(String text) {
        //redraw
        ...
    }
    ...
}
CeylonMigrationBot commented 10 years ago

[@gavinking] Actually it might be better to just define @T to mean PropertyBinding<T>, resulting in this code:

Text { label="Name"; text=person.@name; }

class Text(@String text) satisfies Input {
    text.addListener(onUpdate);
    void onUpdateText(String text) {
        //redraw
        ...
    }
    ...
}

And then an expression like Person.name would have type @String(Person).

CeylonMigrationBot commented 10 years ago

[@pthariensflame] Sounds (relatively) good to me!

CeylonMigrationBot commented 10 years ago

[@gavinking] So here's a summary of what I'm thinking of at this point:

  1. All shared (and unshared toplevel?) values are considered properties.
  2. A property object is an instance of the class Property<T>, which may be abbreviated to @T.
  3. A property reference is an expression of form @v or x.@v which evaluates to a property object of type @T for the value or object attribute, and which has operations for setting and getting, and which maintains a list of listeners.
  4. A static property reference is an expression of form @X.v which evaluates to a function of type @T(X).
  5. The property object for a value or object attribute is instantiated lazily the first time a property reference for the value is evaluated.

So defining a property is just a matter of declaring a shared attribute, like this:

//define property using field 
class Person(name) {
    shared variable String name;
}

Or like this:

//define property using getter/setter 
class Person(parsedName) {
    ParsedName parsedName;
    shared String name=>parsedName.fullName;
    assign name=>parsedName.fullName=name;
}

A further possibility for the future would be to let you declare an attribute using a property reference, like this:

//define property using property alias 
class Person(parsedName) {
    ParsedName parsedName;
    shared @String @name = name.@fullName;
}

Now we can obtain references using a very reasonable syntax:

//obtain property reference
@String nameOfPerson = @person.name;

And even static references:

//obtain static property reference
@String(Person) nameOfPerson = @Person.name;

This is, of course, especially useful in UIs.

//use of property reference in UI
Span {
    Label(“Name”), 
    Text(person.@name)
}

class Text(text = DefaultProperty(“”)) {
    shared @String text;
    text.addListener(onChange);
    function onChange(String newText) {
        //refresh UI
        ...
    }
}

We can also define things that look like properties, but that aren't actually attributes:

@String fake = Property {
    ()=>”hello”;
    (String string) {};
};

or perhaps:

object fake
        extends Property<String>() {
    val=>”hello”;
    assign val {}
}

Static property refs can be composed as lenses:

@Country countryOfPerson = @Person.address.compose(@Address.country)

(And we can have merge() and product() functions as mentioned above.)

The big open question in my mind now is what to do about readonly properties. We need them, if we're to make composition useful. For example, I need to be able to write stuff like this:

@Person.addresses.compose(@List.first).compose(@Address.country)

Is it acceptable to say that the "set" operation for a readonly property is a noop?

CeylonMigrationBot commented 10 years ago

[@gavinking]

Is it acceptable to say that the "set" operation for a readonly property is a noop?

Or should we design this stuff to accommodate copy-on-write? That would be a significant escalation of the whole facility...

CeylonMigrationBot commented 10 years ago

[@vietj] my 2 euro cents

property navigability (composition)

it is essential for proper addressing complex cases, specially for templates in which bind property references and not instances. It looks like static property ref composition address this case.

multi valued properties

do you plan to address this case ? (list) or should it be handled by frameworks using collection wrappers.

read / covariance

I would extract a super class with a covariant Value for properties that allows to have only access to property read that would allow covariance, it can be useful. This interface could also define the listen-abilty, like

shared interface Observable<out Value> {
  shared formal Value get();
  shared formal void addListener(Anything(Observable) listener);
  shared formal void removeListener(Anything(Observable) listener);
}
CeylonMigrationBot commented 10 years ago

[@vietj]

computed

need also something for creating computed observable properties :

Observable(Person) property = @Person.name.compose((String s) => s.trimmed.size > 0);

or for several properties:

fullName = firstName + " " + lastName

it could be read write (by parsing the fullName and setting firstName / lastName)

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @vietj That last one might cause some problems; the compiler would have to have special information about the bijectivity (or lack thereof) of every operation, either by fiat or in some user-extensible way (I would obviously prefer the latter, but either option would cause some significant additional complexity that I don't think would be worth dealing with, at least for now).

EDIT: Actually, now that I think about it, if we decide to also have copy-on-write versions of all of this, then the problems I alluded to would already be resolved. So, in a way, that's yet another argument for including immutable lenses as well as mutable ones in our scheme.

CeylonMigrationBot commented 10 years ago

[@FroMage] I don't quite understand why we have mixed the issue of attribute references and interceptors? IMO catching value changes is intercepting, not references. We don't have anything similar with function references for example.

CeylonMigrationBot commented 10 years ago

[@pthariensflame] @FroMage I don't understand either; perhaps @gavinking could clarify his ideas on that for us?

CeylonMigrationBot commented 10 years ago

[@gavinking]

I don't quite understand why we have mixed the issue of attribute references and interceptors?

Because it was clear, after discussions with @vietj, that this was necessary for things like MVVM.

CeylonMigrationBot commented 10 years ago

[@FroMage] But why can't we separate both issues? I think having attribute references like we have method references is a must, and I also think having interceptors (method/attribute/class initialiser) is useful, but why merge the two issues/syntax?

CeylonMigrationBot commented 10 years ago

[@gavinking] @FroMage look at the example I gave above: when I pass a property ref to a UI element, the UI element needs to be able to be notified of changes to the property value. That's just how stuff works in traditional MVC (as opposed to 90s-style server-side "web MVC").

CeylonMigrationBot commented 10 years ago

[@gavinking]

That's just how stuff works in traditional MVC (as opposed to 90s-style server-side "web MVC").

I think the fashionable term for this is "reactive".

CeylonMigrationBot commented 10 years ago

[@FroMage] OK, but then shouldn't 1.add give you a Method<Integer,[Integer]> which would gain methods such as addInterceptor? Or should it be 1.@add? ATM 1.add will return a Callable<Integer,[Integer]>, which will be less useful than attribute references (properties by the new definition apparently), since we can't register interceptors to it.

The other questions are:

CeylonMigrationBot commented 10 years ago

[@gavinking]

ATM 1.add will return a Callable<Integer,[Integer]>, which will be less useful than attribute references

Potentially we could explore adding interception to Callable, though there is a little problem with that in that we've said that you can't get members of Callable off a method reference.

Or should it be 1.@add?

This might be reasonable. The natural type of this expression would be Property<Callable<Integer,[Integer]>>, that is, @<Integer(Integer)>, but perhaps we could enhance it with something extra for function interception.

do attributes have a single Property instance per containing instance? Or is it like method references and we get one new instance every time we do @attr?

According to the strawman proposal above:

The property object for a value or object attribute is instantiated lazily the first time a property reference for the value is evaluated.

So it would be one instance per containing instance. I think that's necessary because the Property maintains the list of listeners.

do those property listeners only listen to changes made through the property instance? Or does it also listen to changes made by accessing the attribute directly?

The point is that they notify their clients when the attribute is directly changed.

This would has performance impact in the order of that required by method interception and solutions similar to it involving indirection and/or INDY bytecodes.

Yes, according to the strawman proposal, it would add a if (property!=null && !property.listeners.empty) on every assignment to a shared attribute. I think this is an acceptable cost.