Open CeylonMigrationBot opened 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.
[@FroMage] We can't use String=
for attributes of type String?
, we will need to use something like String|DoNotSet=
or something.
[@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.
[@gavinking]
We can't use
String=
for attributes of typeString?
, we will need to use something likeString|DoNotSet=
or something.
Why? Sure we can...
[@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.
[@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 ;-)
[@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.
[@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.
[@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.
[@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);
[@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.
[@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.
[@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 Lens
es and you could write TextField(lens.attribute)
.
[@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?
[@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.
[@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).
[@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.
[@gavinking] @pthariensflame Good, this is more or less what I figured. (Our posts crossed.)
[@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)
.
[@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.
[@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. :)
[@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 ;-)
[@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.
[@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:
merge
(sometimes written as the operator |||
) takes two lenses and turns them into a lens out of a sum; its type is Lens<Either<Outer1,Outer2>,Inner>(Lens<Outer1,Inner>,Lens<Outer2,Inner>)
.cross
(sometimes written as the operator ***
) takes two lenses and turns them into a lens into a product; its type is Lens<Outer,[Inner1,Inner2]>(Lens<Outer,Inner1>,Lens<Outer,Inner2>)
.id
takes an object to itself, and is the mathematical identity of lens composition, but I'm having trouble imagining what the mutable version would look like; its (immutable) type is Lens<SomeType,SomeType>
.modify
takes a function, a lens, and an object, and uses the lens to alter the provided object in the way described by the function; its immutable type is Outer(Lens<Outer,Inner>,Inner(Inner),Outer)
and it's mutable type is void(Lens<Outer,Inner>,Inner(Inner),Outer)
.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. :)
[@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.
[@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.
[@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...
[@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.
[@gavinking] OK, thanks, then I think I get that much.
[@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. :)
[@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
, orWriter
monads critically useful in Ceylon any time soon. :)
Hehe. Agreed :)
[@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. :)
[@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.
[@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.
[@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
...
}
...
}
[@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)
.
[@pthariensflame] Sounds (relatively) good to me!
[@gavinking] So here's a summary of what I'm thinking of at this point:
Property<T>
, which may be abbreviated to @T
.@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.@X.v
which evaluates to a function of type @T(X)
.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?
[@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...
[@vietj] my 2 euro cents
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.
do you plan to address this case ? (list) or should it be handled by frameworks using collection wrappers.
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);
}
[@vietj]
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)
[@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.
[@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.
[@pthariensflame] @FroMage I don't understand either; perhaps @gavinking could clarify his ideas on that for us?
[@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.
[@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?
[@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").
[@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".
[@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:
Property
instance per containing instance? Or is it like method references and we get one new instance every time we do @attr
?[@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.
[@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:
Thoughts?
P.S. Note if
name
were non-variable
, then the type of the function would be justString()
.[Migrated from ceylon/ceylon-spec#685]