4 Trabes

Rails a través de una extensión de la clase Module en ActiveSupport permite delegar métodos de manera comoda. Veamos un ejemplo, para entenderlo:

class A
  def a
    'a'
  end 
  def aa
    'aa'
  end
end

class B
  attr_accessor :the_a

  def initialize(the_a)
    @the_a = the_a
  end    

  delegate :a, :to => :the_a
end

the_b = B.new(A.new)
the_b.a    # => "a" 

Interesante, pero con limitaciones. Probemos otras posibiliades:

class B
  delegate :a2, :to => :the_a
end

the_b = B.new(A.new)
the_b.a2  # => NoMethodError: undefined method `a2' ...

the_b = B.new(nil)
the_b.a     # => NoMethodError: You have a nil object...
the_b.a2   # => NoMethodError: You have a nil object...
El código causante de este comportamiento tiene la siguiente pinta:
def delegate(*methods)
  options = methods.pop
  unless options.is_a?(Hash) && to = options[:to]
    raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)." 
  end

  methods.each do |method|
    module_eval("def \#{method}(*args, &block)\n\#{to}.__send__(\#{method.inspect}, *args, &block)\nend\n", "(__DELEGATION__)", 1)
  end
end

Esto no nos agrada del todo, ya que lo interesante sería que se pudiesen escribir cosas como las siguientes sin preocuparse:

p1.product.name   # => 'FypURL'
p1.product.brand.name   # => "Trabe" 
p1.product.brand_name   # => "Trabe" 
p2.product.name   #  =>  "Unbranded product" 
p2.product.brand.name   # =>  NoMethodError: You have a nil object...
p2.product.brand_name   # => "unknown" 

Aqui os pongo nuestro Module#delegate_method que permite todo esto:

class Module

  def delegate_method(method, options)    

    to,to_method = options[:to].to_s.split('.')
    to_method = method if to_method.nil?
    default = options[:default]

    code = %{
      def #{method}(*args, &block)
        if #{to}        
           #{to}.__send__(#{to_method.inspect}, *args, &block) || #{default.inspect}
        else 
           #{default.inspect}
        end
      end
      }

  module_eval code, __FILE__, __LINE__    
  end 
end
Esta versión se diferencia de Module#delegate_method anterior en lo siguiente:
  • No reescribimos el método original ya que la implementación no es compatible al 100% con la anterior. No soportamos la definición de multiples delegaciones y no queremeos que código ya escrito deje de funcionar. Si queremos delegar del modo Rails seguimos teniendo Module#delegate. La posibilidad de definir varias delegaciones en una sola sentencia no es una característica fundamental (para delegar muchos métodos a lo mejor compensa trastear con el method_missing).

  • No chequeamos la existencia del parametro :to (presuponemos que vamos a escirbir correctamente el código y si no es asís ya saltará otra expceción.
  • Permitimos delegar en un método diferente al método delegado. Esto es super útil como se vió en el ejemplo de productos y marcas.

  • Si no existe el delegado o éste no tiene valor obtenemos nil y no una sucia excpeción, o lo que es mejor, obtenemos un valor por defecto.

Probemos de nuevo el código con el que abrimos el post sutituyendo delegate por delegate_method

class B
  ... 
  delegate_method :a, :to => :the_a, :default => 'default'
  delegate_method :aa, :to => 'the_a.aa'
end

the_b = B.new(A.new)
the_b.a       # => "a" 
the_b.aa     # => "aa" 

the_b = B.new(nil)
the_b.a      # => default
the_b.aa    # => nil

Una necesidad típica a la hora de programar es copiar o clonar objetos. Los métodos de Ruby Object#clone y Object#dup realizan copias superficiales (shallow), es decir, si el objeto contiene referencias a otros objetos se duplican las referencias pero no los objetos apuntados. Si la copia fuese profunda (deep), también se duplicarían los objetos referenciados. Ejemplo:

h = { 1 => ['a','b'], 2 => ['c'] }
h1 = h.clone
h2 = h.deep_clone
h1[1].shift     # => "a" 
h    # =>  {1=>["b"], 2=>["c"]}
h1  # =>  {1=>["b"], 2=>["c"]}
h2  # =>  {1=>["a", "b"], 2=>["c"]}

Lo primero que se nos puede ocurrir es crear un método deep_clone específico para cada objeto (o incluso uno más genérico que, mediante introspección del objeto supiese que copiar). Ésto es tedioso y tiene sus problemas (básicamente el meanejo de referencias cruzadas), además, eso no es DRY. Por fortuna Ruby tiene soporte para serialización (módulo Marshal). El truco es sencillo: serializo, deserializo.

class Object
  def deep_clone
    # Abracadabra !!!
    Marshal::load(Marshal::dump(self))
  end
end

Sencillo, bello, y todas esas cosas que tanto gustan de Ruby.

A esta técnica sólo podemos ponerle dos pegas: 1) si una parte de la clase no es serializable no podremos copiarla, y 2) es cierto que una implementación ad hoc de copia puede ser más rápida (pero yo, al menos, estoy dispuesto a perder esa velocidad).

