Skip to content

Idioms and Patterns

Marcel Taeumel edited this page Jan 20, 2016 · 26 revisions

General Remarks

Many idioms employ general script evaluation semantics and the script editing wizard with its templates. Several patterns are recognized by the wizard such as simple one-to-one mappings. The snippet [:number | number + 1] will be expanded to something like:

[:in :out | [:objects | objects collect: [:number | number + 1]] 
   value: in) in: [:result | out addAll: result asList]]

Note that the formatting may seem rather bogus to the experienced Smalltalk programmer but it is optimized on the block filter, which can be triggerd via [CMD] + [left/right].

Outside the script editor, pattern recognition and template insertion also takes place when calling #asScript, #openScript, or #openScriptWith: messages on string literals, block literals, or array literals. Here is an example:

[:number | number + 1] asScript.

Here is an example with two script parts using a literal array:

{
   [:number | number * 2].
   [:even | even + 1].
} asScript.

On this page, we will omit the curly brackets and focus on the transformation parts.

The wizard will also recognize one-to-many mappings as you can see in the example above through out addAll: result asList. Here, resulting objects will be converted into a single-element list except for collections that are no Dictionary, ByteArray, String, or Text. nil will be converted into an empty list.

The wizard will also recognize some many-to-many mappings by looking terms from the collection's enumeration protocol such as collect and select:

[:numbers | numbers select: [:n | n even]] openScriptWith: #(1 2 3 4 5 6).

For more complex expressions, the wizard might have to be disabled. For example, combining a one-to-many mapping with a filter cannot be recognized correctly:

"The wizard cannot detect this."
[:class | class methodDict values select: [:method | ...]].

The programmer has to use two script parts in such a case:

