13 - Declarative Programming and It Blocks
Chapter 8 described how Fantom uses closures to represent functions that are used as data, for example as parameters to standard library functions.
Closures in Fantom can be written with a number of shortcuts, but normally consist of two parts: a signature in | |
followed by a closure body in { }
.
Here we will look an extension of this syntax in which the signature is omitted entirely, and some associated syntactic sugar for function calls, which together make idiomatic Fantom using closures look very clean. This is especially useful for declarative programming.
It-Blocks
Where there are no parameters, or one parameter with type inferred, the signature part of a closure is unnecessary. Fantom allows this to be omitted entirely in contexts where a function value of known type is expected.
x.map(|Int n ->Int| { return n*n}) =>
x.map( {it*it} ) =>
x.map {it*it}
A closure without signature and without any return
statement is called an it-block because the missing parameter can be used in the closure body as name it. Thus it-blocks are the ultimate compact way to write closures.
Whenever a method's last parameter expects a closure it can be taken out of the method call brackets and written immediately after the method call. This syntax makes such constructions much more readable. The map
method of a List
expects a function so this compact notation represents a list of squares. All of the expressions below mean the same thing in Fantom, and represent the list [1,4,9]
.
[1,2,3].map {it*it} /*preferred idiomatic form */
[1,2,3].map() {it*it}
[1,2,3].map( {it*it} )
[1,2,3].map |n| {n*n}
[1,2,3].map |Int n| {return n*n}
[1,2,3].map (|Int n->Int| {return n*n}) /*form with no shortcuts*/
Slot Lookup and Assignment in It-Blocks
Inside an it-block closure the it
parameter is used for default lookup of field or method names just like the object in a class:
[1,2,3].map { negate } => [-1,-2,-3]
The method name negate
is looked up as it.negate
in this it-block so this negates each list value.
This slot lookup is particularly useful when an it-block is used inside a constructor. In Fantom this is common because many of the API constructors take a closure as optional last parameter of signature |This|
. The parameter type-name This
here is a special marker which represents the constructed object type and indicates that an it-block closure is expected. Note that in the constructor below f
represents the it-block, and this
binds to the constructed object. The function call f(this)
thus applies the closure to the object.
// default constructor for fwt::Button accepts optional
// function f as parameter
// f is usually a closure written immediately after
// the constructor call as an it-block
new make(|This|? f := null) { if (f != null) f(this) }
// constructor call with it-block initialisation
Button { size = "100x100" }
If an it-block closure is supplied to the constructor, the closure is called, with its it parameter set to the constructed object, inside the object constructor.
To see why this is useful remember that field and method names inside an it-block are looked up against it. In this case it is the new Button object and so this is a convenient way to initialise fields of a new class instance.
In this example Button.size
is set to "100x100"
.
Inside the constructor the closure is given static permission to set (initialise) const
fields on the object. So this is a way to parametrise newly created constant objects. Note that the same operation would not work with the closure called outside the constructor on the object instance, because at that time static const
fields cannot be changed.
It-Add Statements (AKA comma operator)
One final tweak added to the it-block syntax is especially useful when it-blocks are used to initalise construction of objects. Any expression in Fantom terminated with a ,
is transformed into a statement as a parameter to an add
method call:
x, => add(x)
The meaning of this depends on context. The add
name is looked up against either the current object instance, inside a class, or the current it
paramter, inside an it-block closure.
x, => it.add(x)
Idiomatically this allows it-blocks used to initialise constructors that have a number of field assignments, followed by a number of calls to the add
method. This is useful in GUI programming as the following examples show.
With-blocks
With-blocks superficially look and behave like it-blocks, but they are semantically very different.
An it-block is a closure appended to a method expecting a closure, possibly to a constructor (typically shortened to a class name). Inside the closure the comma operator appended to an expression translates to an add
method call on the constructed object because of the magic of a special constructor that expects a closure as parameter and applies the closure to this
inside the constructor. other statements can be used conveniently to set object fields because a name that does not resolve locally will be matched against the closure it
parameter which is, because of the form of the constructor, the same as the constructors
this`.
A with-block is an extension of this paradigm to a more general case, where an it-block closure clos
is appended to any expression exp
not expecting a closure. When this happens the expression's with
method exp.with
is implicitly used. The effect of this is to apply the expression to the closure, returning the original expression:
clos(exp); exp
The with
method (on Obj
, inherited by every class) is therefore defined:
virtual This with(|This| f) {
f.call(this)
return this
}
Parenthetically it is worth noting that because with
can be overridden, with-block semantics is customisable on a per-class basis. This allows some interesting non-standard uses of with-block syntax.
For a simple with-block example, incorporating the ,
add operator:
[1,2] {3,4,} =>
[1,2].with { it.add(3); it.add(4) } =>
[1,2].add(3).add(4)
Here {3,4,}
is the with-block. Compare this with:
List {1,2,3,4,} => /* error */
This does not compile because, unusually, the List class does not have any constructor written specially to accept a closure. Also the List class has special syntax for its constructor and no make method, so List on its own is not a valid expression.
The semantic distinction between it-blocks and with-blocks is that an it-block applied to a (closure accepting) constructor will operate on the constructed object inside the constructor and therefore may initialise const
fields and objects.
A with-block operates on an already constructed object and therefore cannot change const
fields.
Syntactically the two forms are identical.
ClassName { <it-block contents> }
The ClassName written on its own will, if possible, result in an implicit call to its constructor. The semantics then depends on whether this accepts a closure parameter, or whether it does not.
Declarative Programming Examples
Here is how to define a top-level Window widget, containing three Button widgets:
class Main
{
Void main()
{
Window
{
it.title = "two Buttons"
it.size = Size(100,200)
Button { text = "A"},
Button { text = "B"},
Button { text = "C"}
}.open
}
}
The Window
widget is initialised with an it-block that also uses it-add to add three Button
widgets to the window. The add
method is invoked by appending the ,
operator to the relevant expression (the Buttons). Note that as a convenience the last expression in the block is assumed it-add and does not require a ,
.
Each Button widget is itself initialised on construction with a nested it-block that sets the text
field of the widget.
The open
method of the top-level window starts the GUI event loop.
User classes can be used in the same fashion as this example from the standard Fantom desktop fwt example shows:
class DesktopDemo : Canvas
{
Void main()
{
Window
{
it.title = "Desktop Demo"
it.size = Size(600,400)
DesktopDemo {},
}.open
}
/* custom onPaint method for DesktopDemo not shown */
}
This function constructs a Window
GUI widget and then calls its open
method which has the effect of starting the GUI event loop.
The it-block that constructs the Window
initialises fields title
and size
on the Window
object and then calls the add
method of Window
to add the constructed object of type DesktopDemo
to the newly created window. The DesktopDemo
object is sub-classed from Canvas
so this will add a Canvas
widget to the window. Note that the it-block that initialises DesktopDemo
(whose constructor is inherited from Canvas
) in this case contains no further initialisation. This style can easily be used to add nested sub-widgets as is needed, either from the standard API or user-defined.
Summary
Closures can be written compactly in Fantom as it-blocks. This notation combines with syntactic sugar that allows a last function parameter to be pulled out of a function call's brackets and written without brackets after the function. It allows clean code when used with many API functions that accept functions as parameters.
Widgets, and many other Fantom API classes, have constructors that accept an optional last parameter that is a closure called from inside the constructor. That allows the closure to initialise object fields, even const
fields.
Inside an it-block an expression terminated by ,
is translated into a call of the add method
on the it
parameter.
GUI code typically consists of nested make
and add
calls with initialisation and benefits from this notation. Closures are written using field assignment to initialise each widget, and ,
after sub-widgets constructors - themselves using it-blocks - to add sub-widgets.
Any expression not expecting a closure, with it-block appended, forms a with-block. The with-block closure is called with the expression as parameter. This is a post-construction equivalent of an it-block appended to constructor.