Estoy aprendiendo y practicando Ruby, y como es costumbre, lo hago escribiendo algo interesante para mí: el intérprete AjLisp (hace unos meses lo implementé en Javascript). TDD es mi amigo: escribo un test, lo ejecuto en rojo, codifico para pasarlo a verde, refactorear, y así sigue. El código de este nuevo intérprete, trabajo en progreso, en:
https://github.com/ajlopez/AjLispRb
Inicialmente (pueden ver los logs) escribí todo en un solo archivo (código y tests), siguiendo el simple y claro ejemplo:
http://kanemar.com/2006/03/04/screencast-of-test-driven-development-with-ruby-part-1-a-simple-example/
Vean que Ruby tiene un paquete ‘test/unit’ que ya viene en su instalación, listo para usar. Despues de algo de “research”, dividí el archivo en código de producción y código de pruebas. Quiero llegar a armar una gema (un paquete Ruby, distribuido por el utilitario gems), así que estudié los primeros pasos del tutorial:
http://guides.rubygems.org/
Tengo pendiente de leer y estudiar:
http://speakerdeck.com/u/pat/p/cut-polish-a-guide-to-crafting-gems
http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html
Así que mi código aún no es una gema. Pero va teniendo la estructura de una:
El directorio lib contiene un solo archivo ajlisp.rb:
require 'ajlisp/list.rb'
require 'ajlisp/named_atom.rb'
require 'ajlisp/context.rb'
require 'ajlisp/string_source.rb'
require 'ajlisp/token.rb'
require 'ajlisp/lexer.rb'
require 'ajlisp/parser.rb'
require 'ajlisp/primitive.rb'
require 'ajlisp/primitive_first.rb'
require 'ajlisp/primitive_rest.rb'
require 'ajlisp/primitive_cons.rb'
require 'ajlisp/primitive_list.rb'
require 'ajlisp/primitive_closure.rb'
require 'ajlisp/fprimitive.rb'
require 'ajlisp/fprimitive_quote.rb'
require 'ajlisp/fprimitive_lambda.rb'
require 'ajlisp/fprimitive_flambda.rb'
require 'ajlisp/fprimitive_let.rb'
require 'ajlisp/fprimitive_closure.rb'
require 'ajlisp/fprimitive_define.rb'
require 'ajlisp/primitive_add.rb'
module AjLisp
@context = Context.new
@context.setValue "quote", FPrimitiveQuote.instance
@context.setValue "first", PrimitiveFirst.instance
@context.setValue "rest", PrimitiveRest.instance
@context.setValue "cons", PrimitiveCons.instance
@context.setValue "list", PrimitiveList.instance
@context.setValue "lambda", FPrimitiveLambda.instance
@context.setValue "flambda", FPrimitiveFLambda.instance
@context.setValue "let", FPrimitiveLet.instance
@context.setValue "define", FPrimitiveDefine.instance
@context.setValue "+", PrimitiveAdd.instance
def self.context
return @context
end
def self.evaluate(context, item)
if item.is_a? List or item.is_a? NamedAtom
return item.evaluate(context)
end
return item
end
end
Escribí algunas primitivas (formas normales, y formas especiales: estas últimas no evalúan sus parámetros antes de su aplicación, ejemplos: quote y define). Noten que los archivos adicionales los puse en un subdirectorio ajlisp dentro de lib, ¿por qué? Porque cuando este código sea instalado como una gema, todo el directorio lib estará disponible para require, y si hubiera un archivo ahí, se podría hacer require(‘elarchivo’). Es por eso que los archivos adicionales a ajlisp.rb se colocan en otro lado, evitando colisión de nombres. Se recomienda colocarlos debajo de lib (vean el código de gemas que tienen en su instalación de Ruby, o vean ejemplos en GitHub).
El el directorio test hay un archivo test.rb que incluye a los otros archivos de tests:
require 'ajlisp'
require 'test/unit'
require "test_list.rb"
require "test_named_atom.rb"
require "test_context.rb"
require "test_string_source.rb"
require "test_token.rb"
require "test_lexer.rb"
require "test_parser.rb"
require "test_primitive_first.rb"
require "test_primitive_rest.rb"
require "test_primitive_cons.rb"
require "test_primitive_list.rb"
require "test_primitive_closure.rb"
require "test_primitive_add.rb"
require "test_fprimitive_quote.rb"
require "test_fprimitive_lambda.rb"
require "test_fprimitive_let.rb"
require "test_fprimitive_closure.rb"
require "test_fprimitive_flambda.rb"
require "test_fprimitive_define.rb"
require "test_evaluate"
Pueden ejecutar los tests desde la línea de comando:
ruby –Ilib;test test\test.rb
En Windows, dejé el archivo runtest.cmd conteniendo esta línea. Los parámetros –Ilib;test le indican a Ruby que incluya los directorios lib y test para cuando tenga que resolver un require. De esta forma evito poner directorios explícitos (o usar __FILE__) en los require.
Algo de tests:
require 'ajlisp'
require 'test/unit'
class TestList < Test::Unit::TestCase
#...
def test_create_with_first
list = AjLisp::List.new("foo")
assert_equal("foo", list.first)
assert_nil(list.rest)
end
def test_create_with_first_and_rest
rest = AjLisp::List.new("bar")
list = AjLisp::List.new("foo", rest)
assert_equal("foo", list.first)
assert_not_nil(list.rest)
assert_equal("bar", list.rest.first)
assert_nil(list.rest.rest)
end
def test_create_from_array
list = AjLisp::List.make [1, "a", "foo"]
assert_not_nil list
assert_equal 1, list.first
assert_equal "a", list.rest.first
assert_equal "foo", list.rest.rest.first
assert_nil list.rest.rest.rest
end
#..
end
Cada lista en AjLisp es un objeto de esta clase, list.rb:
module AjLisp
class List
attr_reader :first
attr_reader :rest
def initialize(first=nil, rest=nil)
@first = first
@rest = rest
end
def evaluate(context)
form = AjLisp::evaluate(context, @first)
form.evaluate(context, self)
end
def self.make(array)
if array and array.length > 0
first = array.shift
if first.is_a? Array
first = make(first)
elsif first.is_a? Symbol
first = NamedAtom.new first.to_s
end
return List.new first, make(array)
end
return nil
end
end
end
Los méteodos de acceso first y rest son de sólo lectura. Gracias a la naturaleza no tipada de Ruby (facilidad que también encontré en la implementación de Javascript) la implementación de este intérprete es directa, sin mayor “ceremonia de código”.
En mis nuevos tests, ahora incluye el código DENTRO del módulo AjLisp, así me evito de escribir el prefijo AjLisp:: antes de referenciar a una clase:
require 'ajlisp'
require 'test/unit'
module AjLisp
class TestLexer < Test::Unit::TestCase
def test_get_atom_token
source = StringSource.new "atom"
lexer = Lexer.new source
token = lexer.nextToken
assert_not_nil token
assert_equal "atom", token.value
assert_equal TokenType::ATOM, token.type
assert_nil lexer.nextToken
end
def test_get_atom_token_with_spaces
source = StringSource.new " atom "
lexer = Lexer.new source
token = lexer.nextToken
assert_not_nil token
assert_equal "atom", token.value
assert_equal TokenType::ATOM, token.type
assert_nil lexer.nextToken
end
#...
end
Próximos tópicos: algunos detalles de implementación, primitives vs fprimitives, contexto (ambiente anidado con pares nombre/valor), lambdas y closures, el lexer y el parser.
Próximos pasos: completar las primitivas (let, letrec, definef, do, if…), macro (mlambda, definem, expansión de macros…)
Nos leemos!
Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez