The Topher F3 Application

Introduction

This document discusses the Topher F3 demo based on the original Topher's Breakfast Cereal Character Guide web page as well as on its Simile application version. It's assumed that the reader is familiar with the Java programming language and has read the F3 Programming Language document.

It's also assumed that the reader has run the F3 Topher Webstart application.

The application's F3 source code files are:

Three Versions of the Topher Guide

Content in the original web page is static. This page has limited query capabalities as it allows link-based selection by brand only:

The Simile version is a purely client-side application written in a combination of Javascript and CSS. This version improves significantly over the original webpage by providing sophisticated query capabilities based on the notion of selecting one or more elements of four filters: brand, country, decade and character form. It also provides for user-defined ordering and grouping and boasts a pop-up window displaying the character's description as well as a timeline graph:

The F3 version mimics the Simile version but uses a richer Swing GUI that makes better use of window real estate by using tabs for filter criteria and combo boxes for sort criteria. Because this version is meant to illustrate F3 capabilities rather than fully supporting the guide, though, some features present in the Simile version (such as result grouping and description pop-up) have been omitted.

This application is the subject of this document and is analyzed from both the application user and the F3 programmer perspectives.

The User Perspective

In this section the Topher F3 application is presented in terms of its behavior and the tasks required for its use.

Four visual and functional areas are identified that make up the application window:

  1. The top title area: an informative title that contains copyright and credit notes.
  2. The Filter area where filter criteria are specified
  3. The result display area where results of the current query are displayed
  4. The sort area where ordering criteria are specified
The filter and sort areas are discussed in terms of their functionality and required user interaction.

The Programmer Perspective

This section constitutes the bulk of this document and is meant to dissect and provide a detailed analysis of the F3 implementation. Among others, the following F3 features are emphasized as they're especially exploited in the code:

This section begins by examining the simple CerealCharacter class and its relationships with the four enumerations used as filter criteria.

The DB class is then examined in detail as the application's workhorse providing support for generic queries on enumerations and dynamic ordering. This class illustrates the use of:

Finally, class ExhibitView is analyzed as the core class of the Topher application GUI. This class illustrates the use of:


The User Perspective

This section presents the Topher F3 application from the end user's perspective:

The Topher F3 Application

The Topher demo is an F3 application that displays characters used on cereal boxes for promotional purposes. The Topher website maintains over 900 images and detailed descriptions of such characters discriminated by:

The application's window is divided into four main areas:

  1. The top title area: an informative title that contains copyright and credit notes.
  2. The Filter area where filter criteria are specified
  3. The result display area where results of the current query are displayed
  4. The sort area where ordering criteria are specified

Of these four areas only the filter and sort ones have application functionality.

Filtering

Filtering is the act of restricting the number of characters displayed by specifying what values to select for any combination of the brand, country, decade and form properties.

The filter area is composed of four tabs -one per filter attribute- each of which displays the distinct values taken by the corresponding property:

When a filter criteria value is selected, the display area will show only the characters whose corresponding property value match that of the value selected:

When more than one filter criteria values are selected the results displayed correspond to the set union of such values:

It is possible to specify criteria for more than one property at a time. In this case, the result area will show only those characters that satisfy simultaneously the filter criteria specified for each property. The results displayed in this case correspond to the set intersection of the values returned by each individual query.

By convention, when no property values are selected then all characters for the corresponding property are returned. Thus, leaving all values unselected is equivalent to selecting them all.

Sorting

In addition to selecting what characters to display based on their property values it's also possible to order the results by up to five criteria:

where label corresponds to the character's name.

Thus, the above query can be ordered as shown to look like:

There are no provision to sort in descending order or to group results.

Note that filtering and sorting take place immediately after being specified. Thus, for example, when a brand is selected the display area will be automatically updated to show only characters in that brand without any intervening "execute button".

The F3 Programmer Perspective

This section presents the Topher application from the perspective of the F3 programmer. Emphasis is placed in illustrating features of the F3 language the are peculiar to it and especially those for which there is no Java equivalent.

The F3 Topher Application

From a programming perspective, the Topher application has two layers:

Both layers are examined in turn by presenting a detailed analysis of the language features used by the implementation.

This is a code-intense section that requires basic familiarity with the concepts and syntax of F3.

The CerealCharacter class

The subject of our example application is the CerealCharacter class:

The F3 definition for this class is:

public class CerealCharacter {
    public attribute type: String;
    public attribute label: String;
    public attribute cereal: String;
    public attribute brand: String;
    public attribute decade: String;
    public attribute country: String;
    public attribute form: String;
    public attribute image: String;
    public attribute url: String;
    public attribute text: String;
    public attribute thumbnail: String;
}

This is a rather trivial class dealt with primarily by the GUI portion of the application. Its attribute names are self-explanatory. The remainder of this section will cover the DB class used by the application to access the collection of CerealCharacters but which is totally agnostic with respect to the structure and semantics of any particular database relation.

The DB Class: An In-memory Database

Class DB is the workhorse of Topher application. This class contains data, filters, orderings and results of user-defined queries.

The definition of the DB class is:

public class DB {
    public attribute extent: **;
    public attribute filter: AttributeValueAssertion*;
    public attribute sortCriteria: Attribute*;
    public attribute selection: **;
    private function applyFilter(obj):Boolean;
}

The above attributes are defined as follows:

The applyFilter() private function is used to check whether a particular instance of the extent matches the current set of filter criteria.

