Happy !

MarcoNaddeo / IniModules

Project infos

License MIT
Tags initialization, modularity
Creation date 2014-11-26
Website

Monticello registration

About IniModules

A modular approach to object initialization

Ini-modules are a different way of initializing objects compared with traditional constructors. With this approach to object initialization, you can split the code that you would put into the body of a constructor into small pieces of code, called ini-modules. The interested reader will find a description of the problems with traditional constructors at the end of this page.

5-minutes tutorial

In this section we give the basic steps to get you started with ini-modules.

Installation

To load ini-modules into your Pharo image, you can execute this piece of code:

Gofer it
  smalltalkhubUser: 'MarcoNaddeo' project: 'IniModules';
  configurationOf: 'IniModules';
  loadStable

All the code is contained into three packages named IniModules, IniModules-Nautilus and IniModules-Tests.

Defining and using ini-modules

In this tutorial you will learn how to initialize the instances of a simple class Rectangle2D, by using ini-modules. A Rectangle2D is represented by an upper-left corner (x,y), a width and a height:

Object subclass: #Rectangle2D
  instanceVariableNames: 'x y width height'
  classVariableNames: ''
  category: 'IniModules-Tests-Classes'

We want the upper-left corner to be initialized in 3 different ways: (1) by providing Cartesian coordinates directly, (2) by providing polar coordinates, and (3) from a Point object.

In standard Pharo, this would mean implementing instance-creation methods on class side and mutators on instance side. If for some reason the class designer wants the instances of Rectangle2D to be immutable, things become even more complex because of the required mutators.

With ini-modules, you can obtain the desired behavior as follows:

"Rectangle2D>>(x y)()
 initialize upper-left corner from Cartesian coordinates"
