Lazy binding and functional programming

Functional programming techniques can be used in conjunction with lazy binding to rather easily and compactly express the complex multi-valued dependencies we require.

As an example, let's consider the humble HBox, a node which performs a simple horizontal layout of the nodes it contains:

public class HBox extends CustomNode {
    public var content: Node[];
    public var spacing: Number;

    bound lazy function layout(nodes:Node[], x:Number):Node[] {
        if (nodes == [])
        then []
        else {
           def theNextToLayout = nodes[0];
           def theRestToLayout = nodes[1..];
           [Group {content: theNextToLayout, x: bind lazy x},
            layout(theRestToLayout, x + spacing + theNextToLayout.bounds.width)]
	}
    }

    override var internalContent = Group {
         content: bind lazy layout(content, 0);
    }

}

In our system CustomNode is defined like this:


public abstract class CustomNode extends Node {

    protected var internalContent: Node on replace { internalContent.parentNode = this };

    override var contentBounds = bind lazy internalContent.bounds;
    
    ...
}

Concrete subclasses of CustomNode are required to override its protected internalContent variable for their specific content, and CustomNode's contentBounds is defined as the bounds of its internal content.

HBox declares two public variables, "content", which is the sequence of nodes it will contain, and "spacing" which defines an additional uniform distance used to separate them. It overrides its inherited "internalContent" variable to consist of a Group containing a sequence of auxiliary nodes used to perform the layout, which are are set up in the recursive lazy bound function "layout".

The location on the x axis of a given child of HBox depends on the spacing and on the combined widths of all the nodes that precede it. All of these values can be directly or indirectly animated.

The layout function takes two parameters, a list of nodes on which to perform the layout, and an accumulated displacement along the x axis. Each time it's called, it wraps the first node in the list in a Group whose x coordinate is bound to the accumulated displacement, and then (lazily) calls itself with the remainder of the list, adding the spacing and the width of that node to the accumulator (note: because this is a bound function this expression is also bound). The layout group of the first node is then concatenated with the result of laying out the rest and returned. Note that calling a lazy bound function is quite unlike calling an unbound JavaFX function or a Java method. Basically all it does at the time of the call is build an unevaluated dependency tree.

The end result of this is that I can insert/delete/replace nodes in HBox.content, animate their transforms or other characteristics which affect their ultimate width, or animate the spacing, and the layout will be correctly recomputed - but lazily only when required.

Here's a simple example, which creates an HBox containing a variable number of spheres. The scale of the contained spheres is animated, as well as the count and the spacing.



def SPHERE_N = 30;
var count: Integer = 10;
var spacing: Number = 1;
var s: Number = 1.0;
var color: Color;

def shader = FixedFunctionShader {
    diffuse: bind lazy color;
}

def t = Timeline {
    keyFrames:
    [KeyFrame {
       time: 0s;
       values:
       [color => BLUE,
        s => 1.0,
        spacing => 1.0,
        count => 10]
    },
    KeyFrame {
        time: 5s;
        values:
        [color => RED tween LINEAR,
         s => 3.0 tween LINEAR,
         spacing => 5.0 tween LINEAR,
         count => SPHERE_N tween LINEAR]
    }]
    autoplay: true;
    autoReverse: true;
    repeatCount: Timeline.INDEFINITE;
}

def spheres = bind lazy for (i in [1..SPHERE_N]) {
    Sphere {
        radius: 1;
        transform: bind lazy scale(s, s, s);
        shader: shader;
    }
}

Stage {

   scene: Scene {
        content:
            HBox {
                spacing: bind lazy spacing;
                content: bind lazy spheres[0..<count];
            }
    }
}

This approach eliminates the need for any special procedural "layout" pass or protocol. Accessing any variable which depends on the layout (for example the bounds of the HBox itself, the parent transform or world transform of any of its contained nodes, etc), will implicitly evaluate the bindings woven together in our "layout" function as required, thus effecting the layout.