The above defines a generic mechanism that can perform queries on any collection of F3 class instances. The use of untyped arrays and reflective attributes allows for any F3 class to be used as an extent. It also allows for the formulation of arbitrary query filters and sort criteria. There's a restriction, though, in that filters operate only on enumerated attributes such as brand or country; there are no provisions for queries based on comparisons, wildcards or their combinations.

The "Wildcard" Type

Note that both extent and selection are arrays of the wildcard type frequently referred to as "any." The wildcard type is akin to Java's Object in that an attribute of this type can hold any value regardless of its object's type. Unlike Java's Object, however, the wildcard type is not an F3 class. The wildcard type is better thought of as "untyped." Its main use is to store values whose runtime type is not known in advance. It's especially well suited to implement collection-type classes dealing with their contents in a generic fashion that is independent of their class. The wildcard type is specified in F3 with an asterisk in place of the attribute type.


    public attribute extent: **; // ** means "zero or more of any"
                                 // the first asterisk is the "wildcard" type,
                                 // the second is the cardinality specifier
    public attribute selection: **;

Arrays of the wildcard type can contain elements of heterogeneous types.

var any:**;
insert 'Hi!' into any;
insert 0 into any;
insert new <<java.util.Date>>() into any;

In the DB class, however, extents are required to hold elements of a single class. This class is referred to as the extent class.

Attribute Reflection

The extent of a DB is untyped and can store in it collections of instances of any F3 class. Due to this generality, accessing attributes of objects stored in an extent requires reflection.

Like in Java, an object can refer to its class through its class property, as in:

var greeting = "Hi there!";
var clazz = greeting.class;
println(clazz.Name); // prints 'String'

Retrieving attribute metadata is also straightforward:

class Person {
    attribute name: String;
    attribute age: Number;
}

var nameAttr = Person.class.Attributes[Name == 'name'];
var ageAttr = Person.class.Attributes[Name == 'age'];

In the above snippet, nameAttr and ageAttr are of type Attribute.

Variables can be indexed with expressions of type Attribbute (as long as such expressions correspond to actual attributes of the variable). Thus, it's possible to retrieve attributes reflectively like in:

var alex = Person { name: 'Alex', age: 7 };

println(alex[nameAttr]); // Prints 'Alex'
println(alex[ageAttr]); // Prints '7'

println(alex[nameAttr] == alex.name); // Prints true
println(alex[ageAttr] == alex.age); // Prints true

The Topher F3 application makes extensive use of reflective attribute access.

Filter Assertions

DB has a filter array attribute of type AttributeValueAssertion where user selections for each extent filter attribute are stored.

For any given extent the filter array contains as many elements as extent properties are being used for filtering. Thus, in the Topher application, the filter array will contain four elements corresponding to brand, country, decade and form.

Class AttributeValueAssertion is defined as:

public class AttributeValueAssertion {
    public attribute attr: Attribute;
    public attribute values: **;
    public function evaluate(obj):Boolean;
}

function AttributeValueAssertion.evaluate(obj) =
        sizeof values == 0 or obj[attr] in values;

This class embodies the notion of filter criteria.

To illustrate the above, lets consider the case where the user wants to filter characters to display only those of brands Kellog's or Nestlé and decade 1990. The following object literals correspond to these filters:

AttributeValueAssertion {
    attr: CerealCharacter.class.Attribute[Name == 'brand']
    values: [ 'Kellog\'s', 'Nestlé' ]
}
AttributeValueAssertion {
    attr: CerealCharacter.class.Attribute[Name == 'decade']
    values: [ '1990' ]
}

Function evaluate() is straightforward: an object matches the assertion if either there are no values specified or if its corresponding attribute value is among the assertion's values:

function AttributeValueAssertion.evaluate(obj) =
        sizeof values == 0 or obj[attr] in values;

Unconditionally matching an object if the user has specified no filter values (sizeof values == 0) implements the convention that selecting no filter values is equivalent to selecting them all.

If the user has specified at least one filter value then the attr attribute of the object is looked up in the values array (obj[attr] in values).

Elaborating on the above, let's consider the following extent instances:

var trixRabbit = CerealCharacter {
    label: 'Trix Rabbit'
    brand: 'General Mills'
    decade: '1960'
    country: 'USA'
    form: 'rabbit'
};
var batman = CerealCharacter {
    label: 'Batman'
    brand: 'Ralston'
    decade: '1980'
    country: 'USA'
    form: 'person'
};
var winnie = CerealCharacter {
    label: 'Winnie the Pooh'
    brand: 'Nabisco'
    decade: '1970'
    country: 'USA'
    form: 'bear'
};

And the following assertion:

var brandAttr = CerealCharacter.class.Attributes[Name == 'brand'];

var brandAssertion = AttributeValueAssertion {
    attr: brandAttr
    values: [ 'Nabisco', 'Ralston' ]
};

We can now formulate filter queries like:

// Prints true: batman is of brand Ralston
println(batman[brandAttr] in brandAssertion.values);

// Prints true: winnie is of brand Nabisco
println(winnie[brandAttr] in brandAssertion.values);

// Prints false: trixRabbit is of brand General Mills, not Nabisco or Ralston
println(trixRabbit[brandAttr] in brandAssertion.values);

Note, by the way, that in this example expressions like winnie[brandAttr] could have been written as winnie.brand. We use the reflective notation instead to illustrate how generic filtering works.

Combining Filters

So far, we've considered the case in which only one filter is applied over the extent. Class DB has a selection attribute that holds the results of the current user-defined query. This attribute contains the intersection of the sets returned for each individual filter assertion element.

To understand how this works, let's assume we have the following four user-defined filters:

