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:
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:
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 endEscribí 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 #.. endCada 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 endLos 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 #... endPró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