#2259 Documentation on it-blocks

tomcl Sun 13 Apr 2014

As a newbie learning Fantom I have finally (I think) got to grips with it-blocks.

It-blocks can be used in any context expecting a closure, which makes sense. They can be used appended to (some) type names which only makes sense if you realise that the default constructor in this case has a special form that applies the constructed object to the it-block.

This use of an it-block appears similar to (but is semantically quite different from) a with-block in which an it-block closure is appended to an object.

In order to get things straight I've written a short tutorial which maybe will become part of Hertz's book. Corrections would be welcome - especially if there are things I have not quite got straight yet.

I'm writing it from the context of somone not previously familiar with Ruby's closures. Sorry for the wrong formatting of inline code.

It-Blocks and Declarative Programming

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 is expected.

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.

When a methods'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 these expressions 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 const fields cannot be changed.

It-Add Statements (AKA comma operator)

One final tweak of the it-block syntax is especially useful when it-blocks are used to initalise construction of objects. If an expression inside the it-block is terminated with a , the expression is used as a parameter in an it.add method call:

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 constructor's 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

That is equivalent to the with method (on Obj, inherited by every class) being 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.

For a simple 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.

brian Sun 13 Apr 2014

This is all excellent and really covers the features in details well.

In your discussion of with blocks, it might be bring everything more together if you show that with-blocks actually are it-blocks, just that they implicitly call sys::Obj.with:

foo { bar }
foo.with { bar }
foo.with |Foo it| { it.bar }

With is actually virtual so you can actually do some pretty cool things with "with-blocks"

SlimerDude Sun 13 Apr 2014

Wow tomcl, that's a lotta writing! And it all reads pretty good to me.

Further to what Brian said about Obj.with()... If you look at the signature, it's:

virtual This with(|This| f) {
  f.call(this)
  return this
}

Which means it is an it-block and your list example is actually:

[1,2] {3,4,} => [1,2].with() { it.add(3); it.add(4); }

And, for a different chapter maybe, I'd note that the add() function only works with the comma , character when add() is annotated with the facet @Operator to make it a shortcut operator.

Personally, to make my code more explicit (and hence readable) I tend to always include function brackets and use the it keyword. So I would favour something like:

[1,2,3].map() { it.negate }

(Oh, and I'd thought I'd mention that whereas in markdown the ` backtick ` is used to denote code, in fandoc it's used for http://links. Use the standard apostrophe for code.)

Good work!

tomcl Sun 13 Apr 2014

Many thanks.

backticks corrected. with-block stuff changed.

My take on this. Though I've programmed in LISP I prefer fewer brackets. That makes it easier to read the semantically important stuff.

But it only works if the syntax is visually recognisable. It-blocks are OK for me because although you might think they get confused with normal blocks, in practice they don't. And the similarity with function bodies is appropriate.

With-blocks are OK too, used idiomatically to operate on objects they are like customised post-fix operators.

I'm uneasy about the fact that whether an it-block is pulled into a constructor, or applied post-construction, is unknowable without looking at the corresponding class definition. But if all classes are written correctly, with constructors doing this where necessary, I guess it is OK.

It-blocks are one of the cleverest bits of Fantom syntax. Language syntax is not superficial - it links to both semantics and readability - so I care about it.

I don't know Ruby so I'm not sure how much is borrowed from there. I wrote this because it-blocks are really difficult to understand when you first come across them - but you cannot read any significant Fantom examples without understanding them!

They need a decent tutorial well signposted and I hope this can be the stating-point for one.

SlimerDude Mon 14 Apr 2014

I don't know Ruby so I'm not sure how much is borrowed from there.

Not as much as you might think! Yes you declare closures in a similar fashion, but I'm not aware of any it-block type syntax contraction.

Login or Signup to reply.