var brandAttr = CerealCharacter.class.Attributes[Name == 'brand'];
var brandAssertion = AttributeValueAssertion {
    attr: brandAttr
    values: [ 'Nabisco', 'Ralston' ]
};

var countryAttr = CerealCharacter.class.Attributes[Name == 'country'];
var brandAssertion = AttributeValueAssertion {
    attr: countryAttr
    values: [ 'USA', 'France' ]
};

var formAttr = CerealCharacter.class.Attributes[Name == 'form'];
var formAssertion = AttributeValueAssertion {
    attr: formAttr
    values: [ 'person', 'rabbit', 'bear' ]
};

var decadeAttr = CerealCharacter.class.Attributes[Name == 'decade'];
var decadeAssertion = AttributeValueAssertion {
    attr: decadeAttr
    values: [ '1960', '1970', '1980' ]
};

filter = [
    brandAssertion, countryAssertion, formAssertion, decadeAssertion
];

Let's revisit the previously defined extent instance batman:

var batman = CerealCharacter {
    label: 'Batman'
    brand: 'Ralston'
    decade: '1980'
    country: 'USA'
    form: 'person'
};

If we evaluate the expression:

select f.evaluate(obj)
from   f in filter

we'll get the following array:

[
    true,  // 'Ralston' is among {Nabisco, Ralston}
    true,  // 'USA' is among {USA, France}
    true,  // '1980' is among {1960, 1970, 1980}
    true,  // 'person' is among {people, rabbit, bear}
]

Because all conditions are true batman will be selected by the query because it holds that:

not (false in (select f.evaluate(obj) from f in filter));

that is, false does not appear among the boolean array returned by the above select statement.

If we instead revisit the extent instance trixRabbit:

var trixRabbit = CerealCharacter {
    label: 'Trix Rabbit'
    brand: 'General Mills'
    decade: '1960'
    country: 'USA'
    form: 'rabbit'
};

The resulting boolean array is:

[
    false, // 'General Mills' is not among {Nabisco, Ralston}
    true,  // 'USA' is among {USA, France}
    true,  // '1960' is among {1960, 1970, 1980}
    true,  // 'rabbit' is among {people, rabbit, bear}
]

Because false does appear in the boolean array returned by the select statement, trixRabbit will not be selected by the query.

After seeing these examples we can take a look at the DB.applyFilter() function declaration:

function DB.applyFilter(obj) =
    not (false in (select f.evaluate(obj) from f in filter));

We can now introduce an expression that yields the array of extent elements selected by the combination of DB filters:

extent[elm | this.applyFilter(elm) == true]

The above is an array predicate that reads: each elm in extent such that applyFilter(elm) yields true. This expression generates an array containing all elements in the extent array for which function applyFilter returns true (the result set).

Since function applyFilter has a Boolean return type, the above can be shortened to:

extent[elm | this.applyFilter(elm)]

An even more succint form of the above expression -à la XPath- is:

extent[this.applyFilter(.)]

Note, incidentally, how calculating the result set is possible without any intervening procedural logic. Thanks to F3's functional programming features it's simple to derive non-trivial results through the use of array predicates. All this without loops, conditional statements or intermediate variables.

Dynamic Ordering

The F3 order by operator is a binary operator. The left-hand side is any list of objects. The right-hand side is an expression that must return a list of Comparable objects. The right-hand expression is evaluated in the context of each element of the left hand side. The current element can be accessed with the . (dot) operator (as in XPath).

var winnie = CerealCharacter {
    label: 'Winnie the Pooh'
    brand: 'Nabisco'
    decade: '1970'
    country: 'USA'
    form: 'bear'
};
var leRequinKix = CerealCharacter {
    label: 'Le Requin Kix'
    brand: 'Nestlé'
    decade: '1980'
    country: 'France'
    form: 'shark'
};
var pico = CerealCharacter {
    label: 'Pico'
    brand: 'Nestlé'
    decade: '1980'
    country: 'Spain'
    form: 'dog'
};

var characters = [winnie, leRequinKix, pico];

var orderedCharacters = select c
                        from   c in characters
                        order  by [decade, country];

for (c in orderedCharacters) {
    println("{c.label} ({c.decade}): {c.brand}, {c.country}");
}
/*
prints:
    Winnie the Pooh (1970): Nabisco, USA
    Le Requin Kix (1980): Nestlé, France
    Pico (1980): Nestlé, Spain
*/

The right-hand side of the order by clause is not limited to a list of attribute values. It can also contain arbitrary expressions as in:

var orderedCharacters = select c
                        from   c in characters
                        order  by [if country == 'France' then 0 else 1, decade];

for (c in orderedCharacters) {
    println("{c.label} ({c.decade}): {c.brand}, {c.country}");
}
/*
prints:
    Le Requin Kix (1980): Nestlé, France
    Winnie the Pooh (1970): Nabisco, USA
    Pico (1980): Nestlé, Spain
*/

order by clauses can be combined with attribute reflection to provide even more general ordering capabilities.

Let's recall attribute sortCriteria of class DB is an array of reflective type Attribute that holds the sort criteria specified by the user.

Also, attribute selection of class DB holds the subset of the extent that matches the current user-defined filter. This attribute must be ordered in accordance to the sort criteria specified by the user and is defined as follows:

attribute DB.selection =
    bind extent[elm|this.applyFilter(elm)]
         order by
             select obj[a]
             from   obj in .,
                    a in sortCriteria;

Here, the right-hand side of the order by expression is a list of extent attribute values dynamically built from the sortCriteria attribute.

Since the form obj[attr] is reflectively equivalent to obj.attr this dynamic ordering works as if the given sort attributes had been specified explicitly.