Rectangle2D addIniModuleWithCode: [ :x :y :instVars | instVars at: #x put: x; at: #y put: y ].

"Rectangle2D>>(angle rad)(x y)
 initialize upper-left corner from polar coordinates"
Rectangle2D addIniModuleWithOutput: [ :angle :rad | {#x -> (angle cos * rad) . #y -> (angle sin * rad)} ].

"Rectangle2D>>(point)(x y)
 initialize upper-left corner from a Point object"
Rectangle2D addIniModuleWithOutput: [ :point | { #x -> (point x). #y -> (point y) } ].

"Rectangle2D>>(width)()
 initialize width"
Rectangle2D addIniModuleWithCode: [ :width :instVars | instVars at: #width put: width ].

"Rectangle2D>>(height)()
 initialize height"
Rectangle2D addIniModuleWithCode: [ :height :instVars | instVars at: #height put: height ]

To create an instance of Rectangle2D you will use then the message mipNew:, whose argument is an array of associations mapping parameter names to their values:

|rectangle|
rectangle := Rectangle2D mipNew: { #point -> (5@20).
                                   #width -> 50.
                                   #height -> 10 }

Notice that the order of the associations that are provided in an object creation expression does not matter. During initialization, the ini-modules to be executed will be found based on the provided associations (see the Section How is an object creation expression evaluated? for details).

Each ini-module is defined via a block. An ini-module can have input parameters, which directly correspond to the arguments of the block (except for the special argument instVars, see below). An ini-module can also produce output parameters. Since ini-modules have no names (like Java constructors), we will use input and output parameters to refer to them, together with the class to which they belong. For example, we will refer to the ini-module for polar coordinates as follows: Rectangle2D>>(angle rad)(x y). We will omit the class when it is easily deducible from the context, i.e., we will write (angle rad)(x y) if it is obvious that we are talking about an ini-module of the class Rectangle2D.

Ini-modules are thought to be modular, this means that they cooperate. In this example, (angle rad)(x y) and (point)(x y) delegate to (x y)() based on the parameter names. Indeed, their output parameters match the input parameters of (x y)().

The message addIniModuleWithCode: sent to a class allows you to define a "terminal" ini-module. Such an ini-module does not delegate to other ones and it is typically used to set fields. The parameter of this message is a block in which you can use a special argument instVars to initialize the fields. Notice that this argument will not be an input parameter of the ini-module.

The message addIniModuleWithOutput: sent to a class is used to define an ini-module that delegates to other ini-modules by producing output parameters. The parameter of this message is a block whose last expression must be a dynamic array (enclosed between { and }), containing associations mapping each output parameter to a value.

We showed how ini-modules allow a class designer to define the initialization protocol of an object in a modular fashion. We will see now how to naturally extend it in subclasses without any need of code duplication.

Extending the initialization protocol in subclasses

Typically, subclasses add new fields which need to be initialized as well. In standard Pharo, this means to manually extend each of the instance-creation methods defined in the superclass. If the new fields have multiple initialization options, the designer of the new subclass must manually manage all the combinations.

Let us suppose that we want to implement a subclass ColoredRectangle2D of Rectangle2D, which adds three new fields r, g and b for color:

Rectangle2D subclass: #ColoredRectangle2D
  instanceVariableNames: 'r g b'
  classVariableNames: ''
  category: 'IniModules-Tests-Classes'

To manage two different color palettes (RGB and CMYK), you only need to add two new ini-modules:

"ColoredRectangle2D>>(red green blue)()"
ColoredRectangle2D addIniModuleWithCode: [ :red :green :blue :instVars | instVars at: #r put: red;
                                                                                  at: #g put: green;
                                                                                  at: #b put: blue].

"ColoredRectangle2D>>(c m yc k)(red green blue)"
ColoredRectangle2D addIniModuleWithOutput: [ :c :m :yc :k | { #red -> (255 * (1 - c) * (1 - k)) .
                                                              #green -> (255 * (1 - m) * (1 - k)) .
                                                              #blue -> (255 * (1 - yc) * (1 - k)) } ]

Notice that to obtain the same behavior in standard Pharo you would be forced to implement 6 instance-creation methods ([3 ways of initializing x and y] × [2 ways of initializing r, g, b]). In general, there is an exploding number of the constructors to implement in classes inheriting from superclasses with multiple initialization variants.

Here there is an example of instantiation of a ColoredRectangle2D:

|purpleRectangle|
purpleRectangle := ColoredRectangle2D mipNew: { #point -> (5@20).
                                                #width -> 50.
                                                #height -> 10.
                                                #c -> 0.5.
                                                #m -> 0.8.
                                                #yc -> 0.3.
                                                #k -> 0.1 }

This concludes our 5-minutes tutorial. The following sections will go into some more details of the ini-module approach.

Technical details

How is an object creation expression evaluated?

When the mipNew: message is sent to a class C with a parameter map P, the following steps are executed:

  1. an uninitialized instance of the object is created via the message basicNew;
  2. an ordered list L of ini-modules is created by collecting all the ini-modules declared in the hierarchy of C, in the following way:
    1. the ini-modules in a subclass have precedence over those in the superclass;
    2. the ini-modules defined in the same class are ordered based on explicit constraints, which can be declared by the developer, and default constraints, which apply only in absence of explicit constraints that contradict them (see below);

  3. each ini-module I in L is considered and, if all its input parameters are in P and none of its output parameters that are not only input parameters is already in P, then:

    1. I is executed using the values in P as input parameters;
    2. P is updated by removing the input parameters of I and by adding its output parameters;

  4. at the end, if P is not empty then an error is raised (there are still parameters in the map); otherwise, the new object is returned.

Ordering the ini-modules

The ini-modules defined in a same class are ordered based on explicit constraints, which can be declared by the developer, and default constraints, which apply only in absence of explicit constraints that contradict them.

Default constraints handle automatically a pair of order relations that may recur programmatically between two ini-modules A and B:

  1. if A outputs at least one parameter that is taken in input (but not outputted) by B then A is tried first;
  2. if A takes as input parameters a strict subset of B’s input parameters, and A and B share at least one output parameter, then B is tried first.

Rule 1 captures a "produce-consuming" relationship between two ini-modules. In the previous examples, we have that, according to Rule 1:

  • ini-modules Rectangle2D>>(angle, rad)(x, y), Rectangle2D >>(point)(x, y) must be considered for activation before ini-module Rectangle2D>>(x, y)();
  • ini-module ColoredRectangle2D>>(c, m, yc, k)(red, green, blue) must be considered for activation before ini-module ColoredRectangle2D>>(red, green, blue)()

Rule 2 establishes that, when two ini-modules share at least one output parameter, the one requiring more data in input must be tried first as it is probably more precise and increases the chance that all parameters will be terminated before the end of the activation process. This rule in particular manages ini-modules that produce default values for some field. Consider for example to add the following ini-module to the previously considered ini-modules for the class Rectangle2D:

"Rectangle2D>>()(x y)
 initialize lower-left corner in the origin by default"
Rectangle2D addIniModuleWithOutput : [ | { #x -> 0. #y -> 0 } ]

According to Rule 2, ini-modules Rectangle2D>>(angle, rad)(x, y) and Rectangle2D>>(point)(x, y) must be considered for activation before ini-module Rectangle2D>>()(x, y).

Explicit constraints allow the developer to force a specified ordering between two ini-modules. To define an explicit constraint, the developer can send a message applyBefore: or applyAfter: to an ini-module, with the other ini-module to put in an order relationship with the receiver passed as argument. Consider a class Person, with three fields, two for name and surname, and one for a nickname:

Object subclass: #Person
  instanceVariableNames: ’name surname nickname code’
  classVariableNames: ’’
  package: ’IniModules-Tests-Classes’

Consider that we want to initialize the fields as follows:

  • if no nickname is submitted in an object-creation expression, then we want a default one to be automatically generated from the name and the surname;
  • every person is associated to a code, and we want this code to be calculated starting from nickname (if provided) or name/surname (if not).

Then, we can write the following set of ini-modules:

|codeFromNick codeFromName|
codeFromNick := Person addIniModuleWithOutput:
                       [ :nick | {#nick -> nick.
                                  #code -> ('a code from ', nick)}
                       ].
codeFromName := Person addIniModuleWithOutput:
                       [ :name :surname | {#name -> name.
                                           #surname -> surname.
                                           #code -> ('a code from ', name, ' ', surname)}
                       ].
codeFromNick applyBefore: codeFromName.
Person addIniModuleWithOutput:
                       [ :name :surname | {#name -> name.
                                           #surname -> surname.
                                           #nick -> ('a nickname from ', name, ' ', surname)}
                       ].
Person addIniModuleWithCode: [ :name :surname :instVars | instVars at: #name
                                                                   put: name;
                                                                   at: #surname
                                                                   put: surname ].
Person addIniModuleWithCode: [ :nick :instVars | instVars at: #nick put: nick ].
Person addIniModuleWithCode: [ :code :instVars | instVars at: #code put: code ]

Note that, since we want the code to be calculated from the nickname and not from name/surname, if both are submitted, we need to explicitly state that the ini-module (aNickname)(aNickname, aCode) must be considered before the ini-module (aName, aSurname)(aName, aSurname, aCode).

Graphical user interface

Image and video hosting by TinyPic

By selecting a class in the System Browser, a protocol -- ini-modules --, containing all the ini-modules defined for the selected class, is shown by default. By clicking on it:

  • the underlying panel shows two templates for ini-module definition, guiding the developer in defining new ini-modules via the messages addIniModuleWithCode: and addIniModuleWithOutput:;
  • the fourth panel shows a list of the defined ini-modules for the selected class; in this panel, the ini-modules are ordered as imposed by the inferred default constraints and the imposed explicit constraints; this allows the developer to have a direct check over the ordering in which the ini-modules will be considered for execution.

By clicking on one of the ini-modules in the list, the panel below shows the ini-module definition code, allowing the user to edit it when needed. If explicit constraints involving the shown ini-module have been declared, they are shown as well. When defining an explicit constraint by using the underlying panel of the System Browser, the message iniModuleWith: can be sent to a class for retrieving a previously defined ini-module. For example:

Person iniModuleWith: #((name surname)(name surname code))

retrieves the ini-module Person>>(name surname)(name surname code).

Finally, by right-clicking on an ini-module in the fourth panel of the System Browser, a drop-down menu appears allowing the user to remove it.

Problems with traditional constructors

Ini-modules have been initially introduced to solve some general problems with traditional constructors in mainstream class-based object-oriented languages:

  • Multiple initialization options. Each initialization option of a field set can be managed by a dedicated ini-module. As a result, the developer avoids duplicating code.
  • Optional initialization. Each optionally initialized field can be managed by implementing two ini-modules: (1) the first one produces as output the default value for the field; (2) the second one takes as input a value for the optionally initialized field and sets the field.
  • Duplication in subclasses. A subclass can define new ini-modules to initialize its own fields, with no need of duplicating those that are defined in the superclass.
  • Constructor signature uniqueness. With ini-modules, parameters are supplied by name and not by position, therefore it is always possible to implement the ini-modules that are needed, also with the same number and types of parameters.

Ini-modules also solve more specific problems related to Pharo's object initialization:

  • No user-supplied parameters. Unlike Pharo's initialize method, ini-modules allow the developer to define ini-modules both to deal with default parameters and with parameters supplied at object creation time.
  • Required mutating methods. Ini-modules do not require the class developer to implement mutating methods (as needed with instance-creation methods) because basic ini-modules allow to modify the class fields via the special instVars argument.
  • Pollution of the class’ API. Ini-modules are only activated during object creation and they cannot be explicitly called outside it.
  • Unavoidable setting of default methods. Ini-modules make the initialize method useless to initialize fields and thus no default initialization happens if the class user specifies initialization values.