This technique is quite general, and is used throughout, for example in the case of aim, parent, orient, and point transform constraints. Here's part of the implementation of "point constraint". A point (or location) constraint is an operation which positions a node based on the weighted locations of a set of other nodes. For example, "position node A halfway between node B and node C".


 public class Constraint {
     public var node: Node;
     public var weight: Number = 1.0;
 }

 ...

 bound lazy function pointConstraint(constraints:Constraint[], translation:Vec3, weight:Number):Vec3 {
        
        if (constraints == []) {
           if (weight == 0) then Vec3.ZERO else translation / weight;
        } else {
            def c = constraints[0];
            def cs = constraints[1..];
            def location = c.node.worldTransform.getTranslation();
            pointConstraint(cs,
                            translation + location \* c.weight,
                            weight + c.weight)
        }
    }

 bound lazy function pointConstraint(constraints:Constraint[]):Vec3 {
     pointConstraint(constraints, Vec3.ZER0, 0);
 }
 ...

Here's an example of the how the above mentioned constraint might be expressed:

  var B:Node = ...;
  var C:Node = ...;
  def c1 = Constraint { node: B, weight 0.5 };
  def c2 = Constraint { node: C, weight: 0.5 }; 
  def transform = bind lazy translate(pointConstraint([c1, c2]));
  def A = Cube { transform: bind lazy transform, ... };
Note that the locations of the nodes and/or the weights associated with the constraints may be animated or otherwise change. The lazy bound recursive function "pointConstraint" above receives two accumulated values as it iterates the list of constraints, the accumulated translation and the accumulated weight. When the end of the list is reached the result is produced by dividing the total displacement (translation) by the total weight.

Here's a very simple test case of a point constraint imported from Maya. The red sphere is point constrained to the 4 yellow cubes. The test animates the position of the sphere around and among the cubes by simply animating the weights of the 4 constraints.

Comments:

Chris -

This is a great description.

We use a similar system in our Java apps, although we had to implement our dependancy-tracking binding system to do it. I look forward to being able to leverage JavaFX bindings next time.

Can I have a little more clarification on when the final result gets evaluated?

Take the case where you change a bunch of parameters on a node. I can think of three ways to cause this node to rerender:

1) Fire off a change notice each time a property is changed, and rerender the node each time this notice is received. Naive and expensive.

2) Wrap property change notifications inside an editing session, suppressing all change notices until the session edit is completed and firing off a single aggregate notice at the end of the session. This gets messy because you forget to close the editing session.

3) Don't fire notices outside the bound system and rely on a disconnected polling system.

It sounds like you are using method 3. Is that accurate?

Does JavaFX binding have any support for method 2?

Thanks,
Willis Morse

Posted by Willis Morse on July 23, 2009 at 08:01 AM PDT #

Yes, the rendering loop is a polling mechanism by definition - which blocks on vertical sync. Nevertheless, it is important to be able to detect that no rendering of a new frame is required - in which case you can skip swapping the frame buffer and just sleep instead.

Posted by Christopher Oliver on July 23, 2009 at 10:20 AM PDT #

In my opinion, the same approach can be used for enterprise software tools integrated with Web Services as in Sun Java CAPS. Note that as above nobody is going to say "Hey, here you go, why don't you test your enterprise tools on my enterprise". Instead in each case we need to simulate the enterprise software problem that our tool is supposed to solve. http://www.aygulum.net
http://sohbetcide.com

Posted by Chat on June 17, 2010 at 09:06 AM PDT #

thank you very much In my opinion, the same approach can be used for enterprise software tools integrated with Web Services as in Sun Java CAPS. Note that as above nobody is going to say http://www.parcatlkontor.com

Posted by parça kontör on October 20, 2010 at 01:37 AM PDT #

In my opinion, the same approach can be used for enterprise software tools integrated with Web Services as in Sun Java CAPS.hele yar zalım yar. http://www.memleketchat.com

Posted by Chat on November 01, 2010 at 03:13 PM PDT #

Note that as above nobody is going to say "Hey, here you go, why don't you test your enterprise tools on my enterprise". teşekkürler

Posted by konyachat on November 01, 2010 at 03:13 PM PDT #

Instead in each case we need to simulate the enterprise software problem that our tool is supposed to solve. http://www.askizaman.com

Posted by chat siteleri on November 01, 2010 at 03:14 PM PDT #

important and awake! great job bro thanks.

Posted by Egitim on December 11, 2010 at 05:50 AM PST #

Simple and Nice example !

Posted by شات on December 15, 2010 at 03:45 AM PST #

Instead in each case we need to simulate the enterprise software problem that our tool is supposed to solve

Posted by muhabbet on December 17, 2010 at 05:00 AM PST #

Post a Comment:
  • HTML Syntax: NOT allowed
About

user12610627

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today