The right-hand select statement uses an alias (obj) for the current element (.). This is done for readability only as the expression could also be written as:

select .[a]
from   a in sortCriteria;

Finally, note the use of bind in the attribute definition. Thanks to it, F3 keeps track of all direct and indirect dependencies of this attribute. Thus, any change in the filter or sortCriteria attributes results in the immediate -and efficient- recalculation of the result set stored in selection. This is referred to as incremental evaluation.

Again, notice how it's possible to keep the result set always up-to-date without any intervening procedural logic. The combination of array predicates and -especially- binding provide an almost "magical" declarative solution.

The GUI

This section discusses the GUI of the Topher F3 application as follows:

GUI Structure

CompositeWidgets generally compose their GUI as an object literal whose containment structure mirrors that of the GUI's visual appearance. The following snippet shows the skeletal object literal making up the ExhibitView widget:

BorderPanel {
        top: Label // Section #1: top title (html)
        center: BorderPanel {
            left: TabbedPane { // Section #2: filter area
                Tab { // One per filter criterion.
                    BorderPanel {
                        ScrollPane {
                            GroupPanel {
                                // One per filter value
                                SimpleLabel // Filter value count
                                CheckBox // Filter value
                            }
                        }
                    }
                }
            }
            center: BorderPanel { // Section #3: result area
                top: Label // Result count (html)
                center: ScrollPane { // Result display area
                    Label // One per result (html).
                }
                bottom:  GroupPanel { // Section #4: sort area
                    Label // "order by"
                    ComboBox // One per sort criterion.
                }
            }
        }
    };

Topher's Containers and Visible Widgets

A container widget is a type of widget that provides an area inside which other widgets (including, possibly, other containers) are displayed and laid out. The Topher application makes use of the following F3-provided containers:

We use the term visible widgets to refer to GUI components capable of displaying visible content and possibly capturing user input. Our application uses the following visible widgets:

We now examine briefly each of the above F3 widgets.

The BorderPanel Container

The BorderPanel is a container that can place its contents in one of five areas: top, left, bottom, right and center.

From the Swing programming perspective BorderPanel is a convenience container whose peer component is a JPanel configured with a BorderLayout. Note how the terms NORTH, WEST, etc. have been renamed to make it more intuitive for developers not familiar with Swing.

It's not necessary to specify all five regions in order to be able to use a BorderPanel. Thus, if only left and center are specified the effect is to divide the area in two vertical regions. Likewise, if only top and bottom are specified the effect is to divide the area in two horizontal regions. Region sizes are adjusted automatically as necessary.

BorderPanel {
    top: Label { ... } // The Top Title area
    left: TabbedPane { ... } // The Filter area
    center: BorderPanel { ... } The Result Display area
    bottom: GroupPanel { ... } The Sort area
}

The TabbedPane Container

A TabbedPane container is a multiscreen container that has Tab handlers to switch among screens. Only a Tab (the selected tab) is displayed at a time.

TabbedPane {
    tabs: [
        Tab { title: 'brand' ...  },
        Tab { title: 'decade' ...  },
        Tab { title: 'country' ...  },
        Tab { title: 'form' ...  },
    ]
}

Tabs within a TabbedPabe are containers in their own right and thus capable of containing visible widgets as well as other containers.

Tab {
    content: BorderPanel {
        top: Label { ... } // Filter criteria name
        center: BorderPanel { ... } // Filter value checkboxes
    }
}

From the Swing programming perspective TabbedPane wraps a JTabbedPane component. Unlike Swing's JTabbedPane, though, F3's TabbedPane makes the notion of Tab explicit, through a class that can be populated separatedly. The Tab component is a JPanel that is the actual container of the Tab contents.

The ScrollPane Container

The ScrollPane container adds vertical and horizontal scrolling bars to its view content, which is frequently another container.

ScrollPane {
    view: Label { ... } // The Result Display area
}

From the Swing programming perspective TabbedPane wraps a JScrollPane as well as a JViewPort. F3's TabbedPane simplifies scrolling by limiting it to just populating a view attribute.

The GroupPanel Container

The GroupPanel container organizes its contents in rows and columns so that they align properly. For a widget to be placed inside a GroupPanel it must initialize its row and column attributes. Remarkably, All F3 widgets have these attributes which are simply omitted they're not contained inside a GroupPanel.


GroupPanel {
    var row1 = Row {}
    var row2 = Row {}
    ...
    var countColumn = Column {alignment: LEADING}
    var checkBoxColumn = Column {alignment: LEADING, resizable: true}
    rows: [row1, row2, ...]
    columns: [countColumn, checkBoxColumn]
    content: [
        SimpleLabel {
            text: '3'
            row: row1
            column: countColumn
        },
        CheckBox {
            text: 'General Mills'
            row: row1
            column: checkBoxColumn
        },

        SimpleLabel {
            text: '9'
            row: row2
            column: countColumn
        },
        CheckBox {
            text: 'Kellog\'s'
            row: row2
            column: checkBoxColumn
        },
        ...
    ]
}

Where Row and Column are auxiliary classes that control alignment and resizability.

The attributes of GroupPanel are:

GroupPanel provides a very precise control over the placement of contained items when these are laid out as a grid.

From the Swing programming perspective GroupPanel encapsulates a JPanel and a JDesktop GroupLayout.

The SimpleLabel Widget

The SimpleLabel F3 widget is a component that displays simple text optionally accompanied by an icon or image. It's typically used for prompts and boilerplate text.

SimpleLabel {
    text: 'Country'
}