PD: Por cierto, esta técnica tan sencilla es trasladable directamente a cualquier otro lenguaje/plataforma con soporte para serialización: Java, .NET, etc.

Ponencia de Trabe Soluciones aceptada en la Conferencia Rails 2007

Publicado por el Martes, 23 de Octubre de 2007

Ayer, la organización de la Conferencia Rails 2007 me confirmó la inclusión de una de nuestras ponencias en el programa de las conferencias. Podéis ver la relación completa en la dirección http://ponencias.conferenciarails.org.

De las dos propuestas que enviamos aceptaron una que titulamos Proyectos de bajo coste con limitaciones severas de tiempo y recursos y dejaron fuera otra de APIs Rest y proxificación, lo cual me ha sorprendido bastante. Sin quitarle interés a la primera ponencia, creo que la segunda era mucho más interesante y amena (mucho más tecnológica y menos empresarial). En cualquier caso nos vemos en la Conferencia Rails en Noviembre.

PD: Aunque el que escribe aparece como ponente en la página web, debo decir que este punto no está confirmado y puede dar la charla cualquier otro miembro de Trabe. Más adelante os confirmo a quién le podéis tirar los tomates el día de la charla.

Symbol#to_proc de rails y otras alternativas (o locuras, según se mire)

Publicado por el Jueves, 18 de Octubre de 2007

Este post es consecuencia directa de un comentario de Sergio Gil a mi post anterior: Extensiones al core de Ruby, pequeños pedazos de código que harán tu vida más sencilla. Hoy Enumerable.pluck y Enumerable.invoke.