{
   [:class | class methodDict values].
   [:methods | methods select: [:method | method selector beginsWith: #draw ]].
} asScript.

Here, we will mainly show script code that can be expanded by the wizard into the [:in :out | ...]-form as shown above. In the script, the access to input objects is constrained by the underlying container type. By default, in and out are of type OrderedCollection and are read-only resp. write-only.

The wizard is able to recognize script properties (here: input kind and view class) if those are associated in a literal array:

{
   [:number | number * 2] -> { #inputKind -> Number. #view -> ViTreeView }.
} asScript.

We will also make use of this syntax here but omit the surrounding curly braces.

For historic reasons, the terms (script) part and (query) step may be used interchangeably.

Idioms

Replace nested enumerations with multiple script parts

Scripts can be used to express a chain of transformations conveniently. For filter-and-map operations, the Squeak/Smalltalk collection interfaces offers #select:thenCollect: but longer chains may result in deeply nested expressions with lots of braces. Readability may be aggravated. So

[:numbers | ((numbers select: [:n |
   n even]) collect: [:n |
      n + 5]) reject: [:n |
         n > 10])]

should be written as

[:numbers | numbers select: [:n | n even]].
[:number | number + 5].
[:numbers | numbers reject: [:n | > 10]].

The wizard works fine here.

Store volatile data in blocks

Programmers can allows views to access volatile model data without re-evaluating the underlying script by providing a block instead of the raw data:

[:morph | #text -> [morph color]].

Whenever a view asks the model node for the text property, the block will be evaluated automatically/transparently. The view will not know about the block.

Having this, storing an actual block as data requires a block within a block:

[:morph | #clicked -> [[morph color]] ].

If views ensure to only read that data when evaluating the block, such nesting may not be necessary.

Managing side-effects

Script transformations should be free of side-effects because the programmer should not bother about when or how much a script is evaluated.

Property extractions can encode side-effects by specifying the write operation like this:

[:ref |
   #text -> "read"  ref sourceString
         <- "write" [:newSource | ref actualClass compile: newSource] ].

Be sure to have objects (here: a method reference) that remain valid after evaluating the write-block. Otherwise you may want to trigger a notification event or connect your script to a notification source:

[:ref |
   #text -> "read"  ref sourceString
         <- "write" [:newSource | 
                       ref actualClass compile: newSource.
                       ViEventNotifier trigger: #foo] ]
-> {#notifier -> [ViEventNotifier named: #foo]}.

Some views may also support computing side-effects in read-blocks such as the ViButtonBarView does with its callback #clicked:

[:morph | #clicked -> [[morph addHalo]] ]

When in doubt, let it tick

If your domain data does not have an observer pattern established to notify about changes, just refresh the script frequently:

[:class | class methodDict values]
  -> { #notifier -> [ViTimedNotifier every: 10000] }

Note that you can always trigger script re-evaluation by using the pane's halo.

Exploit script switching with #inputKind and #priority

On arriving objects, the pane looks up an appropriate script and changes it if the current one does not fit anymore. As for the moment, fitting considers the following properties:

  • Is it the current script? (stability)
  • Is it in the list of a pane's recently opened scripts? (awareness)
  • Is it in the current organization and has a #label attached? (check-pointing)
  • What is the #priority compared to other scripts? (weighting)
  • Does the #inputKind match the incoming objects? (robustness)

Model (recursive) tree structures

Alternating script transformation and extraction steps will result in a lazily evaluated tree structure.

[:n | #text -> n]. "first level"
[:n | n + 1].
[:n | #text -> n]. "second level"
[:n | n + 1].
[:n | #text -> n]. "third level"

evaluated on the objects #(1 2 3) will result in

1
|-2
  |-3
2
|-3
  |-4
3
|-4
  |-5

Recursive tree structures require a reference to a previously defined script part. As evaluation happens lazily and stops if the result is empty, the tree may or may not have an infinite depth and views have to care for this. Operations such as expand-all might not work then. ;-) Here is an example:

{
   [:morph | #text -> morph printString].
   [:morph | morph submorphs].
   1. "Wizard will insert a reference to first script part here."
} openScriptWith: ActiveWorld.

The interactive script editor provides a drag-drop gesture for creating references without having to manually type the target script's (generated) UUID.

Use tuples to keep temporary data

By default, tuples will be reduced to their first object when it comes to property extraction:

[:morph :color | #text -> morph printString. #ballonText -> color printString]
   openScriptWith: { {Morph new. Color yellow} }.

In the model node, views can access the morph via #object and the tuple (morph, color) via #objects. The wizard does this by recognizing the tuple syntax. Programmers can change this; this is only a suggestion.

Having this, temporary data can be kept in tuples. For example, a list of classes can be transformed to the inheritance hierarchy like this:

{
   [:classes | {
      classes reject: [:cls | classes includes: cls superclass].
      "add temp data" {classes} } asTuples ].
   [:class :other | #text -> class name].
   [:class :other | {
      class subclasses select: [:cls | other includes: cls].
      "keep temp data" {other} } asTuples ].
   2. "Reference to second script part."
} openScriptWith: (PackageInfo named: #Morphic) classes.

This script avoids listing subclasses of morph that are not in the input set. In this example, temporary data means the list of input classes.

Calling view code from within scripts

During evaluation, scripts get access to several dynamically scoped globals: thisScript, thisPane, thisView. Having this, scripts can call view code such as buttons that are used to trigger data flow:

[:behavior | |v| v := thisView.
  { #text -> 'instance'.
    #clicked -> [[v select: behavior theNonMetaClass]] } ] -> {#view -> ViButtonBarView}.
[:behavior | |v| v := thisView.
  { #text -> 'class'.
    #clicked -> [[v select: behavior theMetaClass]] } ].

Note that you have to capture the globals in temps bevause globals are evaluated lazily in blocks. Of course, the programmer has to be aware of the current view's interface. It is advisable to only use messages that TViObjectView encodes because all views are expected to provide that.

Grouping objects with separators

It is common to group a set of objects according to prominent attributes to ease browsing. Alphabetically, by category, thresholds -- you name it. Scripts can support this process by inserting separating objects that have to be distinguished in views. (If views do not care, we cannot help here. Use tree structures instead. But views have to be capable of displaying such strutures, too.)

The transformation process goes like this (the wizard will help):

  1. Extract a property to group by: [:morph | {morph color. morph} asTuples]
  2. (optional) Sort contents: [:tuples | tuples sorted: [:t1 :t2 | t1 second asString <= t2 second asString]]
  3. Create groups: [:tuples | tuples asGroups]
  4. (optional) Sort groups: [:groups | groups sorted: [:g1 :g2 | g1 first asString <= g2 first asString]]
  5. Insert a separator for each group: [:group :contents | {{#dummy. {{group asString}} }. {group.contents} } ].
  6. Expand the group: [:group | group expandGroup]
  7. Omit the grouping object: [:colorOrDummy :morphOrSeparator | morphOrSeparator]
  8. Extract properties: [:object | #text -> (object isString ifTrue: [object] ifFalse: [object printString])]

Step 5 may be confusing. But grouping a list of tuples of arbitrary size means storing the tail of each object in a custom list. So #( (1 2) (1 3 4) (2 4) ) asGroups results in #( (1 ((2)(3 4))) (2 ((4))) ), which can be reverted via #expandGroups. Inserting an additional (dummy) group into this structure has to be that way.

Don't worry, such a script is meant to be reused by other scripts. 😃 Here is the comprehensive example without the optional steps:

{
   [:morph | {morph color. morph} asTuples].
   [:tuples | tuples asGroups].
   [:group :contents | {{#dummy. {{'[', group asString, ']'}} }. {group.contents} } ].
   [:group | group expandGroup].
   [:colorOrDummy :morphOrSeparator | morphOrSeparator].
   [:object | #text -> (object isString ifTrue: [object] ifFalse: ['   ', object printString])].
} openScriptWith: ActiveWorld allMorphs.

It is advisable to study the (partially) generated scripts in their [:in :out | ...]-forms.

Close a tool window with a button

If you want to add a button that closes a particular tool window, you can write a script like this:

{
   [:object | | topmostPane |
      topmostPane := thisPane topmostPane.
      { #text -> 'Close'. #clicked -> [[ topmostPane close ]] } ]
   -> { #view -> ViButtonBarView } 
} openScriptWith: #(dummy)