From the Swing programming perspective SimpleLabel encapsulates a JLabel.

The Label Widget

Label is a powerful F3 widget that supports rich text using HTML 3.2 syntax. Because it can display arbitrary HTML, Label can be used to display graphics, style text, show and follow links as F3 operations and also produce complex text layouts through the use of HTML tables.

Label {
    text:
        "<html>
          <table id='header'>
           <tr valign='middle'>
             <td><img src='http://simile.mit.edu/exhibit/examples/cereals/banner.png'
                      style='float: left; margin-right: 2em;'/></td>
            <td>
            <p>The information within this page, including all images,
            is ALL copyrighted by Topher.<br/>
            The original web page can be found at
            <a href='http://www.lavasurfer.com/cereal-guide.html' target='_blank'>
                http://www.lavasurfer.com/cereal-guide.html
            </a>.
            </p>
            <p>We are grateful to Topher for letting us host this data on our site.
            </p>
            </p>
           </td>
          </tr>
         </table>
        </html>"
}

The above snippet results in:

From the Swing programming perspective Label wraps an F3 XLabel which is a simple specialization of JEditorPane. This pane is configured with an HTMLToolKit when the text starts with the <html> directive.

The CheckBox Widget

The CheckBox widget wraps a JCheckBox and behaves like it. Unlike JCheckBox though, it implements an onChange() operation that makes replaces the implementation of the ActionListener interface and can be inlined in object literals.

CheckBox {
    selected: true
    text: 'General Mills'
    onChange: operation(newValue: Boolean) {
        if (newValue) {
            println('General Mills has been selected');
        } else {
            println('General Mills has been deselected');
        }
    }
}

The ComboBox Widget

The ComboBox widget works in conjunction with the ComboBoxCell class to provide a simple way to create drop-down lists with selectable items.


ComboBox {
    cells: [
        ComboBoxCell { text: 'Brand' }
        ComboBoxCell { text: 'Country' }
        ComboBoxCell { text: 'Decade' }
        ComboBoxCell { text: 'Form' }
        ComboBoxCell { text: 'Label' }
    ]
}

From the Swing programming perspective, ComboBox encapsulates a JComboBox but makes the notion of drop-down list element explicit through class ComboBoxCell.

Binding

Used in assignments and attribute initializers, the bind operator drives the F3 interpreter to keep track of all variables and attributes referenced in the right-hand side of the assignment. Thus, whenever a variable used in the right-hand side of an assignment changes its value, F3 recursively recalculates the value of all dependent bound variables. This is comparable to the way spreadsheets recalculate formula values whenever the cells they depend upon change their value.

When used in conjunction with array predicates, bind can recalculate values that would otherwise require complex procedural code. Let's revisit the definition of attribute DB.selection used to store the result set of a filter query:

attribute DB.selection =
    bind extent[elm|this.applyFilter(elm)]
         order by
            select obj[a]
            from   obj in .,
                    a in sortCriteria;

Here, selection depends on

In our Topher example elements of the sortCriteria change their value depending on the selection of a set of combo boxes. Thus, when the user changes a sort combo box selection the result set stored in model.selection will be automatically reordered to reflect the change. As usual, this takes place without any intervening procedural logic. It's all based on dependencies.

Function applyFilter() depends on the value of array attribute filter as well as the application of function AttributeValueAssertion.evaluate() for each filter element. This function, in turn, depends on the entries selected by the user as filter values. Thus, whenever the user selects a checkbox corresponding to a filter value, the selection is recalculated to include the newly selected value.

As can be seen, bind promotes a declarative programming style that minimizes or eliminates the need for procedural state management and dependency tracking.

It could be argued that the main difference between F3 and regular Swing programming is that in Swing it's the programmer who statically wires bindings through procedural listeners and event handlers. In F3 bindings area automatically taken care of by declaring dependencies through the bind operator.

The foreach Operator

The foreach operator iterates over an array and yields another array built from repeatedly expanding its control variable within its template body. The template body can contain object literals. Likewise, the resulting array can be inserted into object literals where it's interpreted as if defined statically.


ComboBox {
    cells: foreach (name in [ 'brand', 'country','decade', 'form', 'label' ])
                ComboBoxCell { text: name }
    
}

// Generates: ComboBox { cells: [ ComboBoxCell { text: 'brand' } ComboBoxCell { text: 'country' } ComboBoxCell { text: 'decade' } ComboBoxCell { text: 'form' } ComboBoxCell { text: 'label' } ] }

foreach is extensively used in F3 and in particular in GUI programming. Thanks to its use, it's possible to create highly dynamic user interfaces when used in conjunction with binding. This is so because a bound foreach is re-executed whenever the expressions it depends upon change their values. As a result, entire regions of the GUI can change, appear or disappear in response to model changes without any intervening procedural logic.

ComboBox {
    cells: bind foreach (j in sortBy)
        ComboBoxCell {
                text: j.Name
        }
}

In the above snippet, the combo box will automatically have its cell array change, grow or shrink depending on whether elements in the sortBy array are updated, inserted or deleted (programmatically or in response to user input).

Object Literals

F3 GUIS's are built from object literals inside which widgets are declared and their attributes initialized. The visual composition of the GUI is reflected in the containment structure of widgets defined in its object literal.

// Skeletal containment structure of the Topher GUI
BorderPanel {
        top: Label
        center: BorderPanel {
            left: TabbedPane {
                Tab {
                    BorderPanel {
                        ScrollPane {
                            GroupPanel {
                                SimpleLabel { ... }
                                CheckBox { ... }
                            }
                        }
                    }
                }
            }
            center: BorderPanel {
                top: Label
                center: ScrollPane {
                    Label { ... }
                }
                bottom:  GroupPanel {
                    Label { ... }
                    ComboBox {
                        ComboBoxCell { ... }
                    }
                }
            }
        }
    };

