-
Notifications
You must be signed in to change notification settings - Fork 9
Idioms and Patterns
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.
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.
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.
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]] ]
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.
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)
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.
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.
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.
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):
- Extract a property to group by:
[:morph | {morph color. morph} asTuples]
-
(optional) Sort contents:
[:tuples | tuples sorted: [:t1 :t2 | t1 second asString <= t2 second asString]]
- Create groups:
[:tuples | tuples asGroups]
-
(optional) Sort groups:
[:groups | groups sorted: [:g1 :g2 | g1 first asString <= g2 first asString]]
- Insert a separator for each group:
[:group :contents | {{#dummy. {{group asString}} }. {group.contents} } ].
- Expand the group:
[:group | group expandGroup]
- Omit the grouping object:
[:colorOrDummy :morphOrSeparator | morphOrSeparator]
- 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.
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)