Spec-Zone .ru
спецификации, руководства, описания, API

Bind Update

What does "bind" mean?

When you write a bound expression:

def x = bind someExpression; 

it means that when someExpression changes, x will be updated to match. That's it. In most cases that is all you need know. But in some cases you need to know how exactly the update occurs and in some cases, what is meant by someExpression changing. These are discussed below.

What is recalculated on update?

Within a bind, the minimal recalculation is done -- this only matters in limited circumstances, for example when object creation is done in a bind and if, because of object identity, it matters if a new object is created or not.

Let's look at an example of bind:

def sum = bind expr1 + expr2; 

if the value of expr2 changes then the addition is redone but expr1 is not recalculated, its value has been stored and it is simply re-fetched.

Let's make this concrete:

var y = 3; 
function ten() : Integer { 
   println("Called ten");
   10 
} 
def sum = bind ten() + y; 
println(sum); 
y = 7; 
println(sum); 

This prints:

Called ten
13 
17 

Note that the function ten() is called to compute the initial value of sum, but when y is set to 7, the function ten() is not called again (since it didn't change), its value has been remembered and reused.

Conditional Expressions

def x = bind if (condExpr) expr1 else expr2;

if the value of condExpr changes, this switches which branch of the if-statement is to be evaluated thus causing a recalculation each time the value of condExpr changes (the previous value of a branch is not stored). If condExpr is true, a change to the dependencies of expr1 will cause it to be recalculated, but this will not cause a calculation of expr2 nor will changes to the dependencies of expr2. Specifically, if condExpr is true, only expr1 will calculated; expr2 will not be. The inverse is, of course, also true.

For Expressions

def newSeq = bind for (elem in seq) expr;

If seq changes, the elements in newSeq which corresponded to elements still in seq are not recalculated. That is, if an element is inserted into seq, the result of applying expr to that element are inserted into newSeq at the corresponding position and the other elements are not recalculated. Well, OK, there is an exception to that rule, if expr uses indexof elem then those elements whose index changed will need to be updated, but again, corresponding to the minimal update rules. For example:

var min = 0;
var max = 3;
function square(x : Integer) : Integer { x*x }
def values = bind for (x in [min..max]) square(x);  1
println(values);
max = 5;  2
println(values);
min = 1;  3
println(values);
min = 0;  4
println(values);

The output is:

[ 0, 1, 4, 9 ]  1
[ 0, 1, 4, 9, 16, 25 ]  2
[ 1, 4, 9, 16, 25 ]  3
[ 0, 1, 4, 9, 16, 25 ]  4

But what about recalculations?

1

first the squares of 0 through 3 are calculated

2

then the squares of 4 and 5 (0 through 3 are not recalculated when the max changes)

3

then the square of zero is deleted (without recalculating any values)

4

and then the square of zero is added back (this does require it to be recalculated)

The behavior is the same if insert and delete are used.

[To do: indexof]

[To do: add on-replace clause example]

Block Expressions

A block expression is a list of expressions enclosed in curly braces. The value of a block expression is the value of the final expression. Within a bind, the only expressions which can occur in the non-final position of a block-expression are variable definitions (def). Note: some early version of the language may allow var, but that this will be disallowed. Note also that assignment (including increment and decrement) are prohibited within bind. Thus a bound block-expression has the form:

bind { 
   def a = expr;  
   def b = expr;  
   def c = expr;  
   expr 
} 

Because any changes to the bound expression cause an update, and because that update is minimal, it is easy to see that the variables are effectively bound.

We can deduce that while, insert, delete, etc cannot occur in a bound block expression; they can not occur in the non-final positions since they are not variable declarations; and they have Void type, thus have no value and cannot be bound; and thus cannot occur in the final position.

Function and Method Calls

def val = bind foo(a, b);

A non-bound function is one that is not proceeded with the bound keyword. For calls to JavaTM methods or non-bound JavaFXTM functions, the function is re-invoked if any of the arguments change. However, the body of a function is a black-box, dependencies it might have beyond the parameters do not cause a recalculation. For example:

class Point {
  var x : Number;
  var y : Number;
}

var scale = 1.0;
function makePoint(x0 : Number, y0 : Number) : Point {
  Point {
    x: x0 * scale
    y: y0 * scale
  }
}

var myX = 3.0;
var myY = 3.0;
def pt = bind makePoint(myX, myY);
println(pt.x);
myX = 10.0;   1
println(pt.x);
scale = 2.0;  2
println(pt.x);

Will print:

3.0 
10.0   1
10.0   2

1

Changing the argument myX causes makePoint to be called again.

2

But, the function makePoint is a black-box. The change to scale won't cause an update. That's where bound functions come in.

Bound Functions

Bound functions have as their body a block-expression which is bound (it thus has the above restrictions on bound block-expressions). When binding to a bound function, changes to values in the body expression, not limited to the arguments, cause updates, and argument changes are seen by the function. So, if the above function makePoint were instead a bound function:

bound function makePoint(x0 : Number, y0 : Number) : Point { ...

The scale change would cause an update (20.0). Note also, that if myX changed, only x0 * scale would be recalculated, not y0 * scale.

Calling a bound function from outside a bind is just like calling a non-bound function.

Object Literals

Object literals behave like simple operators (+, etc) and non-bound functions. That is, if one of the arguments to the object literal changes, then it is re-executed (a new instance is created).

def pt = bind Point { 
   x: myX
   y: myY  
}

if myX changes a new Point object is built -- this is exactly what you want for immutable objects.

What if you want the value of x to track the value of myX, without creating a new Point? Then you bind the instance variable initializers:

def pt = bind Point { 
   x: bind myX
   y: myY  
}

Now if myX changes, the x instance variable of the point pt will be updated, but a new Point will not be created because the object literal's instance variable initializer hasn't changed (x is still, and will always be, bound to myX). Changes to myY will still cause a new Point to be created.

So, what you would probably want for this example would be for the point to track myX and myY:

def pt = Point { 
   x: bind myX
   y: bind myY  
}

Here pt would always remain the same Point instance. Note that there is no longer a need for bind on the initializing expression of pt since there are no dependencies.