In addition to attribute initializers, object literals also can contain embedded variables, functions, operations and triggers. These constructs are legal only before or after attribute initializers. They're visible only after their declaration and only inside their enclosing object literal block.

Object Literal Variables

Object literal variables are variables declared between attribute initializers and must be assigned a value in their declaration. These variables can be referenced later in subsequent attribute initializers as well as in function and operation code inside their scope. Code can be embedded in object literals when attributes being initialized are of type function or operation (e.g. event handlers) or in object literal-level functions or operations (discussed below).

BorderPanel {
    top: Label {
        ...
    }

    var extent = bind (CerealCharacter*)model.extent
    var selection = bind (CerealCharacter*)model.selection

    center: BorderPanel {
        left: TabbedPane {
            background: white
            preferredSize: {height: 300, width: 200}
            tabs: foreach (name in ["brand", "decade", "country", "form"])
                Tab {
                    var at = CerealCharacter.class.Attributes[a|a.Name == name]
                    var allValuesOfThisAttribute = bind extent[at]
                    var values = bind
                      (select unique s from s in allValuesOfThisAttribute) order by .
                    var filter = bind model.filter[f|f.attr == at]

                    title: bind "{name} {
                                    if sizeof filter.values > 0
                                    then "({sizeof filter.values}/{sizeof values})"
                                    else "(all)"}"
                    ...
                            onChange: operation(newValue:Boolean) {
                                updating = true;
                                if (newValue) {
                                    if (filter <> null) {
                                        insert value into filter.values;
                                    } else {
                                        insert AttributeValueAssertion {
                                            attr: at
                                            values: value
                                        } into model.filter;
                                    }
                                } else {
                                    delete filter.values[. == value];
                                }
                                updating = false;
                            }

                    ...
                }
        }

    }
    ...
} 

Note that control variables in foreach expressions behave as object literal variables.

Object Literal Functions and Operations

In addition to variables, stand-alone functions and operations can be embedded inside object literals. These functions and operations can be referenced later in attribute initializers as well as in other code.

...
BorderPanel {
    center: BorderPanel {
        operation reset() {
            updating = true;
            delete model.filter.values;
            updating = false;
        }
        top: Label {
            text: bind
                "<html>
                    <b style='font-size:18;'>{sizeof selection}</b>
                    Characters <font color='gray'>
                    {if sizeof selection == sizeof extent
                     then "total</font>"
                     else "filtered from {sizeof extent}</font>
                           (<a href='{#reset}'>reset</a>)"
                    }
                 </html>"
        }
        ...
    }
    ...
}
...

Object Literal Triggers

It's possible to declare change triggers on attributes of objects declared inside object literals.

...
ComboBox {
    var: box
    horizontal: {pref: 100}
    row: r1
    column: otherCols[i]
    cells: bind foreach (j in sortBy) 
        ComboBoxCell {
            text: j.Name
        }
    selection: bind select indexof x from x in sortBy where x == model.sortCriteria[i]
    trigger on (k = box.selection) {
        updating = true;
        model.sortCriteria[i] = sortBy[k];
        updating = false;
    }
}

The var pseudo-attribute is used to introduce a variable pointing to the context object. Thus, in the above snippet box is used to refer to the ComboBox instance being populated. This is comparable to a this qualifier on the current object.

There are many uses for the var pseudo-attribute. One of them is the definition of change triggers on attributes of the context object. In the above snippet, a change trigger is defined on the combo box's selection attribute. This trigger alters the value of the model's sortCriteria attribute. Since the model's selection attribute is bound to an expression involving sortCriteria the result set will be automatically recalculated (in this case reordered.)

The ExhibitView GUI class

Class ExhibitView implements the application's GUI and is defined as:

public class ExhibitView extends CompositeWidget {
    public attribute model: DB;
    private attribute updating: Boolean;
}

The above attributes are defined as follows:

Composite Widgets

Class ExhibitView extends CompositeWidget, an F3-provided base class used to create new widgets out of the composition of existing ones. CompositeWidget subclasses must implement the composeWidget operation which has the following signature:

protected operation composeWidget(): Widget

In the Topher application the entire GUI is composed by the ExhibitView.composeWidget() function. This function returns an object literal whose containment structure mirrors that of the GUI's visual appearance.

function ExhibitView.composeWidget() = 
    BorderPanel {
        top: Label // Section #1: top title (html)
        center: BorderPanel {
            left: TabbedPane { // Section #2: filter area
                Tab { // One per filter criterion.
                    BorderPanel {
                        ScrollPane {
                            GroupPanel {
                                // One per filter value
                                SimpleLabel // Filter value count
                                CheckBox // Filter value
                            }
                        }
                    }
                }
            }
            center: BorderPanel { // Section #3: result area
                top: Label // Result count (html)
                center: ScrollPane { // Result display area
                    Label // One per result (html).
                }
                bottom:  GroupPanel { // Section #4: sort area
                    Label // "order by"
                    ComboBox // One per sort criterion.
                }
            }
        }
    };

Here, the composeWidget returns an object literal which uses attributes model and updating. A detailed analysis of how these two attributes are used to provide application functionality follows below.

Once a new widget has been created by this composition mechanism it can be used wherever a language-provided widget could be legally used.


