viernes, noviembre 18, 2005

La magia del Smalltalk: Capítulo 13 – Expression Builder (Un ejemplo de #doesNotUnderstand:)

Ya hemos hablado, en un post anterior, del mensaje #doesNotUnderstand:; ese mensaje "especial", que envía el Smalltalk automáticamente, al objeto receptor, cuando este no tiene un método para responder al mensaje original.

En el un post anterior habíamos hablado de como se puede hacer un Proxy y otros ejemplos, y ahora veremos un ejemplo un poco más complicado, pero también más interesante.

El problema a resolver es el siguiente: Necesitamos crear expresiones válidas en algún lenguaje (por ejemplo SQL) desde sentencias escritas en bloques de Smalltalk.

Supongamos que queremos convertir un bloque tipo:

customers select: [:each | each income > 0]

En una sentencia SQL tipo:

SELECT * FROM Customer WHERE (Customer.income > 0)


NOTA: Vi por primera vez usar este truco en el framework de mapeo relacional-objetos llamado GLORP, de allí que use un ejemplo donde se genera sentencias SQL.



El truco es evaluar el bloque en cuestión con un objeto que sólo entienda el mensaje #doesNotUnderstand:. Esa evaluación “capturará" la información del mensaje y devolverá otro objeto capaz de seguir capturando esa información. El resultado final será una estructura que describa, en un árbol, la secuencia de mensajes (incluyendo receptores y argumentos).

BlockContext>>asExpression
    
"answer the receiver transformed as an EBExpression"

    
^ self value: EBArgumentExpression new



EBExpression>>doesNotUnderstand: aMessage
    
aMessage selector == #doesNotUnderstand:
        
ifTrue: [^ self basicDoesNotUnderstand: aMessage].

    
^ EBMessageExpression
            
receiver: self
            
message: aMessage.



El siguiente test muestra uno de los casos:

EBTest>>testSimplestUnary
    
| expected result |

    
expected := EBMessageExpression
                        
receiver: EBArgumentExpression new
                        
selector: #isNil.

    
result := [:param | param isNil] asExpression.

    
self should: [result equals: expected].



Y, para terminar, vamos a crear un par de métodos llamados #asLispyString en Object (para que todos los objetos puedan convertirse a un string “Lispy") y en la jerarquía de EBExpression:

Object>>asLispyString
    
^ self asString


EBArgumentExpression>>asLispyString
    
^ '[arg]'


EBMessageExpression>>asLispyString
    
| result |
    
result := String new writeStream.
    
    
result nextPutAll:'('.
    
result nextPutAll: self selector asString.

    
result space.
    
result nextPutAll: self receiver asLispyString.
    
    
self arguments do:[:each |
            
result space.
            
result nextPutAll: each asLispyString.
        ].
    
    
result nextPutAll:')'.
    
    
^ result contents.


De esa manera podemos escribir algunos tests como estos:

EBTest>>testLispyString1
    
| result |
    
    
result := [:param | param isNil] asExpression.
    
    
self should: [result asLispyString = '(isNil [arg])'].


EBTest>>testLispyString2

    
| block1 block2 result |

    
block1 := [:each | each > 1].
    
block2 := [:each | each < 10].

    
result := [:each | (block1 value: each) & (block2 value: each)] asExpression.

    
self should: [result asLispyString = '(& (> [arg] 1) (< [arg] 10))'].





Espero les guste el ejemplo.

Pueden bajar un changeset con el ejemplo completo: ExpressionBuilder-dgd.4.cs.gz