вторник, 29 марта 2016 г.

New version of Ghost is out

New version of Ghost is out. All code is now hosted in github repo https://github.com/dionisiydk/Ghost (all packages moved here). You can load it by:

Metacello new
  baseline: 'Ghost';
  repository: 'github://dionisiydk/Ghost';

Ghost is framework to implement special kind of objects which process messages in unnatural Smalltalk way.
Let's implement little example from the idea of Stephan Ducasse http://lists.pharo.org/pipermail/pharo-dev_lists.pharo.org/2016-January/118417.html.
We want to know what messages are sent to object by inspector. Inspector is implemented by composition of multiple views which makes it difficult to collect full information statically. But we can do this dynamically by sending special object to inspector which will collect all message sends. Ghost provides suitable infrastructure to implement such kind of objects.

Any ghost implementation should answer three questions:
  1. How ghost will behave? (what to do with any received message?)
  2. What messages should not be part of ghost logic and processed in natural Smalltalk way?
  3. How to instantiate ghosts?
To answer first question we should implement GHGhostBehaviour. Lets call it Learning because our ghost will learn what messages should be implemented by object to participate in specific operations (like inspecting).

GHGhostBehaviour subclass: #Learning
instanceVariableNames: 'studiedMessages'
classVariableNames: ''
package: 'Ghost-Learning'

super initialize
studiedMessages := Dictionary new.

We add instance variable studiedMessages to store messages and their implementation methods from Object class. When learning will be completed we can inspect this dictionary to analyze required messages.
To process message send our behaviour should retrieve corresponding method from Object class and execute it on ghost instance:

Learning>>send: aMessage to: aGhost
|  learnedMethod |
learnedMethod := studiedMessages at: aMessage selector ifAbsent: [ nil].
learnedMethod ifNil: [
learnedMethod := Object lookupSelector: aMessage selector.
studiedMessages at: aMessage selector put: learnedMethod].
"here we should explicitly execute method by primitive to not introduce new messages to ghost because it not what it learn"
^GHMetaMessages executeWith: aGhost andArguments: aMessage arguments method: learnedMethod

Any ghost behaviour should implement message processing method #send:to:.
GHMetaMessages class provides suitable set of methods to execute mirror primitives on given object without sending extra messages to them.

Now to complete our behaviour we need to implement method #currentMetaLevel which defines set of messages which should not be processed by ghost logic. It is answer to second question of any ghost implementation. Here we want to learn all messages which will sent to our ghost:

^GHMetaLevel empty

We can also use "GHMetaLevel standard" which will not intercept standard messages from tools. For example #printString or #class will be processed by the meta level instead of ghost behaviour logic. And #class will return class of ghost as any other object. Standard meta level is useful to debug new ghost implementation because tools will look at ghost as any other normal object. And they will not produce recursive ghost behaviour.
But in our case we want to see what exactly inspector doing with ghost. And for this we will use empty meta level.

Last thing which we need to implement is new kind of ghost itself. Let's call it Student:

GHObjectGhost subclass: #Student
instanceVariableNames: 'learning'
classVariableNames: ''
package: 'Ghost-Learning'

Any ghost should implement #ghostBehaviour method and instantiation class side method:

^learning ifNil: [learning := Learning new]

Student class>>new
^self basicNew

And now we can execute script in workspace:

student := Student new.
student inspect.

And then inspect "student ghostBehaviour studiedMessages":

Improved version of this example is in package Ghost-Learning.

Update: Project was moved to github

четверг, 24 марта 2016 г.

New version of StateSpecs 2.0

I am finished new version of StateSpecs 2.0. 
With this library you can describe object state with first class specifications. For example there are SpecOfCollectionItem, SpecOfObjectClass and SpecOfObjectSuperclass. They can match and validate given objects. In case when object is not satisfied specification you will get failure result with detailed information about problem.
spec matches: anObject.
spec validate: anObject. "it returns validation result which can be success or particular failure"
To easily create specifications and validate objects by them StateSpecs provides two kind DSL: should expressions and "word" classes.
First allows you to write "assertions":
1 should be: 2
1 should equal: 10
And second allows you to instantiate specs by natural readable words:
Kind of: Number
Instance of: String
Equal to: 'test'
In new version new kind of should expressions added:

Boolean properties validation. You can send any message to "should be" which will be executed by receiver of "should" and then validated for truth:
1 should be between: 10 and: 50
#(1 2) should be isEmpty
Deep object state validation. You can send any series of messages to "object where" to write should expression for internal property:
(1@3) where x should be: 1
(0@1 corner: 10@20) where origin x should be: 0
Other expressions: 
1 should be: 2. "fail with message: Got '1' but it should be '2'"
1 should not be: 1. "fail with message: Got '1' but it should not be '1'"

3 should equal: 2. "fail with message: Got '3' but it should equal '2'"
3 should not equal: 3. "fail with message: Got '3' but it should equal '3'"

3 should beKindOf: String.
3 should not beKindOf: Number.

3 should beInstanceOf: Number.
3 should not beInstanceOf: SmallInteger.

#(1 2) should equal: #(10 20).
#(1 2) should equal: #(1 2) asOrderedCollection. "not fail because by default comparison not look at collection types"
#(1 2) should equal: #(1 2) asSet.
#(1 2) should equal: #(2 1). "not fail because by default equality between collections is not ordered"
#(1 2) should equalInOrder: #(2 1). "fail because it is explicit requirement for ordered equality"

#(1 2) should haveSize: 10.
#(1 2) should include: 10.
#(1 2) should include: 10 at: 1.
#(1 2) should include: (Instance of: String) at: 1.
#(1 2) should include: (Kind of: String) at: 2.

[1 + 2] should raise: ZeroDivide.
[1/0] should not raise: ZeroDivide.
[1/0] should raise: Error.
[1/0] should raise: (Kind of: Error).
[1/0] should fail.
[self error: 'test'] should raise: errorInstance. "fail because raised error is not the same as expected errorInstance"
[1 + 2] should not fail.

3 should be even.
2 should not be even.

3 should be between: 10 and: 50.
2 should not between: 1 and: 5.

#(1 2) should be isEmpty. "fail with message: #(1 2) should be isEmpty"
#() should not be isEmpty.

(1@3 corder: 20@30) where origin x should equal: 100. "fail with message: Got '1' from (1@3 corder: 20@30) origin x but it should equal: 100"
All expressions can be found in SpecOfShouldExpression class which you can extend with new keywords. SpecOfShouldExpressionTests describes them in tests.
Underhood should expression build concrete specification instance and validate subject of should expression by it.  Then concrete validation failure will signal SpecOfFailed exception. It makes possible to extend debugger tools to better analyse problem. Such tools can be specific for different kind of failures

Here I started series of posts on my contribution to Pharo project