Frame {
    onClose: operation() {System.exit(0);}
    title: "Cereal Characters"
    height: 500
    width: 800
    visible: true
    content: ExhibitView {
        border: EmptyBorder {top: 10, bottom: 10, left: 10, right: 10}
        background: white
        model: main
    }
}

Putting It All Together

We now proceed to dissect the object literal making up the ExhibitView widget where all the GUI application functionality is defined. Unlike the DB class, the ExhibitView class is "Topher-aware".

The top-level container of the widget is a BorderPanel for which only the top and center sections are defined:

BorderPanel {

    var extent = bind (CerealCharacter*)model.extent
    var selection = bind (CerealCharacter*)model.selection

    top: Label { ... } // Top-level title area
    center: BorderPanel { // Application area
        ...
    }
}

Here, the object literal variables extent and selection are declared at the top level because they're used throughout the ExhibitView widget composition.

Note the casts to CerealCharacter *. These are necessary because the corresponding model attributes are untyped but the application code needs to reference them as arrays of type CerealCharacter.

The Top-Level Title

The top-level title is a static Label containing HTML:

top: Label {
    text:
    "<html>
      <table id='header'>
       <tr valign='middle'>
         <td><img src='http://simile.mit.edu/exhibit/examples/cereals/banner.png'
                  style='float: left; margin-right: 2em;'/></td>
        <td>
        <p>The information within this page, including all images, is ALL
        copyrighted by Topher.<br/>
        The original web page can be found at
        <a href='http://www.lavasurfer.com/cereal-guide.html' target='_blank'>
            http://www.lavasurfer.com/cereal-guide.html
        </a>.
        </p>
        <p>We are grateful to Topher for letting us host this data on our site.
        </p>
        </p>
       </td>
      </tr>
     </table>
    </html>"
}

The Application Container

The application container contains the windows areas that present application functionality:

center: BorderPanel {
    left: TabbedPane { // Section #2: filter area
    }
    center: BorderPanel { // Section #3: result and sort areas
    }
}

The Filter Area

The filter area is enclosed in a TabbedPane featuring one tab per filter criterion:

left: TabbedPane {
    background: white
    preferredSize: {height: 300, width: 200}
    tabs: foreach (name in ["brand", "decade", "country", "form"])
            Tab {
                ...
            }
}

Tab Variables

The following object literal variables are declared inside each Tab:

Tab {
    var at = CerealCharacter.class.Attributes[a|a.Name == name]
    var allValuesOfThisAttribute = extent[at]
    var values = bind (select unique s from s in allValuesOfThisAttribute) order by .
    var filter = bind model.filter[f|f.attr == at]

   title: bind "{name} {if sizeof filter.values > 0
                        then "({sizeof filter.values}/{sizeof values})"
                        else "(all)"}"

    ...
}

Note that the above variables will hold different values for each iteration of the foreach loop.

Notice how the initializer for the title attribute of the current Tab references some of above object literal variables.

Tab Contents

Each Tab encloses a border panel that holds the filter criterion name and the checkbox area:

Tab {
    ...
    content: BorderPanel {
        top: SimpleLabel {text: "<html><b>{name}</b></html>"}
        center: ScrollPane {
            view: GroupPanel {
                ... // Checkbox area
            }
        }
    }
}
The Filter Area

As can be seen, the actual filter area is enclosed in the ScrollPane. Its contents are:

 GroupPanel {

    var rows = bind foreach (i in [1..sizeof values]) Row {}
    var col1 = Column {alignment: LEADING}
    var col2 = Column {alignment: LEADING, resizable: true}

    rows: bind rows
    columns: [col1, col2]
    content: bind foreach (value in values)
        [SimpleLabel {
            row: rows[indexof value]
            column: col1
            text: bind "{sizeof allValuesOfThisAttribute[. == value]}"
        },
        CheckBox {
            row: rows[indexof value]
            column: col2
            selected: bind value in filter.values
            text: value
            onChange: operation(newValue:Boolean) {
                updating = true;
                if (newValue) {
                    if (filter <> null) {
                        insert value into filter.values;
                    } else {
                        insert AttributeValueAssertion {
                            attr: at // From outer scope
                            values: value
                        } into model.filter;
                    }
                } else {
                     delete filter.values[. == value];
                }
                updating = false;
            }
        }]
}

Recall that attribute rows of container GroupPanel requires an array of Rows containing as many elements as rows will be displayed. This array is normally specified statically as it's generally the case the programmer knows in advance how many rows and columns will be used to lay contents out.

In this case, though, rows will contain as many elements as distinct values there are for the current filter attribute. This is an example of the use of foreach to generate dynamic object literals where static ones would be expected.

Variables col1 and col2 correspond to the columns used to display the number of values and the filter checkboxes respectively.

The GroupPanel content is populated by a foreach that iterates over the distinct values of the current filter attribute. The foreach template body contains a SimpleLabel and a CheckBox. The SimpleLabel displays the number of times the current value occurs in the extent for the current filter attribute. The CheckBox displays the current value and has a onChange operation that modifies the current model's filter attribute depending on whether the checkbox has been selected or deselected.

Recall that model.filter is an attribute model.selection depends upon. Insertions and deletions on the model filter taking place inside the onChange event handler will result in the automatic recalculation of the selection. Thus, each time the user selects or deselects a filter value the result display area will be immediately updated to reflect the change.

Note how attribute model.updating is used. When it's set to true, the display area will be blanked out. When set to false, the recalculated selection will be shown on screen. This avoids a flickering effect. The code that controls how the display area is populated follows immediately below.

The Result Display Area

The result display area is actually a long Label that formats query results as HTML:

