AjLisp en Ruby (1) Estructura, Clases y Tests

Published on Author lopezLeave a comment

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

Leave a Reply

Your email address will not be published. Required fields are marked *