Sergio, acertadamente comenta: ActiveSupport añade a each y a collect/map una versión abreviada de la sintaxis (realmente lo que hace es definir Symbol#to_proc, si no me equivoco) que la deja muy parecida a lo que habéis creado, que dejaría el ejemplo así:
objects.collect(&:key)

a lo que nosotros respondemos: Tienes razón Sergio, existe esa posibilidad, aunque aquí en Trabe consideramos que es una construcción un poco oscura. Por otro lado la versión que utilizamos nosotros, permite hacer alguna cosa adicional fácilmente, por ejemplo, utilizar una cadena como parámetro en lugar de un símbolo (que puede considerarse algo menor) o más interesante, pasar parámetros a la función que se invoca (que como pone el post, queda como ejercicio para el lector ;)
Ejemplo práctico:
%w(un ejemplo).pluck(’*’,2) => [“unun”, “ejemploejemplo”]

Aprovecho ahora para resolver el ejercicio de programación que os propuse:
module Enumerable
  def invoke(method_name, *args)    
    self.each { |i| i.send(method_name, *args) }
  end
  def pluck(method_name, *args)    
    self.collect { |i| i.send(method_name, *args) }
  end  
end

Hasta aquí todo bien. Lo que pasa, es que cuando se escribe en un blog y se reciben comentarios uno se ve obligado a realizar un ejercicio mental y reflexionar mucho sobre lo que escribe. Así que me he tomado un ratito para investigar algo más sobre este tema.

La primera carencia que apuntamos acerca de la solución con Symbol#to_proc es fácilmente superable, sólo necesitamos definir nuestro propio String#to_proc. La falta de parámetros en el método es algo que me llama especialmente la atención. Estuve haciendo unas pruebas en la consola, y no conseguí pasar nigún parametro, así que me fui al código fuente y esto es lo que pone:

def to_proc
  Proc.new { |*args| args.shift.__send__(self, *args) }
end

Dado que no soy un gurú ni un semidios de Ruby no puedo afirmar esto con rotundidad, pero, a primera vista parece que to_proc devolvería un Proc capaz de recibir un número variable de argumentos.

Ante la duda, Google, porque no estamos sólos en elmundo.

Lo que descubrí, a parte de confirmar que Symbol#to_proc no puede recibir argumentos, es la Methodphitamine (algo así como la método-fetamina), una suerte de locura sintáctica que permite hacer algo como:

(1..100).select &it % 2 == 0
File.read("/etc/passwd").split.sort_by &it.split(":")[2]
User.find(:all).map &its.contacts.map(&its.last_name.capitalize)

¡Qué locura!, ¿Efectos de la droga?, ¿Tendrá este hombre que mear en un vaso y pasar un anti-dopping? en fin. Yo, personalmente, comulgo con un comentario de Sam Aaron aparecido en The Methodphitamine: A Better Symbol#to_proc?. I’m not a big fan of the to_proc hack in the first place. To my eyes, it’s ugly, and Methodphitamine is even more ugly. I believe that there’s a balance to be struck between readability and succinctness. I think that the Ruby philosophy (or way) leans more towards the readability side of the spectrum. Check out Matz’s essay in the new O’Reilly book ‘Beautiful Code’ where he likens code to an essay. i.e. essays generally have messages, but the message is mostly meaningless unless it can be read and understood by other people. It’s a good read. Beautiful Code es un libro que tengo en casa en la cola de lectura, cuando le eche un vistazo os comento.

De todos modos, quién este interesado en este tipo de syntactic sugar también puede echarle un ojo a Array#to_proc – Nested properties for to_proc hack, que es una solución para el encadenamiento más legible y en la línea de Symbol#to_proc o bien New magical version of Symbol.to_proc que permite escribir algo como list.map &:to_i elegantemente como list.to_is con una extensión que sólo puede considerarse como “code hell”.

En Trabe Soluciones vamos mejorando las librerías que utilizamos para desarrollar con Rails aplicación a aplicación. Puede decirse que somos como una esponja que va cogiendo buenas ideas de aquí y de allá. Hoy vamos a hablar de una par de extensiones al core de Ruby que utilizamos,

Primero el código:

module Enumerable
  def invoke(method_name)    
    self.each { |o| o.send(method_name) }
  end    
  def pluck(method_name)    
    self.collect { |o| o.send(method_name) }
  end  
end

Cómo podeís apreciar son unas sencillas funciones que solucionan dos tareas más que habituales cuando trabajamos con colecciones: invoke que invoca un método de cada elemento de un Enumerable y pluck que recolecta el resultado de invocar un método concreto sobre cada elemento de un Enumerable. Vamos, que abrevian dos construcciones típicas del lenguaje. Las hemos definido en el módulo Enumerable y no en la clase Array directamente para poder utilizarlas con cualquier clase que incluya este módulo (Array por ejemplo).

La inspiración de estas funciones se la debemos a Prototype, la famosa librería Javascript, que curiosamente se inspira a su vez en ruby: Most of Prototype’s idioms are influenced by Ruby, and those that have no analog in Ruby defer to existing or emerging web standards

Para terminar un ejemplo autoexplicativo de su utilidad.
# ANTES
def find_by_object_keys(objects)
  find(:conditions => { :key => objects.collect { |o| o.key} }
end

# DESPUES
def find_by_object_keys(objects)
  find(:conditions => { :key => objects.pluck(:key) }
end

Ejercicio para lectores avispados: Ahora sólo tenéis que modificar las funciones para que acepten parámetros para el método invocado. Seguramente lo necesitéis en algún momento.

El plugin que recomendamos hoy es annotate_models. Esta pequeña extensión ni modifica ni añade comportamiento a nuestras aplicaciones Rails y tampoco nos evita escribir código. No obstante, no debemos despreciar su valiosa ayuda.

annotate models añade una simpática tarea annotate_models al rakefile del proyecto que se ocupa de añadir al comienzo de los ficheros .rb que contienen modelos ActiveRecord un comentario con la información del schema. Esto es, nos anota los modelos con los atributos de la tabla de base de datos asociada al modelo. Veamos una anotación real como ejemplo:

# == Schema Information
# Schema version: 34
# Annotated on: Wed Oct 10 10:38:58 +0200 2007
#
# Table name: brands
#
#  id         :integer(11)   not null, primary key
#  name_es    :string(255)   
#  name_en    :string(255)   
#  created_at :datetime      not null
#  updated_at :datetime      not null
#  image      :string(255)   
#  complete   :boolean(1)    
#  url        :string(255)
#  margin     :integer(11)   
#

Pequeño pero matón. Una gran ayuda mientras el soporte de los IDE de desarrollo como Netbeans o RadRails no mejore y autocompleten como Dios manda.

Sólo dos pegas (pequeñas, sí, pero ahi están). La primera: siempre anota los modelos, aun cuando no ha cambiado el esquema. Si trabajamos con un control de versiones (porque trabajamos con control de versiones ¿no?) el histórico de revisiones queda un pelín falseado, porque los ficheros han cambiado (esa línea Annotated on maldita), aunque no hayamos modificado el código.

La segunda: sólo anota los modelos de la carpeta model, cuando lo óptimo es que buscase por toda la carpeta del proyecto buscando subclases de ActiveRecord::Base. Si tenéis modelos en otra carpeta tendréis que modifcar el código fuente del plugin (que nadie se asuste, es bastante sencillo).

Resumiendo: anotad vuestros modelos

Este es el segundo post de la fabulosa serie “Rails Plugins”