center: BorderPanel {
    operation reset() {
        updating = true;
        delete model.filter.values;
        updating = false;
    }
    top: Label {
        var location = 'http://simile.mit.edu/exhibit/examples/cereals'
        text: bind
            "<html>
                <b style='font-size:18;'>{sizeof selection}</b>
                Characters
                <font color='gray'>
                    {if sizeof selection == sizeof extent
                     then "total</font>"
                     else "filtered from {sizeof extent}</font> (<a href='{#reset}'>reset</a>)"}
             </html>"}
    center: ScrollPane {
        view: Label {
            var columnCount = 2
            var rowCount = bind (sizeof selection / columnCount).intValue() + 1

            text: bind
                if updating
                then // Blank display area out
                    "<html><body/></html>"
                else
                    "<html>
                      <table> {
                        foreach (i in [1..rowCount])
                        "<tr> {
                            foreach (j in [1..columnCount])
                            "<td>
                               <table cellspacing='5'>
                                <tr>
                               {
                                select

                                       "<td><img src='{location}/{c.thumbnail}"></img></td>
                                        <td>
                                        <table>
                                        <tr>
                                        <td colspan='2'><b>{c.label}</b></td>
                                        </tr>
                                        <tr>
                                           <td>Brand:</td><td>{c.brand}</td>
                                        </tr>
                                        <tr>
                                           <td>Year:</td><td>{c.decade}</td>
                                        </tr>
                                        <tr>
                                           <td>Country:</td><td>{c.country}</td>
                                        </tr>
                                        </table>
                                        </td>"

                                    from c in selection[(i-1)*columnCount + j-1]
                                }
                                </tr>
                             </table>
                           </td>"
                        }
                        </tr>"
                    }
                     </table>
                  </html>"
        }
    }
}

Operation reset() deletes all filters currently in effect. Because of the checkbox bindings, this will result in the immediate deselection of all checkboxes in all tabs.

In the expression delete model.filter.values both filter and values are arrays. When an array within another array is referenced without qualification the entire nested tree of elements is returned. In the particular case of operation reset() the net effect is nullifying all filter elements.

Note that the HTML content of the Label making up the result display area includes an <a href="#reset">reset</a> hyperlink. Clicking on this link will result in the invocation of operation reset(). Thus, a Label doesn't only format text but can enable user input in a button-like fashion too. A Label can also mimic a container by laying out contents inside HTML tables.

Attribute updating is manipulated by both the checkbox onChange event handler and the reset() operation. This variable controls what is displayed on the result display area. If updating is true because filters are being modified then the display area will be blank. Otherwise, the display area will contain the extent elements selected by the current combination of filter values.

The Sort Area

The sort area displays a number of ordering criteria as a set of combo boxes as follows:

bottom: GroupPanel {
    var clazz = CerealCharacter.class
    var sortBy = select clazz.Attributes[Name == n]
                 from   n in ["brand", "country", "decade", "form", "label"]
    var r1 = Row {}
    var c1 = Column {alignment: TRAILING}
    var otherCols = foreach (i in [1..sizeof sortBy]) Column { }
    rows: r1
    columns: [c1, otherCols]
    content:
    [SimpleLabel {
        text: "Order by:"
        row: r1
        column: c1
    },
    foreach (i in [0..sizeof sortBy-1]) 
        ComboBox {
            var: box
            horizontal: {pref: 100}
            row: r1
            column: otherCols[i]
            cells: bind foreach (j in sortBy) 
                ComboBoxCell {
                    text: j.Name
                }
            selection: bind select indexof x from x in sortBy where x == model.sortCriteria[i]
            trigger on (k = box.selection) {
                updating = true;
                model.sortCriteria[i] = sortBy[k];
                updating = false;
            }
        }]
}

sortBy is an array of reflective Attributes corresponding to the CerealCharacter attributes used for ordering. This includes all of the filter criteria plus the label attribute. sortBy is used in conjunction to the model's sortCriteria attribute which is also of type Attribute.

The GroupPanel container requires that the arrays of rows and columns be specified. The number of columns is dynamic as more (or less) attributes could plausibly be used for ordering. Thus, the array otherCols of type Column is built by means of foreach.

The expression [c1, otherCols] would appear to be an array of two elements. However, because otherCols is itself an array the expression array is flattened and will contain as many elements as sizeof otherCols plus one (corresponding to the leading element c1.)

The selection attribute of each combo box is bound to the position of the currently selected element within the model's sortCriteria array attribute.

The change trigger defined on the ComboBox selection fires each time the user changes the selection. The action is to set the value of the corresponding element of the model's sortCriteria attribute.

The model's selection attribute is bound to an expression involving the sortCriteria attribute. Because of this its value will be automatically reordered every time the user selects an option in any of the sort combo boxes.

Conclusion

As can be seen, F3 features powerful constructs that promote a declarative programming style quite different and more productive than regular Swing programming. The main three such constructs are:

These constructs are not limited to GUI programming but are especially well-suited for it.

F3 is very similar to Java in that it's strongly typed and fully object-oriented. In this regard, most of the differences between the two languages are merely syntactic.

F3, however, borrows from functional programming, XPath and XQuery. In F3, functions and operations are first class citizens that can be declared as class attributes or passed as arguments (including closures.) Likewise, F3 array predicates allow for list access in powerful declarative ways.

Most importantly, F3 features incremental evaluation. In conventional GUI programming much of procedural code is devoted to binding: keeping track of changes and synchronizing dependent state in response to value-changing events. In F3, on the contrary, binding is implemented in a purely declarative way.