This is some sample text. Just learning to use BlueCloth. Just a simple test. Las dos primeras líneas es el código específico de RubyGems. La primera línea carga las bibliotecas del núcleo RubyGems que vamos a necesitar para trabajar con las gemas instaladas. require ‘rubygems’
La segunda línea es donde está la mayor parte de la magia.
require_gem ‘BlueCloth’, “>= 0.0.4” Esta línea añade la gema BlueCloth a la variable Ruby $LOAD_PATH y utiliza require para cargar todas las bibliotecas que el creador de la gema especifica que se cargen automáticamente. Cada joya es considerada como un conjunto de recursos. Puede contener un archivo de biblioteca o cien. En una biblioteca antigua, no RubyGems, todos estos archivos se copiaban en algún lugar compartido en el árbol de la biblioteca Ruby, un lugar que estaba en la ruta de carga predefinida de Ruby. RubyGems no funciona de esta manera. Por el contrario, mantiene a cada versión de cada gema en su propio árbol de directorios autónomo. Las gemas no se añaden a los directorios de la biblioteca estándar de Ruby. Como resultado, RubyGems tiene que hacer algunas filigranas para que se pueda llegar a estos archivos. Esto se logra mediante la adición del árbol de directorios de la gema a la ruta de carga de Ruby. Desde el interior de un programa en ejecución, el efecto es el mismo: sólo funciona require. Desde el exterior sin embargo, RubyGems le da mucho más control sobre lo que está cargado en sus programas Ruby. En el caso de BlueCloth, el código de plantillas se distribuye como un archivo, bluecloth.rb. Este es el archivo que carga require_gem, que además tiene un segundo argumento opcional, que especifica un requerimiento de versión. En este ejemplo, se ha especificado que la versión 0.0.4 o superior de BlueCloth debe estar instalada para utilizar este código. Si hubiera requerido la versión 0.0.5 o superior, este programa hubiera fallado, porque la versión que acaba de instalar es demasiado baja para cumplir con los requisitos del programa. require ‘rubygems’ require_gem ‘BlueCloth’, ‘>= 0.0.5’ produce: /usr/local/lib/ruby/site_ruby/rubygems.rb:30: in `require_gem’: (LoadError) RubyGem version error: BlueCloth(0.0.4 not >= 0.0.5) from prog.rb:2
147
Como hemos dicho anteriormente, el argumento de requerimiento de versión es opcional y este ejemplo es obviamente artificial. Sin embargo, es fácil imaginar cómo esta función puede ser útil cuando diferentes proyectos comienzan a depender de múltiples y potencialmente incompatibles versiones de la misma biblioteca. El código entre bastidores
¿Qué es lo que sucede detrás del escenario cuando se llama al método mágico require_gem?
En primer lugar, la biblioteca de gemas modifica su $LOAD_PATH, incluyendo cualquier directorio que se haya añadido a la require_paths de la especificación de gema. En segundo lugar, se llama al método require para cualquiera de los archivos especificados en el atributo autorequires de la especificación de gema (se describe más adelante). Es así como la modificación de la conducta de $LOAD_PATH permite a RubyGems gestionar múltiples versiones instaladas de la misma biblioteca.
Dependencias en RubyGems Los lectores astutos se habrán dado cuenta de que el código que hemos creado hasta ahora depende de que el paquete RubyGems esté instalado. A largo plazo es una apuesta bastante segura (nos imaginamos) que RubyGems hará su camino en la distribución principal de Ruby. Por ahora, sin embargo, RubyGems no es parte de la distribución estándar de Ruby, por lo que si distribuimos código con require “rubygems” en él, ese código fallará. Se pueden utilizar al menos dos técnicas para lograr solucionar este problema. En primer lugar, se puede ajustar el código RubyGems específico en un bloque y utilizar el manejo de excepciones Ruby para rescatar el LoadError resultante durante el require. begin require ‘rubygems’ require_gem ‘BlueCloth’, “>= 0.0.4” rescue LoadError require ‘bluecloth’ end Este código intenta en primer lugar el require de la biblioteca RubyGems. Si esto falla, se invoca la línea del rescue y el programa intentará cargar BlueCloth con un require convencional. Esto último producirá un error si BlueCloth no está instalada, que es el mismo comportamiento que los usuarios verán si no utilizan RubyGems. Por otra parte, RubyGems puede generar e instalar un archivo de código auxiliar durante la instalación de la gema. Este archivo se inserta en la ubicación de la biblioteca estándar de Ruby y llevará el nombre del paquete con los contenidos de la gema (de modo que el archivo auxiliar de BlueCloth se llamará bluecloth.rb). Los usuarios que utilicen esta biblioteca pueden simplemente poner require ‘bluecloth’ Esto es exactamente lo que se habría puesto en los días pre RubyGems. La diferencia ahora es que en lugar de cargar BlueCloth directamente, en su lugar va a cargar el fichero auxiliar que a su vez llama a require_gem para cargar el paquete correcto. Un archivo de código auxiliar para BlueCloth sería algo como esto: require ‘rubygems’ $”.delete(‘bluecloth.rb’) require_gem ‘BlueCloth’ El archivo auxiliar mantiene todo el código RubyGems específico en un solo lugar, por lo que las bibliotecas dependientes no necesitan incluir ningún código RubyGems en su fuente. La llamada
148
require_gem carga todo los archivos de biblioteca que mantenedor de la gema ha especificado que cargen automáticamente. A partir de RubyGems 0.7.0, la instalación de los archivos auxiliares está activada por defecto. Durante la instalación, se puede desactivar con la opción - no-install-stub. La mayor desventaja de la utilización de estos archivos auxiliares es que se pierde la capacidad de RubyGems para gestionar múltiples versiones instaladas de la misma biblioteca. Si necesita una versión específica de una biblioteca, es mejor utilizar el método LoadError descrito anteriormente.
Crear su Propia Gema
Por ahora, hemos visto lo fácil que RubyGems hace las cosas para los usuarios de una aplicación o una biblioteca. Probablemente esté listo para hacer una gema por su cuenta. Si va a crear código para compartir con la comunidad de código abierto, RubyGems es una forma ideal de cara a los usuarios finales para descubrir, instalar y desinstalar el código. También constituye una forma eficaz de gestionar los proyectos internos de empresa o incluso proyectos personales, ya que hace las actualizaciones y restauraciones tan simples. En última instancia, la disponibilidad de más gemas hace más fuerte la comunidad Ruby. Estas gemas tienen que venir de algún lugar y ahora vamos a mostrar cómo pueden comenzar a venir de usted. Digamos que ha conseguido por fin terminar la aplicación que alguien le solicitó, el diario en línea, “MiRegistro”, y que ha decidido liberarlo bajo una licencia de código abierto. Naturalmente, usted desea liberar MiRegistro como una gema (a la gente le encanta que le den joyas).
Diseño del Paquete La primera tarea en la creación de una gema es la organización del código en una estructura de directorios que tenga sentido. Las mismas reglas que se utilizan en la creación de un archivo tar o zip típicos, se aplican en la organización de paquetes. Algunos convenios generales a continuación. • Coloque todos los archivos de código fuente Ruby en un subdirectorio llamado /lib. Más tarde, le mostraremos cómo asegurarse de que este directorio se añade a $LOAD_PATH cuando los usuarios cargan la gema. • Si es apropiado para su proyecto, incluya un archivo en lib/yourproject.rb que realice los necesarios comandos require para cargar la mayor parte de la funcionalidad del proyecto. Antes que la característica RubyGems autorequire, esto hace las cosas más fáciles para que otros puedan usar una biblioteca. Incluso para RubyGems, hace más fácil que otros puedan explorar su código si se les da un punto de partida obvio. • Incluya siempre un archivo README que contenga un resumen del proyecto, información de contacto del autor e indicaciones para empezar. Utilice el formato RDoc para que se pueda agregar a la documentación que se genera durante la instalación de la gema. Recuerde que debe incluir los derechos de autor y de licencia en el archivo README, ya que muchos usuarios comerciales no van a usar un paquete a menos que los términos de la licencia sean claras. • Las pruebas deben ir en un directorio llamado test/. Muchos desarrolladores utilizan una librería de pruebas de unidad como una guía de uso. Es bueno ponerla en algún lugar predecible, lo que facilita a los demás el poderla encontrar. • Cualquier script ejecutables deben ir en un subdirectorio denominado bin/. • El código fuente para las extensiones de Ruby debe ir en ext/. • Si usted tiene una gran cantidad de documentación para incluir en su gema, es bueno mantenerla en su propio subdirectorio llamado docs/. Si el archivo README está en el nivel superior del paquete, asegúrese de referenciar a los lectores a este lugar.
Esta estructura de directorios se ilustra en la figura 10 un poco más adelante.
149
La especificación de Gema Ahora que tiene los archivos establecidos como deseaba, es el momento de llegar al corazón de la creación de la gema: la especificación de gema, o gemspec. Un gemspec es una colección de metadatos en Ruby o YAML que proporciona información clave sobre su gema. El gemspec se utiliza como entrada para el proceso de construcción de la gema. Puede utilizar diferentes mecanismos para crear una gema, pero todos son conceptualmente lo mismo. A continuación la primera y básica gema MiRegistro: require ‘rubygems’ SPEC = Gem::Specification.new do |s| s.name = “MiRegistro” s.version = “1.0.0” s.author = “Jo Programmer” s.email = “[email protected]” s.homepage = “http://www.joshost.com/MiRegistro” s.platform = Gem::Platform::RUBY s.summary = “An online Diary for families” candidates = Dir.glob(“{bin,docs,lib,tests}/**/*”) s.files = candidates.delete_if do |item| item.include?(“CVS”) || item.include?(“rdoc”) end s.require_path = “lib” s.autorequire = “miregistro” s.test_file = “tests/ts_miregistro.rb” s.has_rdoc = true s.extra_rdoc_files = [“README”] s.add_dependency(“BlueCloth”, “>= 0.0.4”) end Vamos a recorrer rápidamente este ejemplo. Los metadatos de una gema se llevan a cabo en un objeto de clase Gem::Specification. El gemspec puede expresarse en YAML o código Ruby. Aquí vamos a mostrar la versión Ruby, ya que generalmente es más fácil de construir y más flexible de utilizar. Los cinco primeros atributos en la especificación dan información básica como el nombre de la gema, la versión, el nombre del autor, correo electrónico y página principal. En este ejemplo, el siguiente atributo es la plataforma sobre la que esta gema se puede ejecutar. En este caso, la gema es una biblioteca pura de Ruby con ningún sistema operativo específico en los requerimientos, por lo que hemos establecido la plataforma en RUBY. Si esta gema hubiera sido escrita sólo para Windows, por ejemplo, la plataforma debería estar listada como Win32. Por ahora, este campo sólo es informativo, pero en el futuro será utilizado por el sistema de gemas para la selección inteligente de la extension de gemas precompiladas nativas. El sumario de la gema es la descripción breve que aparece cuando se ejecuta gem query (como en nuestro ejemplo anterior con BlueCloth). El atributo files es un conjunto de rutas de acceso a los archivos que se incluirán cuando se construye la gema. En este ejemplo, hemos utilizado Dir.glob para generar la lista y filtrar los ficheros CVS y RDoc.
Magia en Tiempo de Ejecución Los siguientes dos atributos, require_path y autorequire, le permiten especificar los directorios que se agregarán a $LOAD_PATH cuando require_gem carga la gema, así como cualquier otro archivo que se cargará automáticamente usando require. En este ejemplo, lib se refiere a una ruta relativa al directorio MiRegistro, y autorequire hará que se requiera lib/miregistro.rb cuando se llama a require_gem “MiRegistro”. Para cada uno de estos dos atributos, RubyGems ofrece sus correspondientes versiones, require_paths y autorequire, que toman matrices, lo que permite tener muchos archivos cargados automáticamente a partir de diferentes directorios, cuando la gema se carga mediante require_gem.
150
Agregar Pruebas y Documentación El atributo test_file contiene el nombre de ruta relativa a un único archivo Ruby incluido en la gema y que debe ser cargado como un Test::Unit (se puede usar la forma plural, test_files, para hacer referencia a una serie de archivos que contiengan las pruebas). Para más detalles sobre cómo crear un conjunto de pruebas, consulte el capítulo sobre las pruebas unitarias. Para terminar con este ejemplo, tenemos dos atributos que controlan la producción de documentación local de la gema. El atributo has_rdoc especifica que se han añadido comentarios RDoc al código. Es posible ejecutar RDoc sin absolutamente ningún comentario, proporcionando una vista navegable de sus interfaces, pero obviamente esto es mucho menos valioso que el funcionamiento de RDoc con el código bien comentado. has_rdoc es una forma de decirle al mundo: “Si. Vale la pena generar la documentación de esta gema“. RDoc tiene la ventaja de ser muy legible por lo que es una excelente opción para un archivo README incluido en un paquete. Por defecto, el comando rdoc sólo se ejecutará en los archivos de código fuente. El atributo extra_rdoc_file toma una serie de rutas de acceso a los archivos no fuente de la gema que se quisieran incluir en la generación de documentación la RDoc.
Añadir Dependencias
Para que su gema funcione correctamente, los usuarios van a necesitar tener instalado BlueCloth.
Hemos visto anteriormente cómo establecer una dependencia de versión en tiempo de carga para una librería. Ahora tenemos que inicar a nuestro gemspec esta dependencia, para lo que el instalador se asegure de que esté presente durante la instalación de MiRegistro. Lo hacemos con la adición de una única llamada al método de nuestro objeto Gem::Specification. s.add_dependency(“BlueCloth”, “>= 0.0.4”) Los argumentos al método add_dependency son idénticos a los de require_gem que hemos explicado antes.
Después de la generación de esta gema, el intentar instalarla en un sistema limpio sería algo como:
% gem install pkg/MiRegistro-1.0.0.gem Attempting local installation of ‘pkg/MiRegistro-1.0.0.gem’ /usr/local/lib/ruby/site_ruby/1.8/rubygems.rb:50:in `require_gem’: (LoadError) Could not find RubyGem BlueCloth (>= 0.0.4) Debido a que se está realizando una instalación local desde un archivo, RubyGems no intentar resolver la dependencia. Por el contrario, falla estrepitosamente y le dice que necesita BlueCloth para completar la instalación. A continuación, se puede instalar BlueCloth como lo hacíamos antes, y las cosas irán bien la próxima vez que se intente instalar la gema MiRegistro. Si había subido MiRegistro al repositorio central de RubyGems y luego trata de instalarla como gema en un sistema limpio, se le pedirá que instale automáticamente BlueClot como parte de la instalación de MiRegistro.
% gem install -r MiRegistro Attempting remote installation of ‘MiRegistro’ Install required dependency BlueCloth? [Yn] y Successfully installed MiRegistro, version 1.0.0 Ahora tienes ambas instaladas, BlueCloth y MiRegistro y tu amigo puede empezar alegremente la publicación de su diario. Si se hubiera optado por no instalar BlueCloth, la instalación habría fallado como lo hizo durante el intento de instalación local.
A medida que se añadan más funciones a MiRegistro, puede que nos encontremos que necesitemos 151
gemas externas adicionales para apoyar esas características. Al método add_dependency se le puede llamar varias veces en una sola gemspec, soportando cualesquiera dependencias que se necesiten.
Extensión de Ruby Gems Hasta ahora, todos los ejemplos que hemos visto han sido código Ruby puro. Sin embargo, muchas librerías Ruby se crean como extensiones nativas. Hay dos formas de empaquetar y distribuir este tipo de librerías como una gema. Se puede distribuir la gema en formato fuente y que el instalador compile el código en tiempo de instalación. Alternativamente, se pueden precompilar estas extensiones y distribuir una gema por separado para cada plataforma a la que se desee dar soporte. Para gemas fuente, RubyGems provee un atributo de Gem::Specification adicional denominado extensions . Este atributo es una matriz de rutas de acceso a los archivos Ruby que van a generar Makefiles. La forma más habitual para crear uno de estos programas es usar la librería Ruby mkmf (se verán detalles más adelante). Estos archivos son llamados convencionalmente extconf.rb, aunque cualquier nombre valdría. Su mamá tiene una base de datos computarizada de recetas que es muy querida para ella, ya que ha estado almacenamiento sus recetas durante años, y le gustaría que le diera la posibilidad de publicar estas recetas en la web para sus amigos y familiares. Usted descubre que el programa de recetas, MenuBuilder, tiene una API nativa bastante agradable y decide escribir una extensión de Ruby para envolverlo. Ya que la extensión puede ser útil para otras personas que pueden estar usando MiRegistro, usted decide empaquetarla como una gema por separado y agregarla como una dependencia adicional para MiRegistro.
Aquí está la gemspec:
require ‘rubygems’ spec = Gem::Specification.new do |s| s.name = “MenuBuilder” s.version = “1.0.0” s.author = “Jo Programmer” s.email = “[email protected]” s.homepage = “http://www.joshost.com/projects/MenuBuilder” s.platform = Gem::Platform::RUBY s.summary = “A Ruby wrapper for the MenuBuilder recipe database.” s.files = [“ext/main.c”, “ext/extconf.rb”] s.require_path = “.” s.autorequire = “MenuBuilder” s.extensions = [“ext/extconf.rb”] end if $0 == __FILE__ Gem::manage_gems Gem::Builder.new(spec).build end Tenga en cuenta que usted tiene que incluir los archivos de código fuente en la lista de las especificaciones de archivos, para que estén incluidos en el paquete de la gema para su distribución. Cuando se instala una gema fuente, RubyGems ejecuta cada uno de sus programas de extensión y luego ejecuta el Makefile resultante.
% gem install MenuBuilder-1.0.0.gem Attempting local installation of ‘MenuBuilder-1.0.0.gem’ ruby extconf.rb inst MenuBuilder-1.0.0.gem creating Makefile 152
make gcc -fPIC- g -O2 -I. -I/usr/local/lib/ruby/1.8/i686linux \ -I/usr/local/lib/ruby/1.8/i686linux -I. -c main.c gcc -shared -L”/usr/local/lib” -o MenuBuilder.so main.o \ -ldl -lcrypt -lm -lc make install install -c -p -m 0755 MenuBuilder.so \ /usr/local/lib/ruby/gems/1.8/gems/MenuBuilder-1.0.0/. Successfully installed MenuBuilder, version 1.0.0 RubyGems no tiene la capacidad de detectar las dependencias del sistema de librerías que las gemas fuente puedan conllevar. Si una gema fuente depende de una librería que no está instalada, la instalación de la gema va a fracasar y se mostrará cualquier salida de error del comando make. La distribución de las gemas fuente, requiere obviamente, que el usuario de la gema tenga un conjunto de las herramientas del trabajo de desarrollo. Como mínimo, se va a necesitar algún tipo de programa make y un compilador. Especialmente para los usuarios de Windows, estas herramientas pueden no estar presentes. Se puede superar esta limitación mediante la distribución de las gemas ya precompiladas. La creación de gemas precompiladas es simple: añadir a la lista files de la especificación de gema los archivos compilados de objetos compartidos (archivos DLL en Windows) , y asegurarse de que estos archivos se encuentran en uno de los atributos require_path de la gema. Al igual que con las gemas puras de Ruby, el comando require_gem modificará la variable Ruby $LOAD_PATH y el objeto compartido será accesible a través de require. Dado que estas gemas son específicas de la plataforma, también puede utilizar el atributo platform (esto lo vimos en el primer ejemplo gemspec) para especificar la plataforma de destino de la gema. La clase Gem::Specification define constantes para Windows, Linux Intel, Macintosh, y Ruby puro. Para las plataformas que noestán en esta lista, se puede utilizar el valor de la variable RUBY_PLATFORM. Este atributo es solamente informativo por ahora, pero es un buen hábito a adquirir. Las futuras versiones de RubyGems utilizaran el atributo platform para seleccionar de forma inteligente las gemas pre-compiladas para la plataforma en la que se está ejecutando el instalador.
La Construcción del Archivo Gema El archivo de especificación de MiRegistro que acabamos de crear es ejecutable como un programa Ruby. La invocación creará un archivo gema, MiRegistro-0.5.0.gem.
% ruby miregistro.gemspec Attempting to build gem spec ‘miregistro.gemspec’ Successfully built RubyGem Name: MiRegistro Version: 0.5.0 File: MiRegistro-0.5.0.gem
Alternativamente, puede usar el comando gem build para generar el archivo de la gema.
% gem build miregistro.gemspec Attempting to build gem spec ‘miregistro.gemspec’ Successfully built RubyGem Name: MiRegistro Version: 0.5.0 File: MiRegistro-0.5.0.gem Ahora que tenemos un archivo gema, podemos distribuirlo como cualquier otro paquete. Se puede poner en un servidor FTP o un sitio Web para descargar o enviar por correo electrónico a los amigos. Una vez que tus amigos tienen este archivo en su ordenador local (descargado desde el servidor FTP si es necesario), se puede instalar la gema (suponiendo que hayan instalado también RubyGems) mediante la llamada:
153
% gem install MiRegistro-0.5.0.gem Attempting local installation of ‘MiRegistro-0.5.0.gem’ Successfully installed MiRegistro, version 0.5.0 Si desea liberar su gema a la comunidad Ruby, la forma más fácil es usar RubyForge (http://rubyforge. org). RubyForge es un sitio Web de gestión de proyectos open-source. También alberga el repositorio central de gemas. Los lanzamientos de gemas a través de la funcionalidad de RubyForge son automáticamente recogidos y añadidos al repositorio central varias veces al día. La ventaja para los posibles usuarios de su software es que éste estará disponible para consulta e instalación remota con RubyGems, haciendo todo aún más fácil.
Construcción con Rake Por último, pero ciertamente no menos importante, podemos utilizar Rake para construir gemas. Rake utiliza un archivo de comandos llamado Rakefile para controlar la construcción. Este define (¡en la sintaxis de Ruby!) un conjunto de reglas y tareas. Para obtener más información sobre el uso de Rake, ver http://rake.rubyforge.org. Tiene una completa documentación y siempre está actualizada. Aquí, nos centraremos sólo en Rake lo suficiente para construir una gema. De la documentación de Rake: Las tareas son la unidad principal de trabajo en un Rakefile. Las tareas tienen un nombre (por lo general como un símbolo o una cadena), una lista de prerequisitos (más símbolos o cadenas) y una lista de acciones (dada como un bloque). Normalmente, puede utilizar Rake con el método integrado task para definir sus propias tareas por nombre en el Rakefile. Para casos especiales, tiene sentido proporcionar el código de ayuda para automatizar algunas tareas repetitivas que puede haber para hacer de otra manera. La creación de gemas es uno de estos casos especiales. Rake viene con una TaskLib especial, llamado GemPackageTask, que ayuda a integrar la creación de gemas con el resto de su funcionalidad de construcción automatizada y el proceso de liberación. Para utilizar GemPackageTask en el Rakefile, hay que crear el gemspec exactamente como lo hicimos anteriormente, pero esta vez colocándolo en el Rakefile. Esta especificación de gema ahora es para GemPackageTask. require ‘rubygems’ Gem::manage_gems require ‘rake/gempackagetask’ spec = Gem::Specification.new do |s| s.name = “MiRegistro” s.version = “0.5.0” s.author = “Jo Programmer” s.email = “[email protected]” s.homepage = “http://www.joshost.com/MiRegistro” s.platform = Gem::Platform::RUBY s.summary = “An online Diary for families” s.files = FileList[“{bin,tests,lib,docs}/**/*”].exclude(“rdoc”).to_a s.require_path = “lib” s.autorequire = “miregistro” s.test_file = “tests/ts_miregistro.rb” s.has_rdoc = true s.extra_rdoc_files = [“README”] s.add_dependency(“BlueCloth”, “>= 0.0.4”) s.add_dependency(“MenuBuilder”, “>= 1.0.0”) end Rake::GemPackageTask.new(spec) do |pkg| pkg.need_tar = true end Tenga en cuenta que tendrá que requerir el paquete rubygems en su Rakefile. También note que hemos utilizado la clase de Rake FileList en lugar de Dir.glob para construir la lista de archivos.
154
FileListes más inteligente que Dir.glob para este propósito, ya que ignora automáticamente los archivos comúnmente no usados (como el directorio CVS que la herramienta CVS de control de versiones deja por ahí).
Internamente, GemPackageTask genera un objetivo Rake con el identificador
package_directory/gemname-gemversion.gem En nuestro caso, este identificador es pkg/MiRegistro-0.5.0.gem. Se puede invocar esta tarea desde el mismo directorio en el que se haya puesto el Rakefile.
% rake pkg/MiRegistro-0.5.0.gem (in /home/chad/download/gembook/code/MiRegistro) Successfully built RubyGem Name: MiRegistro Version: 0.5.0 File: MiRegistro-0.5.0.gem Ahora que tenemos una tarea, se puede utilizar como cualquier otra tarea Rake, añadirle dependencias o agregarla a la lista de dependencias de otra tarea, como el despliegue o la liberación del paquete.
Mantenimiento de la Gema (y un Último Vistazo a MiRegistro) Ya se ha lanzado MiRegistro y ahora hay nuevos usuarios cada semana. Hemos tenido mucho cuidado de empaquetar la gema de forma limpia y hemos utilizado rake para construirla. Su gema está “en tierra salvaje” con su información de contacto y sabe que es sólo cuestión de tiempo el empezar a recibir peticiones de nuevas funcionalidades (¡y cartas de admirador!) de sus usuarios. Sin embargo, su primera solicitud llega a través de una llamada telefónica de nada menos que del viejo y querido amigo que le pidió la aplicación. Acaba de llegar de unas vacaciones en Florida y le pregunta cómo puede incluir las fotos de vacaciones en su diario. No crees que una explicación de comandos FTP sería una buena idea y como es un amigo de la infancia te pasas la noche en la codificación de un módulo de álbum de fotos para MiRegistro. Puesto que se ha añadido una nueva funcionalidad a la aplicación (en lugar de sólo la fijación de un error), decide aumentar el número de versión a MiRegistro de 1.0.0 a 1.1.0. También agrega una serie de pruebas para la nueva funcionalidad y un documento sobre cómo configurar la funcionalidad de subir fotos. La figura 10 en la página siguiente muestra la estructura completa de directorios del paquete MiRegistro-1.1.0 (MomLog). La especificación de gema final (extraído del Rakefile) luce así: spec = Gem::Specification.new do |s| s.name = “MiRegistro” s.version = “1.1.0” s.author = “Jo Programmer” s.email = “[email protected]” s.homepage = “http://www.joshost.com/MiRegistro” s.platform = Gem::Platform::RUBY s.summary = “An online diary, recipe publisher, “ + “and photo album for families.” s.files = FileList[“{bin,tests,lib,docs}/**/*”].exclude(“rdoc”).to_a s.require_path = “lib” s.autorequire = “miregistro” s.test_file = “tests/ts_miregistro.rb” s.has_rdoc = true s.extra_rdoc_files = [“README”, “docs/DatabaseConfiguration.rdoc”, “docs/Installing.rdoc”, “docs/PhotoAlbumSetup.rdoc”] s.add_dependency(“BlueCloth”, “>= 0.0.4”) s.add_dependency(“MenuBuilder”, “>= 1.0.0”) end
155
Ejecuta Rake sobre su Rakefile y ya tiene la gema MiRegistro actualizada. Ya está listo para lanzar la nueva versión. Inicie sesión en su cuenta de RubyForge y cargue su gema en la sección “Files” de su proyecto. Mientras espera a que el proceso automatizado de RubyGems libere la gema en el repositorio central de gemas, puede escribir un anuncio publicando el lanzamiento de su proyecto en RubyForge. Dentro de aproximadamente una hora, su amigo puede conectarse al servidor para instalar la nueva versión:
% gem query -rn MiRegistro *** REMOTE GEMS *** MiRegistro (1.1.0, 1.0.0) An online diary, recipe publisher, and photo album for families. ¡Genial! La consulta indica que hay dos versiones de MiRegistro disponibles. Escribiendo el comando de instalación sin especificar un argumento de versión, se instala por defecto la versión más reciente.
% gem install -r MiRegistro Attempting remote installation of ‘MiRegistro’ Successfully installed MiRegistro, version 1.1.0 No ha cambiado ninguna de las dependencias de MiRegistro, por lo que la instalación existente de BlueCloth y MenuBuilder cumple con los requisitos para MiRegistro 1.1.0.
156
Ruby y la Web Ruby no es ajeno a Internet. No sólo puede escribir su propio servidor SMTP, demonio FTP o servidor Web en Ruby, sino que también puede utilizar Ruby para las tareas más habituales, como programación CGI o como un reemplazo para PHP. Hay muchas opciones disponibles en la utilización de Ruby para implementar aplicaciones Web, y un solo capítulo, no puede contenerlo todo. En su lugar, vamos a tratar algunos puntos y a poner de relieve las bibliotecas y los recursos que pueden ayudar. Vamos a empezar con algunas cosas sencillas: la ejecución de programas Ruby como Common GatewayInterface (CGI).
Escritura de Scripts CGI Se puede utilizar Ruby para escribir scripts CGI con bastante facilidad. Para tener un script Ruby que genere código HTML de salida, todo lo que necesita es algo así como #!/usr/bin/ruby print “Content-type: text/html\r\n\r\n” print “Hello World! It’s #{Time.now}\r\n” Coloque este script en un directorio CGI marcándolo como ejecutable y podrá accederle a través del navegador (si su servidor web no añade automáticamente las cabeceras, tendrá que añadir la cabecera de respuesta usted mismo). #!/usr/bin/ruby print “HTTP/1.0 200 OK\r\n” print “Content-type: text/html\r\n\r\n” print “Hello World! It’s #{Time.now}\r\n” Sin embargo, esto está a un nivel bastante bajo. Tendría que escribir su propia solicitud de análisis, gestión de sesión, manipulación de cookies, mecanismo de escape y así sucesivamente. Afortunadamente, hay opciones disponibles para hacer esto más fácil.
Utilizando cgi.rb La clase CGI proporciona soporte para la escritura de scripts CGI. Con ella, se pueden manipular los formularios, cookies y entorno, mantener sesiones de estado, etc. Es una clase bastante grande, pero aquí vamos a echar un rápido vistazo a sus capacidades.
“Escapado” Cuando se trata de URLs y de código HTML hay que tener cuidado de “escapar” algunos caracteres. Por ejemplo, un carácter de barra (/) tiene un significado especial en una URL, por lo que debe ser escapado si no es parte de la ruta. Es decir, cualquier / en la parte de la URL se convertiría en la cadena %2F y debe ser traducida de vuelta a una / para ser usada. El espacio y el símbolo & son también caracteres especiales. Para controlar esto, CGI proporciona las rutinas CGI.escape y CGI.unescape. require ‘cgi’ puts CGI.escape(“Nicholas Payton/Trumpet & Flugel Horn”) produce: Nicholas+Payton%2FTrumpet+%26+Flugel+Horn
Más frecuentemente, es posible que desee escapar los caracteres especiales HTML.
require ‘cgi’
157
puts CGI.escapeHTML(“a < 100 && b > 200”) produce: a < 100 && b > 200 Para obtener algo realmente de lujo, puede decidir escapar sólo algunos elementos HTML dentro de una cadena. require ‘cgi’ puts CGI.escapeElement(‘
Click Here
’,’A’) produce:
Click Here
Aquí sólo el elemento A fué escapado, los otros elementos se quedan. Cada uno de estos métodos tiene una versión “un-” para restaurar la cadena original. require ‘cgi’ puts CGI.unescapeHTML(“a < 100 && b > 200”) produce: a < 100 && b > 200
Parámetros de Consulta Las peticiones HTTP desde el navegador a su aplicación pueden contener parámetros, ya sean pasados como parte de la URL o pasados como datos incrustados en el cuerpo de la solicitud. El procesamiento de estos parámetros se complica por el hecho de que un valor con un nombre determinado puede ser devuelto varias veces en la misma petición. Por ejemplo, supongamos que estamos escribiendo una encuesta para averiguar por qué a la gente le gusta Ruby. El código HTML de nuestro formulario se vería así: Test Form I like Ruby because: Cuando alguien rellena este formulario, puede tener varias razones por las que le gusta Ruby (como se muestra en la figura 11 en la página siguiente). En este caso, el dato del formulario que corresponde al nombre reason tiene tres valores, correspondientes a las tres casillas marcadas.
158
La clase CGI le permite acceder a los datos del formulario de un par de maneras. En primer lugar, sólo podemos tratar al objeto CGI como un hash, indexándole con nombres de campo y obteniendo valores de campo. require ‘cgi’ cgi = CGI.new cgi[‘name’] cgi[‘reason’]
-> ->
“Dave Thomas” “flexible”
Sin embargo, esto no funciona bien con el campo reason: sólo vemos uno de los tres valores. Podemos solicitar ver todos usando el método CGI#params. El valor devuelto por params actúa como un hash que contiene los parámetros de la solicitud. Se puede leer y escribir en este hash (lo que le permite modificar los datos asociados a una solicitud). Tenga en cuenta que cada uno de los valores del hash es en realidad una matriz.
require ‘cgi’ cgi = CGI.new cgi.params -> {“name”=>[“Dave Thomas”], “reason”=>[“flexible”, “transparent”, “fun”]} cgi.params[‘name’] -> [“Dave Thomas”] cgi.params[‘reason’] -> [“flexible”, “transparent”, “fun”] cgi.params[‘name’] = [ cgi[‘name’].upcase ] cgi.params -> {“name”=>[“DAVE THOMAS”], “reason”=>[“flexible”, “transparent”, “fun”]} Se puede determinar si un parámetro en particular está presente en una solicitud utilizando CGI#has_key?. require ‘cgi’ cgi = CGI.new cgi.has_key?(‘name’) cgi.has_key?(‘age’)
-> ->
true false
159
Generación de HTML CGI contiene un gran número de métodos que pueden utilizarse para crear HTML --un método por cada elemento. Para activar estos métodos, se debe crear un objeto CGI llamando CGI.new, pasándolo al nivel requerido de HTML. En estos ejemplos, vamos a utilizar HTML3. Para hacer más fácil la anidación de elementos, estos métodos toman su contenido como bloques de código. Éstos deben devolver un String, que se utiliza como contenido del elemento. Para este ejemplo, hemos añadido algunas nuevas líneas gratuitas para hacer que la salida se ajuste a la página. require ‘cgi’ cgi = CGI.new(“html3”) # add HTML generation methods cgi.out { cgi.html { cgi.head { “\n”+cgi.title{“This Is a Test”} } + cgi.body { “\n”+ cgi.form {“\n”+ cgi.hr + cgi.h1 { “A Form: “ } + “\n”+ cgi.textarea(“get_text”) +”\n”+ cgi.br + cgi.submit } } } } produce: Content-Type: text/html Content-Length: 302 This Is a Test Este código producirá un formulario HTML titulado “This Is a Test,” seguido por una línea horizontal y un encabezado de primer nivel, un área de entrada de texto, y finalmente un botón de envío. Cuando se de el envío, tendremos un parámetro CGI llamado get_text que contiene el texto introducido por el usuario. Aunque es interesante, este método de generación de HTML es bastante laborioso y, probablemente, no se utilice mucho en la práctica. La mayoría de la gente no parece que escriba el código HTML directamente, más bien utiliza un sistema de plantillas, o utiliza un framework de aplicacion, tal como Iowa. Desafortunadamente, no tenemos espacio para hablar de Iowa --puede echar un vistazo a la documentación en línea en http://enigo.com/projects/iowa, o mirar en el capítulo 6 de The Ruby Developer’s Guide [FJN02] --aunque ahora vamos a ver el sistema de plantillas.
Sistema de plantillas Los sistemas de plantillas le permiten separar lo que es la presentación de lo que es la lógica de su aplicación. Parece que casi todo el mundo que escribe una aplicación web con Ruby, en algún momento también escribe un sistema de plantillas: en el wiki RubyGarden hay unas cuantas. Por ahora, vamos a ver en tres: las plantillas RDoc, Amrita y erb/eruby.
160
Plantillas RDoc El sistema de documentación RDoc (descrito anteriormente) incluye un sistema muy sencillo de plantillas, que utiliza para generar su salida en XML y HTML. Como RDoc se distribuye como parte de la norma Ruby, el sistema de plantillas está disponible donde quiera que Ruby versión 1.8.2 o posterior esté instalado. Sin embargo, el sistema de plantillas no usa formatos HTML o XML convencionales (ya que está destinado a ser utilizado para generar la salida en diferentes formatos), por lo que los archivos formateados con plantillas RDoc pueden no ser fáciles de editar con las herramientas convencionales de edición de HTML. require ‘rdoc/template’ HTML = %{Hello, %name%. The reasons you gave were:
START:reasons - %reason_name% (%rank%) END:reasons
} data = { ‘name’ => ‘Dave Thomas’, ‘reasons’ => [ { ‘reason_name’ => ‘flexible’, ‘rank’ => ‘87’ }, { ‘reason_name’ => ‘transparent’, ‘rank’ => ‘76’ }, { ‘reason_name’ => ‘fun’, ‘rank’ => ‘94’ }, ] } t = TemplatePage.new(HTML) t.write_html_on(STDOUT, data) produce: Hello, Dave Thomas. The reasons you gave were:
- flexible (87)
- transparent (76)
- fun (94)
Se le pasa una cadena al construcrtor que contiene la plantilla que se va a utilizar. A continuación, se le pasa al método write_html_on un hash que contiene los nombres y los valores. Si la plantilla contiene la secuencia %xxxx%, se consulta el hash y se sustituye el valor correspondiente al nombre xxx Si la plantilla contiene START:yyy, el valor hash que corresponde a yyy se supone que es un array de hashes. Las líneas de la plantilla entre START:yyy y END:yyy se repiten para cada elemento del array. Las plantillas también soportan condicionaless: las líneas entre IF:zzz y ENDIF:zzz se incluyen en la salida sólo si el hash tiene una clave zzz.
Amrita Amrita (http://amrita.sourceforge.jp/index.html) es una librería que genera documentos HTML a partir de una plantilla que sí es HTML válido. Esto hace que Amrita sea fácil de usar con los actuales editores de HTML. También hace que las plantillas de Amrita se muestren correctamente como páginas HTML independientes. Amrita utiliza las etiquetas id de los elementos HTML para determinar los valores a sustituir. Si el valor correspondiente a un nombre dado es nil o false, el elemento HTML no se incluirá en el resultado.
161
Si el valor es una matriz, repite el elemento HTML correspondiente. require ‘amrita/template’ include Amrita HTML = %{ The reasons you gave were:
} data = { :greeting => ‘Hello, Dave Thomas’, :reasons => [ { :reason_name => ‘flexible’, :rank => ‘87’ }, { :reason_name => ‘transparent’, :rank => ‘76’ }, { :reason_name => ‘fun’, :rank => ‘94’ }, ] } t = TemplateText.new(HTML) t.prettyprint = true t.expand(STDOUT, data) produce: Hello, Dave Thomas
The reasons you gave were:
- flexible, 87
- transparent, 76
- fun, 94
erb y eruby Hasta ahora hemos visto el uso de Ruby para crear salida HTML, pero se puede cambiar la cuestión de adentro a afuera. Podemos integrar Ruby en un documento HTML. Una serie de paquetes permiten integrar las declaraciones Ruby en algún otro tipo de documento, especialmente en páginas HTML. Generalmente, esto se conoce como “eRuby.” Específicamente, existen varias implementaciones diferentes de eRuby, incluyendo eruby y erb. eruby, escrito por Shugo Maeda, está disponible para su descarga desde el Archivo de Aplicaciones Ruby. erb, su primo, está escrito en Ruby puro y se incluye con la distribución estándar. Aquí veremos erb. La integración de Ruby en HTML es un concepto muy poderoso. Básicamente, nos da el equivalente de una herramienta como ASP, JSP o PHP, pero con toda la potencia de Ruby.
Usando erb erb normalmente se utiliza como filtro. El texto en el archivo de entrada pasa a través tal cuál, sin tocar, sólo con las siguientes excepciones:
Expresión Descripción <% ruby code %> Ejecuta el código Ruby entre los delimitadores. <%= ruby expression %> Evalua la expresión de Ruby y reemplaza la secuencia con el valor de la expresión. <%# ruby code %> El código Ruby entre los delimitadores se tiene en cuenta (para pruebas). % line of ruby code Una línea que comienza con el signo porcentaje se supone que contiene sólo código Ruby.
162
erb se invoca:
erb [ opciones ] [ documento ] Si se omite documento, erb lee desde la entrada estándar. Las opciones de línea de comandos son las siguientes:
Opción Descripción -d Establecer $DEBUG a true. -Kkcode Especifica un sistema de codificación alternativo. -n Muestra el resultado de un script Ruby (con números de línea). -r librería Carga la librería dada. -P No procesar con erb las líneas que comienzan con un %. -S nivel Ajusta el nivel de seguridad. -T modo Establece el modo de ajuste. -v Activa el modo detallado. -x Muestra el resultado de el script Ruby.
Veamos algunos ejemplos sencillos. Vamos a correr el ejecutable erb en la siguiente entrada:
% a = 99 <%= a %> botellas de cerveza... La línea que comienza con el signo de porcentaje, simplemente ejecuta la sentencia Ruby dada. La siguiente línea contiene la secuencia <% a %>, que sustituye el valor de a. erb f1.erb produce: 99 botellas de cerveza... erb trabaja reescribiendo su entrada como un script Ruby para a continuación ejecutar el script. Se puede ver el Ruby que genera utilizando las opciones -n o -x. erb -x f1.erb produce: _erbout = ‘’; a = 99 _erbout.concat(( a ).to_s); _erbout.concat “ botellas de cerveza...\n” _erbout Note como erb crea una cadena, _erbout, que contiene tanto las cadenass estáticas de la plantilla como los resultados de la ejecución de las expresiones (en este caso el valor de a). Por supuesto, puede incrustar Ruby dentro de un tipo de documento más complejo, tal como HTML. La figura 12 muestra un par de bucles en un documento HTML.
163
Instalación de eruby en Apache Si desea utilizar erb como el generador de páginas de un sitio web que recibe una cantidad razonable de tráfico, es probable que prefiera utilizar eruby, que tiene un mejor rendimiento. Entonces, puede configurar el servidor Web Apache para analizar automáticamente documentos integrados de Ruby con eruby, de la misma manera como se hace con PHP. Se pueden crear archivos de Ruby integrados con un sufijo .rhtml y configurar el servidor Web para correr el ejecutable eruby en estos documentos obteniendo así el resultado HTML deseado.
Para utilizar eruby con el servidor Web Apache, es necesario realizar los siguientes pasos:
1. Copiar el binario eruby en el directorio cgi-bin.
2. Añadir las siguientes dos líneas a httpd.conf.
AddType application/x-httpd-eruby .rhtml Action application/x-httpd-eruby /cgi-bin/eruby 3. Si se quiere, también se puede añadir o reemplazar la directiva DirectoryIndex de tal manera que incluya index.rhtml. Esto le permite utilizar Ruby para crear listados de directorios para los directorios 164
que no contengan un index.html. DirectoryIndex index.html index.shtml index.rhtml DirectoryIndex acepta rutas y también nombres de archivo, así que se puede utilizar una ruta URL absoluta donde dejar un script por defecto: DirectoryIndex index.html /cgi-bin/index.rb
Cookies Las cookies son una forma de hacer que las aplicaciones Web almacenen su estado en la máquina del usuario. Mal vistas por algunos, las cookies son todavía una conveniente (si no fiable) manera de recordar la información de sesión. La clase Ruby CGI controla la carga y almacenamiento de cookies. Se puede acceder a las cookies asociadas con la solicitud en curso utilizando el método de CGI#cookies. Se pueden configurar de nuevo las cookies en el navegador estableciendo el parámetro cookies de CGI#out referenciando a una única cookie o un conjunto de cookies. #!/usr/bin/ruby COOKIE_NAME = ‘chocolate chip’ require ‘cgi’ cgi = CGI.new values = cgi.cookies[COOKIE_NAME] if values.empty? msg = “It looks as if you haven’t visited recently” else msg = “You last visited #{values[0]}” end cookie = CGI::Cookie.new(COOKIE_NAME, Time.now.to_s) cookie.expires = Time.now + 30*24*3600 # 30 days cgi.out(“cookie” => cookie ) { msg }
Sesiones Las cookies por sí mismas aún necesitan un poco de trabajo para ser útiles. Realmente queremos de la sesión la información que persiste entre las peticiones de un navegador web en particular. Las sesiones se manejan con la clase CGI::Session, que utiliza las cookies pero proporciona una abstracción de alto nivel. Al igual que con las cookies, las sesiones emulan un comportamiento tipo hash, asociándo valores con claves. A diferencia de las cookies, las sesiones almacenan la mayoría de sus datos en el servidor, utilizando la cookie del navegador residente simplemente como una forma de identificación única de los datos del lado del servidor. Las sesiones también tienen opciónes respecto a las técnicas de almacenamiento de estos datos: el alamcenamiento se puede dar en archivos normales, en un PStore (Persistent Object Storage), en la memoria o incluso en un almacenaje personalizado. Después de su utilización, se deben cerrar las sesiones ya que asegura que se guarden los datos. Cuando se haya terminado definitivamente con una sesión, se deben borrar. require ‘cgi’ require ‘cgi/session’ cgi = CGI.new(“html3”) sess = CGI::Session.new(cgi, “session_key” => “rubyweb”, “prefix” => “web-session.” )
165
if sess[‘lastaccess’] msg = “You were last here #{sess[‘lastaccess’]}.” else msg = “Looks like you haven’t been here for a while” end count = (sess[“accesscount”] || 0).to_i count += 1 msg << “Number of visits: #{count}” sess[“accesscount”] = count sess[“lastaccess”] = Time.now.to_s sess.close cgi.out { cgi.html { cgi.body { msg } } } El código anterior utiliza el mecanismo de almacenamiento predeterminado para las sesiones: los datos persistentes se almacenan en archivos en el directorio temporal por defecto (ver Dir.tmpdir). Los nombres de archivo emepezarán con web-session. y terminará con una versión hash del número de sesión. Ver ri CGI::Session para más información.
Mejorar el Rendimiento Se puede utilizar Ruby para escribir programas CGI para la Web, pero, al igual que la mayoría de los programas de CGI, la configuración por defecto tiene que empezar una nueva copia de Ruby con cada acceso de página cgi-bin. Eso es costoso en términos de la utilización de la máquina y puede ser muy lento para los internautas. El servidor Web Apache resuelve este problema soportando módulos cargables. Típicamente, estos módulos son cargados dinámicamente y se convierten en una parte de los procesos que ejecuta el servidor Web --que no tiene la necesidad de generar otro intérprete una y otra vez para dar servicio a las solicitudes, ya que el servidor Web es el intérprete. Y así llegamos a mod_ruby (disponible en los archivos), un módulo de Apache que acopla un intérprete Ruby completo en el servidor Apache mismo. El archivo readme incluido con mod_ruby proporciona detalles sobre cómo compilarlo e instalarlo. Una vez instalado y configurado, puede ejecutar scripts Ruby más o menos como se hace sin mod_ ruby, sólo que ahora mucho más rápido. También puede aprovechar las características adicionales que ofrece mod_ruby (como la integración en el manejo de las solicitudes de Apache). Sin embargo, hay que prestar atención a algunas cosas. Debido a que el intérprete permanece en la memoria entre solicitudes, puede terminar la tramitación de las solicitudes de varias aplicaciones. Es posible que las bibliotecas de estas aplicaciones entren en conflicto (sobre todo si las diferentes bibliotecas contienen clases con el mismo nombre). Tampoco se puede asumir que el mismo intérprete se encargará de manejar la serie de solicitudes de una sesión de navegador --Apache asignará los procesos controladores utilizando sus algoritmos internos. Algunas de estas cuestiones se resuelven mediante el protocolo FastCGI. Este es un hack interesante, a disposición de todos los programas estilo CGI, no sólo Ruby. Utiliza un programa proxy muy pequeño que generalmente se ejecuta como módulo de Apache. Cuando se reciben las solicitudes, este proxy las reenvía a un proceso concreto de larga ejecución que actúa como un script CGI normal. Los resultados se mandan de vuelta al proxy, y a continuación de vuelta al navegador. FastCGI tiene las mismas ventajas que correr mod_ruby, ya que el intérprete siempre está ejecutandose en segundo plano. También le da más control sobre cómo se asignan las solicitudes a los intérpretes. Encontrará más información en http://www.fastcgi.com.
166
Servidores Web Alternativos Hasta ahora, hemos estado ejecutando scripts de Ruby bajo el control del servidor Web Apache. Sin embargo, a partir de la versión 1.8 Ruby viene con WEBrick, una flexible herramienta escrita en Ruby para dar servicio HTTP. Básicamente, se trata de un plug-in extensible --basado en framework, que le permite escribir servidores para manejar solicitudes y respuestas HTTP. A continuación un servidor HTTP básico que sirve documentos e índices de directorio: #!/usr/bin/ruby require ‘webrick’ include WEBrick s = HTTPServer.new( :Port => 2000, :DocumentRoot => File.join(Dir.pwd, “/html”) ) trap(“INT”) { s.shutdown } s.start El constructor HTTPServer crea un nuevo servidor web en el puerto 2000. El código establece el directorio raíz de documentos en el subdirectorio /html del directorio actual. A continuación, utiliza Kernel.trap para organizar el cierre ordenado de las interrupciones antes de iniciar la ejecución del servidor. Si se apunta el navegador a http://localhost:2000, se debería ver una lista del subdirectorio html. WEBrick puede hacer mucho más que servir contenido estático. Se puede utilizar como un contenedor de servlets de Java. El siguiente código monta un servlet simple en /hello. Se invoca al método do_GET cunado llegan las solicitudes. Se utiliza el objeto respuesta para mostrar la información de agente de usuario y los parámetros de solicitud. #!/usr/bin/ruby require ‘webrick’ include WEBrick s = HTTPServer.new( :Port => 2000 ) class HelloServlet < HTTPServlet::AbstractServlet def do_GET(req, res) res[‘Conten-tType’] = “text/html” res.body = %{
Hello. You’re calling from a #{req[‘User-Agent’]} I see parameters: #{req.query.keys.join(‘, ‘)} } end end s.mount(“/hello”, HelloServlet) trap(“INT”){ s.shutdown } s.start
SOAP y Servicios Web SOAP -jabón, en inglés-, se puso en su tiempo para Simple Object Access Protocol. Cuando la gente ya no pudo soportar la ironía, el acrónimo cayó y ahora SOAP es sólo un nombre. Ruby viene ahora con una implementación de SOAP. Esto le permite escribir servidores y clientes utilizando servicios Web. Por su naturaleza, estas aplicaciones pueden funcionar tanto a nivel local como remota a través de una red. Las aplicaciones SOAP ignoran el lenguaje de implementación de sus pares en la red, por lo que utilizar SOAP es una forma conveniente de interconectar aplicaciones Ruby con otras escritas en lenguajes como Java, Visual Basic o C++.
167
SOAP básicamente es un mecanismo que utiliza XML para enviar datos entre dos nodos de una red. Se suele utilizar para implementar las llamadas a procedimiento remoto, RPC, entre procesos de distribuidos. Un servidor SOAP publica una o más interfaces. Estas interfaces se definen en términos de tipos de datos y los métodos que utilizan estos tipos. A continucación, el cliente SOAP, crea un proxy local que mediante SOAP se conecta a las interfaces en el servidor. En el proxy se pasa una llamada a un método a la interfaz correspondiente en el servidor, y los valores de retorno generados por el método en el servidor, se devuelven al cliente a través del proxy. Vamos a empezar con un servicio SOAP trivial. Vamos a escribir un objeto que hace los cálculos de los intereses. Inicialmente, se ofrece un método único, compound, que determina el interés compuesto dando uno principal, una tasa de interés, el número de veces que el interés se ve combinado por año y el número de años. Con fines de gestión, también vamos a llevar un registro de cuántas veces fue llamado este método y hacer disponible este contador a través de un descriptor de acceso. Tenga en cuenta que esta clase es sólo código Ruby regular --no sabe que se está ejecutando en un entorno SOAP. class InterestCalculator attr_reader :call_count def initialize @call_count = 0 end def compound(principal, rate, freq, years) @call_count += 1 principal*(1.0 + rate/freq)**(freq*years) end end Ahora vamos a hacer un objeto de esta clase que esté disponible a través de un servidor SOAP. Esto permitirá a las aplicaciones cliente llamar a los métodos del objeto a través de la red. Aquí estamos usando un servidor independiente, lo cual es conveniente cuando se hacen pruebas y se puede utilizar la línea de comandos. También se pueden ejecutar los servidores SOAP Ruby como scripts CGI o bajo mod_ruby. require ‘soap/rpc/standaloneServer’ require ‘interestcalc’ NS = ‘http://pragprog.com/InterestCalc’ class Server2 < SOAP::RPC::StandaloneServer def on_init calc = InterestCalculator.new add_method(calc, ‘compound’, ‘principal’, ‘rate’, ‘freq’, ‘years’) add_method(calc, ‘call_count’) end end svr = Server2.new(‘Calc’, NS, ‘0.0.0.0’, 12321) trap(‘INT’) { svr.shutdown } svr.start Este código define una clase que implementa un servidor SOAP independiente. Cuando se inicializa, la clase crea un objeto InterestCalculator (una instancia de la clase que acabamos de escribir). A continuación, utiliza add_method para añadir los dos métodos utilizados por esta clase, compound y call_count. Finalmente, el código crea y ejecuta una instancia de esta clase servidor. Los parámetros para el constructor son el nombre de la aplicación, el espacio de nombres por defecto, la dirección de la interfaz a utilizar y el puerto. Entonces necesitamos escribir algún código de cliente para acceder a este servidor. El cliente crea un proxy local para el servicio InterestCalculator en el servidor, agrega los métodos que desea utilizar y luego los llama. require ‘soap/rpc/driver’ proxy = SOAP::RPC::Driver.new(“http://localhost:12321”, “http://pragprog.com/InterestCalc”) proxy.add_method(‘compound’, ‘principal’, ‘rate’, ‘freq’, ‘years’)
168
proxy.add_method(‘call_count’) puts “Call count: #{proxy.call_count}” puts “5 years, compound annually: #{proxy.compound(100, 0.06, 1, 5)}” puts “5 years, compound monthly: #{proxy.compound(100, 0.06, 12, 5)}” puts “Call count: #{proxy.call_count}” Para probar esto, podemos ejecutar el servidor en una ventana de consola (la salida que se muestra aquí ha sido reformateada ligeramente para adaptarse a esta página).
% ruby server.rb I, [2004-07-26T10:55:51.629451 #12327] INFO -- Calc: Start of Calc. I, [2004-07-26T10:55:51.633755 #12327] INFO -- Calc: WEBrick 1.3.1 I, [2004-07-26T10:55:51.635146 #12327] INFO -- Calc: ruby 1.8.2 (2004-07-26) [powerpcdarwin] I, [2004-07-26T10:55:51.639347 #12327] INFO -- Calc: WEBrick::HTTPServer#start: pid=12327 port=12321
A continuación, se ejecuta el cliente en otra ventana.
% ruby client.rb Call count: 0 5 years, compound annually: 133.82255776 5 years, compound monthly: 134.885015254931 Call count: 2
¡En buen estado! Funciona con éxito, llamamos a todos nuestros amigos y lo ejecutamos de nuevo.
% ruby client.rb Call count: 2 5 years, compound annually: 133.82255776 5 years, compound monthly: 134.885015254931 Call count: 4 Observar cómo la segunda vez que se ejecuta el cliente, el número de las llamadas se inicia ahora en dos. El servidor crea un solo objeto InterestCalculator para atender las solicitudes de entrada que se vuelve a utilizar para cada solicitud.
SOAP y Google Es evidente que el beneficio real de SOAP es la forma en que le permite interoperar con otros servicios en la Web. Como ejemplo, vamos a escribir algo de código Ruby para enviar consultas a la API web de Google. Antes de enviar las consultas a Google, se necesita una clave de desarrollador. Este servicio está disponible desde Google, vaya a http://www.google.com/apis y siga las instrucciones en el paso 2, crear una cuenta de Google. Después de entrar su dirección de correo electrónico y proporcionar una contraseña, Google le enviará una clave de desarrollador. En los siguientes ejemplos, vamos a suponer que usted ha almacenado esta clave en el archivo .google_key en su directorio principal. Vamos a empezar en el nivel más básico. En cuanto a la documentación de la API de Google, el método doGoogleSearch descubrimos que tiene diez (!) parámetros. key La clave de desarrollador. q La cadena de consulta. start El índice del primer resultado requerido. maxResults El número máximo de resultados a devolver por la consulta. filter Si está activado, comprime los resultados para que páginas similares y páginas en el mismo dominio sólo se muestren una vez.
169
restrict se restringe la búsqueda a un subconjunto del índice de Google Web. safeSearch Si está activado, elimina los posibles contenidos para adultos de los resultados. lr Restringe la búsqueda a los documentos en un determinado conjunto de lenguas. ie Ignorado (codificación de entrada) oe Ignorado (codificación de salida) Podemos utilizar la llamada add_method para construir un proxy SOAP para el método doGoogleSearch . El siguiente ejemplo hace exactamente eso, imprime la primera entrada devuelta buscando en Google el término pragmatic. require ‘soap/rpc/driver’ require ‘cgi’ endpoint = ‘http://api.google.com/search/beta2’ namespace = ‘urn:GoogleSearch’ soap = SOAP::RPC::Driver.new(endpoint, namespace) soap.add_method(‘doGoogleSearch’, ‘key’, ‘q’, ‘start’, ‘maxResults’, ‘filter’, ‘restrict’, ‘safeSearch’, ‘lr’, ‘ie’, ‘oe’) query = ‘pragmatic’ key = File.read(File.join(ENV[‘HOME’], “.google_key”)).chomp result = soap.doGoogleSearch(key, query, 0, 1, false, nil, false, nil, nil, nil) printf “Estimated number of results is %d.\n”, result.estimatedTotalResultsCount printf “Your query took %6f seconds.\n”, result.searchTime first = result.resultElements[0] puts first.title puts first.URL puts CGI.unescapeHTML(first.snippet) Al ejecutarlo, se verá algo como lo siguiente (nótese cómo Google ha puesto el término de consulta de relieve). Estimated number of results is 550000. Your query took 0.123762 seconds. The Pragmatic Programmers, LLC http://www.pragmaticprogrammer.com/ Home of Andrew Hunt and David Thomas’s best-selling book ‘The Pragmatic Programmer’
and The ‘Pragmatic Starter Kit (tm)’ series. ... The Pragmatic Bookshelf TM. ... Sin embargo, SOAP permite el descubrimiento dinámico de la interfaz de objetos en el servidor. Esto se hace utilizando WSDL (Web Services Description Language). Un archivo WSDL es un documento XML que describe los tipos, métodos y mecanismos de acceso para una interfaz de servicios Web. Los clientes SOAP pueden leer los archivos WSDL para crear las interfaces de un servidor de forma automática. La página web http://code.creativecommons.org/svnroot/stats/GoogleSearch.wsdl contiene el WSDL que describe la interfaz de Google. Podemos alterar nuestra aplicación de búsqueda para leer este WSDL, que elimina la necesidad de agregar el método doGoogleSearch explícitamente. require ‘soap/wsdlDriver’ require ‘cgi’ WSDL_URL = “http://api.google.com/GoogleSearch.wsdl” soap = SOAP::WSDLDriverFactory.new(WSDL_URL).createDriver query = ‘pragmatic’ key = File.read(File.join(ENV[‘HOME’], “.google_key”)).chomp result = soap.doGoogleSearch(key, query, 0, 1, false, nil, false, nil, nil, nil) printf “Estimated number of results is %d.\n”, result.estimatedTotalResultsCount
170
printf “Your query took %6f seconds.\n”, result.searchTime first = result.resultElements[0] puts first.title puts first.URL puts CGI.unescapeHTML(first.snippet) Por último, podemos ir un paso más allá utilizando la librería Google de Ian Macdonald (disponible en el Ruby Application Archive, en http://raa.ruby-lang.org/project/ruby-google/), que encapsula la API de servicios Web detrás de una interfaz agradable (bueno, si no por otra razón, por lo menos porque elimina la necesidad de todos los parámetros adicionales). La biblioteca también cuenta con métodos para construir rangos de fechas y otras restricciones para una consulta en Google y proporciona interfaces a la caché de Google y a la facilidad de corrección ortográfica. El siguiente código es nuestra búsqueda de “pragmatic” utilizando la biblioteca de Ian. require ‘google’ require ‘cgi’ key = File.read(File.join(ENV[‘HOME’], “.google_key”)).chomp google = Google::Search.new(key) result = google.search(‘pragmatic’) printf “Estimated number of results is %d.\n”, result.estimatedTotalResultsCount printf “Your query took %6f seconds.\n”, result.searchTime first = result.resultElements[0] puts first.title puts first.url puts CGI.unescapeHTML(first.snippet)
Más Información
La programación web con Ruby es un gran tema. Para profundizar más, es posible que desee echar un vistazo al capítulo 9 de The Ruby Way [Ful01], donde encontrará muchos ejemplos de red y de programación Web. En el capítulo 6 de The Ruby Developer’s Guide [FJN02], encontrará algunos buenos ejemplos de la estructuración de las aplicaciones CGI, junto con algunos ejemplos de código Iowa. Si SOAP puede ser complejo, es posible que desee ver el uso de XML-RPC, que se describe brevemente más adelante. Hay un número de otros marcos de desarrollo Ruby para Web que están disponibles en la red. Esta es un área dinámica: nuevos contendientes aparecen constantemente y es difícil para un libro impreso el ser definitivo. Sin embargo, dos frameworks que están atrayendo el espíritu de la comunidad Ruby son
• Rails (http://www.rubyonrails.org), y • CGIKit (http://www.spice-of-life.net/cgikit/index_en.html).
Ruby Tk El archivo de aplicaciones Ruby contiene varias extensiones que proveen a Ruby de una interfaz gráfica de usuario (GUI), incluyendo las extensiones para Fox, GTK y otras. La extensión Tk se incluye en la distribución y funciona tanto en sistemas Unix como Windows. Para utilizarla, es necesario tener instalado Tk en su sistema. Tk es un gran sistema y se han escrito libros enteros sobre él, así que no vamos a perder el tiempo y los recursos ahondando demasiado en Tk, pero si vamos a concentrarnos en la forma de acceder a las funciones Tk desde Ruby. Usted necesitará uno de estos libros de referencia con el fin de utilizar Ruby con Tk eficazmente. El binding que utilizamos es el más cercano al binding de Perl, por lo que es probable que desee obtener una copia de Learning Perl/Tk [Wal99] o Perl/Tk Pocket Reference [Lid98]. Tk trabaja según una composición de modelo, es decir, empieza por la creación de un contenedor (como un TkFrame o un TkRoot) y luego crea los widgets (otro nombre para los componentes de la
171
interfazgráfica de usuario) que lo pueblan, como botones o etiquetas. Cuando se está listo para iniciar la interfaz gráfica de usuario, se llama a Tk.mainloop. El motor de Tk toma entonces el control del programa, que muestra los widgets y llama al código en respuesta a los eventos de la interfaz gráfica de usuario.
Simple aplicación Tk
Una simple aplicación Tk en Ruby puede ser algo como esto:
require ‘tk’ root = TkRoot.new { title “Ex1” } TkLabel.new(root) do text ‘Hello, World!’ pack { padx 15 ; pady 15; side ‘left’ } end Tk.mainloop Veamos el código un poco más de cerca. Después de cargar el módulo de extensión Tk, se crea un marco de nivel raíz (root-level) con TkRoot.new. A continuación hacemos un widget TkLabel como proceso hijo del marco raíz, estableciendo varias opciones para la etiqueta. Por último, se empaqueta el marco raíz y se entra en el bucle principal de eventos GUI. Es un buen hábito especificar la raiz de forma explícita, aunque se puede dejar de lado (junto con las opciones adicionales) y madurar esto en tres líneas: require ‘tk’ TkLabel.new { text ‘Hello, World!’; pack } Tk.mainloop ¡Esto es todo lo que hay que hacer! Armado con uno de los libros de Perl/Tk de los quehacemos referencia al comienzo de este capítulo, se pueden producir todas las sofisticadas interfaces gráficas de usuario que usted necesite. Pero, de nuevo, si desea quedarse para algunos detalles más, aquí vienen.
Widgets La creación de widgets es fácil. Se toma el nombre del widget como figura en la documentación de Tk y se añade Tk al principio de tal nombre. Por ejemplo, los widgets Label, Button y Entry, se convierten en las clases TkLabel, TkButton y TkEntry. Se crea una instancia de un widget utilizando new, al igual que se haría con cualquier otro objeto. Si no se especifica un padre para un determinado widget, se usará por defecto el marco de nivel raíz. Por lo general, se especifica el padre del widget junto con muchas otras opciones --el color, tamaño, etc. También tenemos que ser capaces de obtener información desde nuestros widgets durante la ejecución de nuestro programa, mediante la creación de callbacks (rutinas que se invocan cuando ocurren ciertos eventos) y de intercambiar datos.
Opciones de Configuración para los Widgets Si nos fijamos en un manual de referencia de Tk (el que está escrito para Perl/Tk, por ejemplo), nos daremos cuenta que las opciones para los widgets normalmente se especifican con un guión --como las opciones de línea de comandos. En Perl/Tk, las opciones se le pasan a un widget en un Hash. Se puede hacer en Ruby así, pero también puede pasar las opciones utilizando un bloque de código. El nombre de la opción se utiliza como el nombre de un método dentro del bloque y los argumentos de opción aparecen como argumentos para la llamada al método. Los widgets toman un padre como primer argumento, seguido de un hash de opciones opcional o el bloque de código de opciones. Por lo tanto, las dos formas siguientes son equivalentes. TkLabel.new(parent_widget) do text ‘Hello, World!’ pack(‘padx’ => 5, ‘pady’ => 5, ‘side’ => ‘left’)
172
end # or TkLabel.new(parent_widget, ‘text’ => ‘Hello, World!’).pack(...) Una pequeña precaución cuando se utiliza la forma del bloque de código: el ámbito de las variables no es como se piensa que es. El bloque es evaluado realmente en el contexto del objeto widget, no en el del llamador. Esto significa que las variables de instancia del llamador no estarán disponible en el bloque, pero si las variables locales del ámbito que lo contiene y las globales (no es que utilize variables globales, por supuesto). Vamos a mostrar como se pasan las opciones utilizando ambos métodos, en los ejemplos que siguen. Las distancias (como las opciones padx y pady en estos ejemplos) se supone que son en píxeles, pero puede ser especificado en las diferentes unidades con los sufijos c (cm), i (pulgadas), m (milímetros), o p (puntos). “12p”, por ejemplo, es doce puntos.
Obtención de Datos del Widget Podemos obtener información de vuelta de los widgets mediante retornos de llamada (callbacks) y por variables binding (vinculantes). Los retornos de llamada son muy fáciles de configurar. La opción command (que se muestra en la llamada a TkButton en el ejemplo que sigue) toma un objeto Proc, que será llamado cuando se dispare el retorno de llamada. Aquí pasamos el proc como un bloque asociado con la llamada al método, pero también podríamos haber utilizado Kernel.lambda para generar un objeto Proc explícito. require ‘tk’ TkButton.new do text “EXIT” command { exit } pack(‘side’=>’left’, ‘padx’=>10, ‘pady’=>10) end Tk.mainloop También se puede enlazar una variable Ruby para un valor widget Tk usando un proxy TkVariable. Esto arregla las cosas de manera que cada vez que cambia el valor del widget, la variable Ruby se actualiza automáticamente, y cuando cambia la variable, el widget reflejar el nuevo valor. Se muestra esto en el siguiente ejemplo. Observe cómo está configurado TkCheckButton, la documentación dice que la opción variable toma una var referencia como argumento. Para ello, creamos una variable Tk de referencia con TkVariable.new. El acceso a mycheck.value devolverá la cadena “0” ó “1” dependiendo de si la casilla está marcada. Usted puede utilizar el mismo mecanismo para todo lo que es compatible con una referencia var, como botones radiales y campos de texto. require ‘tk’ packing = { ‘padx’=>5, ‘pady’=>5, ‘side’ => ‘left’ } checked = TkVariable.new def checked.status value == “1” ? “Yes” : “No” end status = TkLabel.new do text checked.status pack(packing) end TkCheckButton.new do variable checked pack(packing) end TkButton.new do
173
text “Show status” command { status.text(checked.status) } pack(packing) end Tk.mainloop
Configuración/Obtención de Opciones Dinámicamente Además de establecer las opciones de un widget cuando se crea, puede volver a configurar un widget mientras se está ejecutando. Cada widget soporta el método configure, que toma un hash o un bloque de código de la misma manera como new. Podemos modificar el primer ejemplo para cambiar el texto de la etiqueta en respuesta a un clic de botón. require ‘tk’ root = TkRoot.new { title “Ex3” } top = TkFrame.new(root) { relief ‘raised’; border 5 } lbl = TkLabel.new(top) do justify ‘center’ text ‘Hello, World!’ pack(‘padx’=>5, ‘pady’=>5, ‘side’ => ‘top’) end TkButton.new(top) do text “Ok” command { exit } pack(‘side’=>’left’, ‘padx’=>10, ‘pady’=>10) end TkButton.new(top) do text “Cancel” command { lbl.configure(‘text’=>”Goodbye, Cruel World!”) } pack(‘side’=>’right’, ‘padx’=>10, ‘pady’=>10) end top.pack(‘fill’=>’both’, ‘side’ =>’top’) Tk.mainloop Ahora, cuando se hace clic en el botón Cancelar, el texto de la etiqueta cambia inmediatamente de “¡Hola, Mundo!” a “¡Adiós, mundo cruel!”
También puede consultar los valores de opción particulares para los widgets con cget.
require ‘tk’ b = TkButton.new do text “OK” justify “left” border 5 end b.cget(‘text’) -> b.cget(‘justify’) -> b.cget(‘border’) ->
“OK” “left” 5
Aplicación de Ejemplo He aquí un ejemplo un poco más largo que muestra una aplicación real --un generador de pig latin. Escriba la frase tal como las reglas de Ruby, y el botón Pig It lo traducirá al instante al latín de cerdo. require ‘tk’ class PigBox def pig(word) leading_cap = word =~ /^[A-Z]/ word.downcase! res = case word
174
when /^[aeiouy]/ word+”way” when /^([^aeiouy]+)(.*)/ $2+$1+”ay” else word end leading_cap ? res.capitalize : res end def show_pig @text.value = @text.value.split.collect{|w| pig(w)}.join(“ “) end def initialize ph = { ‘padx’ => 10, ‘pady’ => 10 } # common options root = TkRoot.new { title “Pig” } top = TkFrame.new(root) { background “white” } TkLabel.new(top) {text ‘Enter Text:’ ; pack(ph) } @text = TkVariable.new TkEntry.new(top, ‘textvariable’ => @text).pack(ph) pig_b = TkButton.new(top) { text ‘Pig It’; pack ph} pig_b.command { show_pig } exit_b = TkButton.new(top) {text ‘Exit’; pack ph} exit_b.command { exit } top.pack(‘fill’=>’both’, ‘side’ =>’top’) end end PigBox.new Tk.mainloop
Enlazar Eventos
Los widgets están expuestos al mundo real. En ellos se hace clic, se mueve el ratón sobre ellos, los usuarios escriben en ellos, todas estas cosas y muchas más, generan eventos que podemos captar. Se puede crear un binding desde un evento en un widget en particular a un bloque de código, usando el método de widget bind. Por ejemplo, supongamos que hemos creado un widget de botón que muestra una imagen. Nos gustaría que la imagen cambie cuando el ratón del usuario pasa por encima del botón. require ‘tk’ image1 = TkPhotoImage.new { file “img1.gif” } image2 = TkPhotoImage.new { file “img2.gif” } b = TkButton.new(@root) do image image1 command { exit } pack end b.bind(“Enter”) { b.configure(‘image’=>image2) } b.bind(“Leave”) { b.configure(‘image’=>image1) } Tk.mainloop En primer lugar, se crean dos objetos de imagen GIF a partir de archivos en el disco utilizando TkPhotoImage . Después, se crea un botón (muy atractivo, llamado “b”), que muestra la imagen image1. A continuación, se enlaza el evento Enter para que cambie dinámicamente la imagen mostrada a image2 cuando el ratón está sobre el botón, y el evento Leave para volver a image1 cuando el ratón abandona el botón. Este ejemplo muestra los eventos simples Enter y Leave. Sin embargo, el nombre de evento dado como un argumento a bind puede estar compuesto de varias sub-cadenas, separadas por guiones, en
175
disposición modificador-modificador-tipo-detalle. Los modificadores son mencionados en la referencia de Tk e incluyen Button1, Control, Alt, Shift, etc. Tipo es el nombre del evento (tomado de las convenciones de nombres X11) e incluye eventos como ButtonPress, KeyPress y Expose. Detalle puede ser un número del 1 al 5 para los botones o un símbolo clave para la entrada de teclado. Por ejemplo, un binding que tendrá lugar a la liberación del botón 1 del ratón mientras se presiona la tecla control puede especificarse como Control-Button1-ButtonRelease o Control-ButtonRelease-1 El evento en sí puede contener ciertos campos, como la hora del evento y las posiciones x e y. bind pueden pasar estos elementos al retorno de llamada, utilizando los códigos de campos de evento. Estos se utilizan como las especificaciones printf. Por ejemplo, para obtener las coordenadas x e y en un movimiento del ratón, se especifica la llamada a bind con tres parámetros. El segundo parámetro es el Proc para el retorno de llamada, y el tercer parámetro es la cadena de campo de evento. canvas.bind(“Motion”, lambda {|x, y| do_motion (x, y)}, “%x %y”)
Canvas Tk proporciona un widget Canvas con el que se puede dibujar y generar una salida PostScript. La figura 13 en la página 178 muestra un pequeño código simple (adaptado de la distribución) que dibuja líneas rectas. Clikcando y manteniendo pulsado el botón 1 se iniciará una línea, que será “bandeada en goma” según se mueva el ratón. Cuando suelte el botón 1, la línea se dibujará en esa posición. Gestión de la Geometría En el código de ejemplo de este capítulo, verá referencias al método widget pack. Es una llamada muy importante --olvídese de ella y usted nunca verá el widget. pack es un comando que le dice al gestor de la geometría como colocar el widget de acuerdo a las limitaciones que se especifican. Los gestores de geometría reconocen tres comandos: Comando
Especificación de Colocación
pack Flexible, basada en restricciones de colocación place Posición Absoluta grid Posición Tabular (Fila/Columna) Como pack es el comando más utilizado, es el que usamos en nuestros ejemplos.
Unos pocos clics del ratón y tienes una obra maestra instantánea.
Como se suele decir, “No se encontró el artista, así que tuvimos que colgar el cuadro. . . . “
176
Desplazamiento A menos que se planee hacer dibujos muy pequeños, el ejemplo anterior puede que no sea útil. TkCanvas , TkListbox y TkText pueden ser configurados para usar barras de desplazamiento, y que se pueda trabajar en un subconjunto más pequeño de una “gran imagen”. La comunicación entre una barra de desplazamiento y un widget es bidireccional. El movimiento de la barra de desplazamiento hace que cambier la vista del widget, pero cuando es ésta la que cambia por algún otro medio, la barra de desplazamiento tiene que cambiar también para reflejar la nueva posición. Aunque que no hemos hecho mucho con listas, el ejemplo de desplazamiento siguiente utiliza el desplazamiento para una lista de texto. En el fragmento de código, vamos a empezar por la creación de un simple TkListbox y un TkScrollbar asociados. El callback o retorno de llamada de la barra de desplazamiento (configurado con command) llama al método widget de lista yview, que hace que cambie el valor de la parte visible de la lista, en la dirección y. Después de configurar el retorno de llamada, hacemos la relación inversa: cuando la lista tenga la necesidad de desplazarse, vamos a configurar el rango adecuado para scrollbar con TkScrollbar#set. Vamos a utilizar este mismo fragmento en un programa totalmente funcional en la siguiente sección: list_w = TkListbox.new(frame) do selectmode ‘single’ pack ‘side’ => ‘left’ end list_w.bind(“ButtonRelease-1”) do busy do filename = list_w.get(*list_w.curselection) tmp_img = TkPhotoImage.new { file filename } scale = tmp_img.height / 100 scale = 1 if scale < 1 image_w.copy(tmp_img, ‘subsample’ => [scale, scale]) image_w.pack end end scroll_bar = TkScrollbar.new(frame) do command {|*args| list_w.yview *args } pack ‘side’ => ‘left’, ‘fill’ => ‘y’ end list_w.yscrollcommand {|first,last| scroll_bar.set(first,last) }
Sólo Una Cosa Más Podríamos seguir hablando de Tk para otros cientos de páginas, pero ese sería otro libro. El siguiente programa es nuestro último ejemplo Tk --un visor simple de imagenes GIF. Se puede seleccionar un nombre de archivo GIF a partir de la lista de desplazamiento y se mostrará una versión en miniatura de la imagen. Vamos a señalar sólo alguna cosa más. ¿Alguna vez ha utilizado una aplicación que crea un “cursor ocupado” y luego se olvida de restablecerlo a la normalidad? Hay un buen truco en Ruby para evitar que esto suceda. ¿Recuerda cómo File.new utiliza un bloque para asegurarse de que el archivo se cierra después de utilizarlo? Podemos hacer algo similar con el método busy, como se muestra en el siguiente ejemplo. Este programa también muestra algunas manipulaciones simples con TkListbox --añadir elementos a la lista, crear un retorno de llamada en un botón del ratón (probablemente se requiera a la liberación del botón, no a la pulsación, ya que el widget es seleccionado en la pulsación del botón), y la recuperación de la selección en curso. Hasta ahora, hemos usado TkPhotoImage para mostrar las imágenes directamente, pero también se puede hacer zoom, submuestreo y también mostrar porciones de las imágenes. Aquí se utiliza la función de sub-muestreo para escalar la imagen para su visualización.
177
require ‘tk’ class GifViewer def initialize(filelist) setup_viewer(filelist) end def run Tk.mainloop end
178
def setup_viewer(filelist) @root = TkRoot.new {title ‘Scroll List’} frame = TkFrame.new(@root) image_w = TkPhotoImage.new TkLabel.new(frame) do image image_w pack ‘side’=>’right’ end list_w = TkListbox.new(frame) do selectmode ‘single’ pack ‘side’ => ‘left’ end list_w.bind(“ButtonRelease-1”) do busy do filename = list_w.get(*list_w.curselection) tmp_img = TkPhotoImage.new { file filename } scale = tmp_img.height / 100 scale = 1 if scale < 1 image_w.copy(tmp_img, ‘subsample’ => [scale, scale]) image_w.pack end end filelist.each do |name| list_w.insert(‘end’, name) # Insert each file name into the list end scroll_bar = TkScrollbar.new(frame) do command {|*args| list_w.yview *args } pack ‘side’ => ‘left’, ‘fill’ => ‘y’ end list_w.yscrollcommand {|first,last| scroll_bar.set(first,last) } frame.pack end # Run a block with a ‘wait’ cursor def busy @root.cursor “watch” # Set a watch cursor yield ensure @root.cursor “” # Back to original end end viewer = GifViewer.new(Dir[“screenshots/gifs/*.gif”]) viewer.run
Trasladar desde Documentación Perl/Tk Eso es todo, usted cuenta con usted mismo ahora. En su mayor parte, se puede trasladar fácilmente la documentación entregada para Perl/Tk a Ruby. Hay algunas excepciones, algunos métodos no se aplican y algunas funcionalidades extra indocumentadas. Hasta que un libro de Ruby/Tk salga, lo mejor es preguntar en el grupo de noticias o leer el código fuente. Pero en general, es bastante fácil ver lo que está pasando. Recuerde que las opciones se pueden dar como un hash o en el estilo bloque de código. Y recuerde el entorno del bloque de código se encuentra dentro del TkWidget que se utiliza, no en la instancia de clase.
Creación de Objetos En la asignación de Perl/Tk, los padres son responsables de crear sus widgets hijos. En Ruby, el padre se pasa como el primer parámetro al constructor de widget.
179
Perl/Tk: $widget = $parent->Widget( [ option => value ] ) Ruby: widget = TkWidget.new(parent, option-hash) widget = TkWidget.new(parent) { code block } Puede que no se necesite salvar el valor devuelto del widget recién creado, pero está ahí si se hace. No hay que olvidar empaquetar (pack) el widget (o utilizar una de las otras llamadas de geometría), o no se mostrará.
Opciones Perl/Tk: -background => color Ruby: ‘background’ => color { background color }
Recuerde que el ámbito de un bloque de código es diferente.
Referencias a Variables Use TkVariable para vincular una variable Ruby al valor de un widget. A continuación, se puede utilizar el accesor value en TkVariable (TkVariable#value y TkVariable#value=) para afectar directamente al contenido del widget.
Ruby y Microsoft Windows Ruby se ejecuta en numerosos entornos diferentes. Algunos de estos están basados en Unix y otros en las diferentes versiones de Microsoft Windows. Ruby llegó de personas que estaban centradas en Unix, pero con los años se han desarrollado también un montón de características útiles en el mundo de Windows. En este capítulo, vamos a ver estas características y compartir algunos secretos para utilizar Ruby eficazmente en Windows.
Obteniendo Ruby para Windows
Dos versiones de Ruby están disponibles para el entorno Windows.
La primera es una versión de Ruby que funciona de forma nativa --es decir, es sólo otra aplicación de Windows. La forma más fácil para esta distribución es utilizar el programa de instalación de un solo clic, que carga una distribución binaria hecho y listo en su compartimento. Siga los enlaces desde http:// rubyinstaller.rubyforge.org/ para obtener la última versión. Si se siente más aventurero, o si necesita compilarlo con las bibliotecas que no se suministran con la distribución binaria, entonces se puede construir desde el código fuente de Ruby. Usted necesitará el compilador VC++ de Microsoft y sus herramientas asociadas para hacerlo. Descargar el código fuente de Ruby de http://www.ruby-lang.org, o usar CVS para comprobar la última versión en desarrollo. A continuación, lea el archivo win32\README.win32 para obtener instrucciones. Una segunda alternativa utiliza una capa de emulación llamada Cygwin. Esto proporciona un ambiente a modo Unix en la parte superior de Windows. La versión Cygwin de Ruby es la más cercana al Ruby que se ejecuta en las plataformas Unix, pero su funcionamiento conlleva que también tenga que instalar Cygwin. Si usted quiere seguir esta vía, puede descargar la versión Cygwin de Ruby de http://ftp. ruby-lang.org/pub/ruby/binaries/cygwin/. También necesitará Cygwin en sí mismo. El enlace de descarga tiene un puntero a la biblioteca de enlace dinámico (DLL) requerida, o se puede ir a http:// www.cygwin.com y descargar el paquete completo (pero atención: necesita asegurarse de que la versión que obtenga es compatible con la de Ruby que se ha descargado). ¿Qué versión de elegir? Cuando se produjo la primera edición de este libro, la versión de Cygwin para Ruby era la distribución a elegir. Esa situación ha cambiado: la construcción nativa se ha vuelto más y más funcional con el tiempo, hasta el punto que ésta es ahora nuestra construcción preferida de Ruby.
180
Ejecutando Ruby en Windows
Hay dos archivos ejecutables en la distribución Windows de Ruby.
ruby.exe está destinado a ser utilizado en un sistema de comandos (un shell de DOS), como en la versión Unix. Para las aplicaciones que leen y escriben en la entrada y salida estándar, esto está muy bien. Pero esto también significa que cada vez que se ejecuta ruby.exe obtendrá un shell de DOS, incluso si usted no lo quiere --Windows creará una nueva consola de comandos y la mostrará mientras que Ruby se esté ejecutando. Este no puede ser el comportamiento adecuado si, por ejemplo, se hace doble clic en un script Ruby que usa una interfaz gráfica (como Tk), o si está ejecutando un script Ruby como tarea de fondo o si lo está ejecutando desde el interior de otro programa. En estos casos, usted querrá usar rubyw.exe. Es lo mismo que ruby.exe excepto que no proporciona entrada estándar, salida estándar o error estándar y no lanzar un shell de DOS cuando se ejecuta. El programa de instalación (por defecto) establece las asociaciones de archivos para que los archivos con la extensión .rb utilicen automáticamente rubyw.exe. Al hacer esto, puede hacer doble clic en scripts Ruby y que simplemente correran sin desplegar un shell de DOS.
Win32API Si su plan es hacer programación Ruby que necesite acceder a algunas de las funciones de la API de Windows 32 directamente, o que tienga que utilizar los puntos de entrada de algunos otros archivos DLL, tenemos buenas noticias para usted --la biblioteca Win32API. Como ejemplo, aquí tenemos un código que es parte de una aplicación de Windows utilizada por nuestro sistema para libro de cumplimientos, que descarga e imprime las facturas y los recibos. Una aplicación web genera un archivo PDF que el script Ruby ejecuta en las descargas de Windows a un archivo local. El script utiliza el comando shell print bajo Windows para imprimir este archivo. arg = “ids=#{resp.intl_orders.join(“,”)}” fname = “/temp/invoices.pdf” site = Net::HTTP.new(HOST, PORT) site.use_ssl = true http_resp, = site.get2(“/fulfill/receipt.cgi?” + arg, ‘Authorization’ => ‘Basic ‘ + [“name:passwd”].pack(‘m’).strip ) File.open(fname, “wb”) {|f| f.puts(http_resp.body) } shell = Win32API.new(“shell32”,”ShellExecute”, [‘L’,’P’,’P’,’P’,’P’,’L’], ‘L’ ) shell.Call(0, “print”, fname, 0,0, SW_SHOWNORMAL) Se crea un objeto Win32API que representa una llamada a un punto de entrada DLL en particular, especificando el nombre de la función, el nombre del DLL que contiene la función, y el armado de la función (tipos de argumentos y tipo de retorno). En el ejemplo anterior, la variable shell envuelve la función ShellExecute de Windows en el DLL shell22. La función toma seis parámetros (un número, cuatro punteros de cadena y un número) y retorna un número. (Este tipo de parámetros se describen en la página más adelante) El objeto resultante se puede utilizar para realizar la llamada a imprimir para el archivo que hemos descargado. Muchos de los argumentos a las funciones DLL son de alguna forma estructuras binarias. Win32API maneja esto mediante el uso de objetos cadena Ruby para pasar los datos binarios de ida y vuelta. Usted tendrá que empacar y desempacar estas cadenas cuando sea necesario (más adelante veremos un ejemplo).
Automatización de Windows Si deslizarse alrededor de la API de bajo nivel de Windows no le interesa, la automatización de Windows puede --se puede utilizar Ruby como un cliente de automatización de Windows, gracias a una
181
extensión de Ruby llamada WIN32OLE, escrita por Masaki Suketa. Win32OLE es parte de la distribución estándar de Ruby. La Automatización de Windows permite a un controlador de automatización (un cliente) el mandar órdenes y hacer consultas a un servidor de automatización, como Microsoft Excel, Word, PowerPoint, etc. Podemos ejecutar un método para un servidor de automatización para llamar a un método del mismo nombre desde un objeto WIN32OLE. Por ejemplo, puede crear un nuevo cliente WIN32OLE que lanza una copia limpia de Internet Explorer y mandarle visitar la página de inicio. ie = WIN32OLE.new(‘InternetExplorer.Application’) ie.visible = true ie.gohome
También podría hacer que navegue a una página determinada.
ie = WIN32OLE.new(‘InternetExplorer.Application’) ie.visible = true ie.navigate(“http://www.pragmaticprogrammer.com”) Los métodos que son desconocidos para WIN32OLE (como visible, gohome, o navigate) se pasan al método WIN32OLE#invoke, el cual envía los comandos apropiados al servidor.
Obtener y Configurar Propiedades Podemos establecer y obtener propiedades del servidor usando la notación hash normal de Ruby. Por ejemplo, para establecer la propiedad Rotation en un gráfico de Excel, se puede escribir excel = WIN32OLE.new(“excel.application”) excelchart = excel.Charts.Add() ... excelchart[‘Rotation’] = 45 puts excelchart[‘Rotation’] Los parámetros de un objeto OLE se configuran automáticamente como atributos del objeto WIN32OLE. Esto significa que se puede establecer un parámetro mediante la asignación de un atributo de objeto. excelchart.rotation = 45 r = excelchart.rotation El siguiente ejemplo es una versión modificada del archivo de ejemplo excel2.rb (que se encuentra en el directorio ext/win32/samples). Se inicia Excel, se crea un gráfico y luego se gira en la pantalla. ¡Ten cuidado, Pixar! require ‘win32ole’ # -4100 is the value for the Excel constant xl3DColumn. ChartTypeVal = -4100; excel = WIN32OLE.new(“excel.application”) # Create and rotate the chart excel[‘Visible’] = TRUE excel.Workbooks.Add() excel.Range(“a1”)[‘Value’] = 3 excel.Range(“a2”)[‘Value’] = 2 excel.Range(“a3”)[‘Value’] = 1 excel.Range(“a1:a3”).Select() excelchart = excel.Charts.Add() excelchart[‘Type’] = ChartTypeVal 30.step(180, 5) do |rot|
182
excelchart.rotation = rot sleep(0.1) end excel.ActiveWorkbook.Close(0) excel.Quit()
Argumentos con Nombre Otros lenguajes de cliente de automatización, tal como Visual Basic tienen el concepto de argumentos con nombre. Supongamos que tienes una rutina de Visual Basic con la forma Song(artist, title, length):
rem Visual Basic
En lugar de llamar con los tres argumentos en el orden especificado, se puede utilizar argumentos con nombre. Song title := ‘Get It On’:
rem Visual Basic
Esto es equivalente a la llamada Song(nil, ’Get It On’, nil).
En Ruby, puede utilizar esta característica pasando un hash con los argumentos con nombre.
Song.new(‘title’ => ‘Get It On’)
for each Donde Visual Basic tiene una sentencia “for each” para iterar sobre una colección de elementos en un servidor, un objeto WIN32OLE tiene un método each (que toma un bloque) para lograr la misma cosa. require ‘win32ole’ excel = WIN32OLE.new(“excel.application”) excel.Workbooks.Add excel.Range(“a1”).Value = 10 excel.Range(“a2”).Value = 20 excel.Range(“a3”).Value = “=a1+a2” excel.Range(“a1:a3”).each do |cell| p cell.Value end
Eventos Su cliente de automatización escrito en Ruby puede registrarse a sí mismo para recibir eventos de otros programas. Esto se hace usando la clase WIN32OLE_EVENT. En este ejemplo (basado en el código de la distribución Win32OLE 0.1.1) se muestra el uso de un receptor de eventos que registra las direcciones URL por las que un usuario navega cuando utiliza Internet Explorer. require ‘win32ole’ $urls = [] def navigate(url) $urls << url end def stop_msg_loop puts “IE has exited...” throw :done end def default_handler(event, *args) case event when “BeforeNavigate” puts “Now Navigating to #{args[0]}...”
183
end end ie = WIN32OLE.new(‘InternetExplorer.Application’) ie.visible = TRUE ie.gohome ev = WIN32OLE_EVENT.new(ie, ‘DWebBrowserEvents’) ev.on_event {|*args| default_handler(*args)} ev.on_event(“NavigateComplete”) {|url| navigate(url)} ev.on_event(“Quit”) {|*args| stop_msg_loop} catch(:done) do loop do WIN32OLE_EVENT.message_loop end end puts “You Navigated to the following URLs: “ $urls.each_with_index do |url, i| puts “(#{i+1}) #{url}” end
Optimización Como con la mayoría (si no todos) los lenguajes de alto nivel, puede ser muy fácil agitar hacia fuera código que es insoportablemente lento, pero que se puede fijar fácilmente con un poco de imaginación. Con WIN32OLE, es necesario tener cuidado con las búsquedas dinámicas innecesarias. Siempre que sea posible, es mejor asignar un objeto WIN32OLE a una variable y luego referenciar elementos desde ella, en lugar de crear una larga cadena de expresiones “.” .
Por ejemplo, en lugar de escribir
workbook.Worksheets(1).Range(“A1”).value workbook.Worksheets(1).Range(“A2”).value workbook.Worksheets(1).Range(“A3”).value workbook.Worksheets(1).Range(“A4”).value
= = = =
1 2 4 8
podemos eliminar las subexpresiones comunes de ahorro salvando la primera parte de la expresión a una variable temporal y luego hacer las llamadas a partir de esa variable. worksheet = workbook.Worksheets(1) worksheet.Range(“A1”).value = 1 worksheet.Range(“A2”).value = 2 worksheet.Range(“A3”).value = 4 worksheet.Range(“A4”).value = 8 También se puede crear archivos de resguardo Ruby para una biblioteca de tipos particulares de Windows. Estos archivos envuelven el objeto OLE en una clase de Ruby con un método por punto de entrada. Internamente, el archivo utiliza el número del punto de entrada, no el nombre, lo que acelera el acceso. Se genera la clase contenedora con el script olegen.rb en el directorio ext\ win32ole\samples, dándole el nombre de la biblioteca de tiposa reflejar. C:\> ruby olegen.rb ‘NetMeeting 1.1 Type Library’ >netmeeting.rb Los métodos y los eventos externos de la biblioteca de tipos se escriben para el archivo dado como los métodos de Ruby. A continuación, puede incluirse en sus programas y llamar a los métodos directamente. Vamos a tratar algunos tiempos. require ‘netmeeting’ require ‘benchmark’ include Benchmark
184
bmbm(10) do |test| test.report(“Dynamic”) do nm = WIN32OLE.new(‘NetMeeting.App.1’) 10000.times { nm.Version } end test.report(“Via proxy”) do nm = NetMeeting_App_1.new 10000.times { nm.Version } end end produce: Rehearsal --------------------------------------Dynamic 0.600000 0.200000 0.800000 ( 1.623000) Via proxy 0.361000 0.140000 0.501000 ( 0.961000) -------------------------------total: 1.301000sec user system total real Dynamic 0.471000 0.110000 0.581000 ( 1.522000) Via proxy 0.470000 0.130000 0.600000 ( 0.952000)
La versión del proxy es más del 40 por ciento más rápida que el código que hace la búsqueda dinámica.
Más Ayuda Si usted necesita la interfaz de Ruby para Windows NT, 2000 o XP, es posible que desee echar un vistazo al proyecto Win32Utils de Daniel Berger (http://rubyforge.org/projects/win32utils/). Allí encontrará módulos para la conexión al portapapeles de Windows, registro de eventos, planificador de procesos, etc. Asimismo, la biblioteca DL (que se describe brevemente más adelante) permite a los programas Ruby invocar a los métodos de carga dinámica de objetos compartidos. En Windows, esto significa que el código de Ruby puede cargar y ejecutar los puntos de entrada en un archivo DLL de Windows. Por ejemplo, el siguiente código, tomado a partir del código fuente de la librería DL en la distribución estándar de Ruby, determina a qué botón el usuario ha hecho clic, cuando aparece un cuadro de mensaje en una máquina Windows. require ‘dl’ User32 = DL.dlopen(“user32”) MB_OKCANCEL = 1 message_box = User32[‘MessageBoxA’, ‘ILSSI’] r, rs = message_box.call(0, ‘OK?’, ‘Please Confirm’, MB_OKCANCEL) case r when 1 print(“OK!\n”) when 2 print(“Cancel!\n”) end Este código abre el archivo DLL User32. A continuación, crea un objeto Ruby, message_box, que envuelve el punto de entrada MessageBoxA. El segundo parámetro , “ILSSI”, declara que el método devuelve un Integer, y toma un Long, dos Strings y un Integer como parámetros. El objeto contenedor se utiliza entonces para llamar al punto de entrada del cuadro de mensaje en el DLL. Los valores de retorno son el resultado (en este caso, el identificador del botón pulsado por el usuario) y una matriz de parámetros que son pasados (que ignoramos).
185
Extendiendo Ruby Es fácil extender Ruby con nuevas características escribiéndolas en código Ruby. Pero de vez en cuando necesita intefaces para conectarse con las cosas a bajo nivel. Una vez que se comienza a añadir código de bajo nivel escrito en C, las posibilidades son infinitas. Dicho esto, la materia de este capítulo es bastante avanzada y probablemente, se debiera omitir en una primera lectura rápida del libro. La extensión de Ruby con C es muy fácil. Por ejemplo, supongamos que estamos construyendo una lista a medida de Internet del jukebox para el “Sunset Diner and Grill”. Se quieren poner archivos de audio MP3 desde un disco duro o CD de audio en el jukebox y queremos ser capaces de controlar el hardware de la máquina de discos desde un programa de Ruby. El proveedor de hardware nos dio un archivo de cabecera C y una biblioteca binaria para utilizar, nuestro trabajo es construir un objeto Ruby que haga las adecuadas llamadas a funciones C. Mucha de la información de este capítulo se ha tomado del archivo README.EXT que se incluye en la distribución. Si usted está pensando en escribir una extensión para Ruby, es posible que desee buscar referencias en ese archivo para obtener más detalles, así como los últimos cambios.
Su Primera Extensión Sólo para introducir la escritura de extensiones, vamos a escribir una. Esta extensión es simplemente una prueba del proceso --no hace nada que no se pudiera hacer en Ruby puro. También vamos a presentar algunas cosas sin demasiada explicación --todos los detalles enmarañados se darán más adelante.
La extensión que escribamos tendrá la misma funcionalidad que la siguiente clase de Ruby.
class def end def end end
MyTest initialize @arr = Array.new add(obj) @arr.push(obj)
Es decir, vamos a escribir una extensión en C que es compatible con esta clase Ruby. El código equivalente en C debería ser algún tanto familiar. #include “ruby.h” static int id_push; static VALUE t_init(VALUE self) { VALUE arr; arr = rb_ary_new(); rb_iv_set(self, “@arr”, arr); return self; } static VALUE t_add(VALUE self, VALUE obj) { VALUE arr; arr = rb_iv_get(self, “@arr”); rb_funcall(arr, id_push, 1, obj); return arr; } VALUE cTest; void Init_my_test() { cTest = rb_define_class(“MyTest”, rb_cObject); rb_define_method(cTest, “initialize”, t_init, 0); rb_define_method(cTest, “add”, t_add, 1); id_push = rb_intern(“push”);
186
} Vamos a ir en detalle a través de este ejemplo, ya que ilustra muchos de los conceptos importantes en este capítulo. En primer lugar, tenemos que incluir el fichero de cabecera ruby.h para obtener las definiciones Ruby necesarias. Ahora observe la última función, Init_my_test. Cada extensión define una función global C llamada Init_name. Esta función es llamada cuando el intérprete carga por primera vez el nombre de extensión (o en el arranque para las extensiones enlazados estáticamente). Se utiliza para inicializar la extensión y para infiltrarse en el entorno Ruby (cómo Ruby sabe exactamente que una extensión es llamada por el nombre lo vamos a cubrir más adelante) En este caso, se define una nueva clase denominada MyTest, que es una subclase de Object (representado por el símbolo externo rb_cObject; ver ruby.h para otros). A continuación se estableció add e initialize como dos métodos de instancia para la clase MyTest. Las llamadas a rb_define_method establecen un enlace entre el nombre del método Ruby y la función C que lo incorpora. Si el código Ruby llama al método add en uno de nuestros objetos, el intérprete a su vez, llama a la función C t_add con un argumento. Del mismo modo, cuando new es llamado para esta clase, Ruby va a construir un objeto básico y luego llamar a initialize, que aquí hemos definido para llamar a la función C t_init sin argumentos (Ruby). Ahora regresa y mira a la definición de t_init. A pesar de que se dijo que no toma argumentos, ¡tiene aqui un parámetro! Además de culaquier argumento Ruby, a cada método se le pasa un argumento inicial VALUE que contiene el receptor para este método (el equivalente de self en código Ruby). La primera cosa que haremos en t_init es crear una matriz Ruby y establecer la variable de instancia @arr referenciandola. Así como se esperaría si estuviera escribiendo código fuente en Ruby, al hacer referencia a una variable de instancia que no existe, se crea. A continuación, retorna un puntero a si misma. ADVERTENCIA: Todas las funciones C que se pueden llamar desde Ruby deben devolver un valor, incluso si es sólo Qnil. De lo contrario, el resultado probable será un volcado de memoria (o GPF) . Por último, la función t_add obtiene la variable de instancia @arr del objeto actual y llama a Array#push para empujar el valor pasado en la matriz. Al acceder a variables de instancia de este modo, el prefijo @ es obligatorio --de lo contrario la variable se crea pero no se puede referenciar desde Ruby. A pesar de la sintaxis extra, torpe, que impone C, está siendo escrito en Ruby --puede manipular objetos mediante todas las llamadas a método que ha llegado a conocer y amar, con la ventaja adicional de ser capaz de crear código ajustado y rápido cuando sea necesario.
Construyendo Nuestra Extensión Vamos a tener mucho más que decir sobre la construcción de extensiones después. Por ahora, sin embargo, todo lo que tenemos que hacer es seguir estos pasos. 1. Cree un archivo denominado extconf.rb en el mismo directorio que nuestro archivo de código fuente en C my_text.c. El archivo extconf.rb debe contener las siguientes dos líneas: require ‘mkmf’ create_makefile(“my_test”)
2. Ejecutar extconf.rb. Esto generará un Makefile.
% ruby extconf.rb creating Makefile
3. Utilice make para construir la extensión. Esto es lo que sucede en un sistema OS X.
% make 187
gcc -fno-common -g -O2 -pipe -fno-common -I. -I/usr/lib/ruby/1.9/powerpc-darwin7.4.0 -I/usr/lib/ruby/1.9/powerpcdarwin7.4.0 -I. -c my_test.c cc -dynamic -bundle -undefined suppress -flat_namespace -L’/usr/lib’ -o my_test.bundle my_test.o -ldl -lobjc El resultado de todo esto es la extensión, todo muy bien envuelto en un objeto compartido (un fichero .so, .dll ó [en OS X] .bundle).
Ejecutando Nuestra Extensión Podemos utilizar nuestra extensión de Ruby con un simple require de forma dinámica en tiempo de ejecución (en la mayoría de las plataformas). Podemos terminar con esto con una prueba para verificar que todo está funcionando como se espera. require ‘my_test’ require ‘test/unit’ class TestTest < Test::Unit::TestCase def test_test t = MyTest.new assert_equal(Object, MyTest.superclass) assert_equal(MyTest, t.class) t.add(1) t.add(2) assert_equal([1,2], t.instance_eval(“@arr”)) end end produce: Finished in 0.002589 seconds. 1 tests, 3 assertions, 0 failures, 0 errors Una vez que estamos contentos con nuestras obras de ampliación, se puede instalar a nivel global mediante la ejecución de make install.
Objetos Ruby en C Cuando escribimos nuestra primera extensión, hicimos trampa, porque en realidad no se hace nada con los objetos Ruby --no se hacen los cálculos basados en cifras Ruby, por ejemplo. Antes de poder hacer esto, tenemos que encontrar la manera de representar y acceder a los datos tipo Ruby dentro de C. Todo en Ruby es un objeto, y todas las variables son referencias a objectos. Cuando vemos los objetos Ruby desde dentro de código en C, la situación es más o menos lo mismo. La mayoría de los objetos Ruby se representan como punteros C a una zona de memoria que contiene los datos del objeto y otros detalles de implementación. En el código C, todas estas referencias son a través de variables de tipo VALUE, por lo que se pase en cuanto a objetos Ruby, se hará por envío de valores. Esto tiene una excepción. Por motivos de rendimiento, Ruby implementa Fixnums, Symbols, true, false y nil como los llamados valores inmediatos. Estos aún están almacenados en variables de tipo VALUE, pero no son punteros. En cambio, su valor se almacena directamente en la variable. Así, a veces los valores son punteros y a veces son valores inmediatos. ¿Cómo funciona el intérprete para manejar esta magia? Se basa en el hecho de que todos los punteros apuntan a zonas de memoria alineadas en límites 4 ó 8 bytes. Esto significa que podemos garantizar que los 2 bits menos significativos en un puntero siempre serán cero. Cuando se quiere almacenar un valor inmediato, se las arregla para tener al menos uno de estos bits establecido, permitiendo al resto del código del intérprete distinguir los valores inmediatos de los punteros. Aunque esto suena complicado, es realmente fácil su uso en la práctica, en gran parte debido a que el intérprete viene con una serie de macros y métodos que simplifican el trabajo con el sistema de tipos.
188
Esta es la forma en que Ruby implementa código orientado a objetos en C: Un objeto Ruby es una estructura asignada en la memoria que contiene una tabla de variables de instancia y la información sobre la clase. La clase en sí es otro objeto (una estructura asignada en la memoria) que contiene una tabla de los métodos definidos para esa clase. Ruby está construido sobre esta base.
Trabajar con Objetos Inmediatos Como hemos dicho anteriormente, los valores inmediatos no son punteros: Fixnum, Symbol, true, false y nil son almacenados directamente en VALUE. Los valores Fixnum se almacenan como números de 31 bits (ó de 63 bits en las arquitecturas más amplias de CPU) que se forman al desplazar el número original un bit a la izquierda y luego establecer el LSB o bit menos significativo (bit 0), a 1. Cuando VALUE se utiliza como un puntero a una estructura específica Ruby, siempre está garantizado el tener el LSB en cero. Los valores inmediatos también tienen el LSB en cero. Por lo tanto, un simple testeo del bit puede indicar si se tiene un Fixnum. Este test está envuelto en una macro, FIXNUM_P. Otros test similares permiten comprobar si hay otros valores inmediatos. FIXNUM_P(value) SYMBOL_P(value) NIL_P(value) RTEST(value)
-> -> -> ->
nonzero nonzero nonzero nonzero
if if if if
value value value value
is is is is
a Fixnum a Symbol nil neither nil nor false
Varias macros útiles de conversión para números, así como otros tipos de datos estándar se muestran en la tabla 5 en la página siguiente. Los otros valores inmediatos (true, false y nil) se representan en C como las constantes Qtrue, Qfalse y Qnil, respectivamente. Se pueden testear las variables VALUE frente a estas constantes directamente o utilizar las macros de conversión (que llevan a cabo el casting adecuado).
Trabajando con Cadenas
En C, estamos acostumbrados a trabajar con cadenas de terminación nula. Las cadenas Ruby, sin embargo, son más generales y también se pueden incluir valores null incrustados. La manera más segura para trabajar con cadenas Ruby, por lo tanto, es hacer lo que el intérprete hace y utilizando un puntero y una longitud. De hecho, los objetos cadena de Ruby son en realidad referencias a una estructura RString , y esta estructura RString contiene una longitud y un campo de puntero. Puede acceder a la estructura a través de la macro RSTRING. VALUE str; RSTRING(str)->len RSTRING(str)->ptr
-> ->
length of the Ruby string pointer to string storage
Sin embargo, la vida es algo más complicada que esto. En lugar de utilizar directamente el objeto VALUE cuando se necesita un valor de cadena, es probable que se quiera llamar al método StringValue, pasándole el valor original. Esto retorna un objeto que se puede utilizar en RSTRING o lanzar una excepción si no se puede derivar una cadena a partir del original. Todo esto es parte de la iniciativa duck typing de Ruby 1.8, que se describe con más detalle más adelante. El método StringValue comprueba si su operando es un String. Si no, trata de invocar to_str en el objeto y lanza una excepción TypeError si no se puede. Por lo tanto, si quiere escribir algo de código que itera sobre todos los caracteres en un objeto String, se puede escribir: static VALUE iterate_over(VALUE original_str) { int i; char *p VALUE str = StringValue(original_str); p = RSTRING(str)->ptr; // puede ser null for (i = 0; i < RSTRING(str)->len; i++, p++) { 189
// process *p } return str; } Si no se desea utilizar la longitud y basta con acceder al puntero de cadena subyacente, puede utilizar el método de conveniencia StringValuePtr, que resuelve la referencia a cadena y retorna el puntero C al contenido. Si va a utilizar una cadena para acceder o para el control de algún recurso externo, es probable que desee conectar al mecanismo de corrupción de Ruby. En este caso vamos a usar el método SafeStringValue , que funciona como StringValue pero produce una excepción si el argumento está corrupto y el nivel de seguridad es mayor que cero.
Trabajando con Otros Objetos Cuando los VALUE no son inmediatos, son punteros a una de las estructuras objeto definidas en Ruby --no se puede tener un VALUE que apunta a un área de memoria arbitraria. Las estructuras para las clases básicas integradas se definen en ruby.h y se nombran de la forma RClasenombre: RArray, RBignum, RClass, RData, RFile, RFloat, RHash, RObject, RRegexp, RString y RStruct. Se puede chekear para ver qué tipo de estructura se utiliza para un determinado VALUE de varias maneras. El macro TYPE(obj) devuelve una constante que representa el tipo C de un objeto dado: T_OBJECT, T_STRING, etc. Las constantes para las clases integradas se definen en ruby.h. Tenga en cuenta que el tipo al que aquí nos referimos es un detalle de implementación --no es lo mismo que la clase de un objeto.
190
Si desea asegurarse de que un puntero value apunta a una estructura particular, puede utilizar la macro Check_Type, que provocará una excepción TypeError si value no es del tipo esperado (una de las constantes T_STRING, T_FLOAT, etc). Check_Type(VALUE value, int type) Una vez más, tenga en cuenta que estamos hablando de “tipo” como la estructura C que representa un particular tipo integrado. La clase de un objeto es una bestia completamente diferente. Los objetos clase para las clases integradas se almacenan en las variables C globales llamadas rb_cNombreclase (por ejemplo rb_cObject). Los módulos se denominan rb_mNombremodulo.
Acceso a Cadenas Anterior a la Versión 1.8 Antes de Ruby 1.8, si se suponía que VALUE contienía una cadena, era posible acceder a los campos RSTRING directamente, y eso era todo. En 1.8, sin embargo, la introducción gradual de duck typing (escritura pato), junto con varias optimizaciones, significa que este enfoque probablemente no funcionará de la manera que se desea. En particular, el campo ptr de un objeto STRING puede ser null para cadenas de longitud cero. Si se utiliza el método 1.8 StringValue, maneja este caso reseteando punteros nulos que referencian una única, compartida y vacía cadena. Así que, ¿cómo se escribe una extensión que funciona tanto con Ruby 1.6 como con 1.8? Cuidadosamente y con macros. Tal vez algo como esto #if !defined(StringValue) # define StringValue(x) (x) #endif #if !defined(StringValuePtr) # define StringValuePtr(x) ((STR2CSTR(x))) #end Este código define las macros 1.8 StringValue y StringValuePtr en términos de sus homólogos 1.6. Si escribe entonces código en función de estas macros, se debe compilar y ejecutar en los dos intérpretes, el antiguo y el nuevo. Si desea que el código tenga el comportamiento duck-typing 1.8, incluso cuando se ejecute en 1.6, es posible que quiera definir StringValue de forma ligeramente diferente. La diferencia entre esta y la anterior implementación se describe un poco más adelante. #if !defined(StringValue) # define StringValue(x) do { \ if (TYPE(x) != T_STRING) x = rb_str_to_str(x); \ } while (0) #end
No es conveniente modificar directamente los datos en estas estructuras C --se puede mirar, pero no tocar. En cambio, normalmente vamos a usar las funciones de C suministradas para manipular los datos Ruby (hablaremos más sobre esto en un momento).
No obstante, en aras de la eficiencia puede ser necesario profundizar en estas estructuras para la obtención de datos. Para desreferenciar los miembros de estas estructuras C, se tiene que emitir el VALUE genérico para el tipo apropiado de estructura. ruby.h contiene una serie de macros que realizan el reparto adecuado para usted, lo que le permite desreferenciar los miembros de la estructura de forma fácil. Estas macros se llaman RNOMBRECLASE, como RSTRING o RARRAY. Ya hemos visto el uso de RSTRING cuando se trabaja con cadenas. Se puede hacer lo mismo con las matrices.
191
VALUE arr; RARRAY(arr)->len RARRAY(arr)->capa RARRAY(arr)->ptr
-> -> ->
length of the Ruby array capacity of the Ruby array pointer to array storage
Hay accesores similares para los hashes (RHASH), archivos (RFILE), etc. Habiendo dicho todo esto, es necesario tener cuidado sobre la construcción de una excesiva dependencia en el chequeo de tipos en el código de extensión. Vamos a hablar más sobre las extensiones y el sistema de tipos Ruby un poco más adelante.
Variables Globales La mayoría de las veces, las extensiones implementan clases y el código Ruby utiliza estas clases. Los datos que se comparten entre el código Ruby y el código C serán pulcramente envueltos dentro de los objetos de la clase. Así es como debe ser. A veces, sin embargo, puede que tenga que implementar una variable global accesible tanto por la extensión C como por el código Ruby. La forma más sencilla de hacer esto es tener la variable como un VALUE (es decir, un objeto Ruby) y entonces, ligar la dirección de esta variable C con el nombre de una variable Ruby. En este caso, el prefijo $ es opcional, pero ayuda a aclarar que es una variable global. Y recordar: hacer global una variable basada en la pila Ruby no va a trabajar (por mucho tiempo). static VALUE hardware_list; static VALUE Init_SysInfo() { rb_define_class(....); hardware_list = rb_ary_new(); rb_define_variable(“$hardware”, &hardware_list); ... rb_ary_push(hardware_list, rb_str_new2(“DVD”)); rb_ary_push(hardware_list, rb_str_new2(“CDPlayer1”)); rb_ary_push(hardware_list, rb_str_new2(“CDPlayer2”)); }
Del lado de Ruby se puede acceder a la variable C hardware_list como $hardware.
$hardware
->
[“DVD”, “CDPlayer1”, “CDPlayer2”]
A veces, sin embargo, la vida es más complicada. Tal vez usted desea definir una variable global cuyo valor debe ser calculado cuando se accede a ella. Esto se hace mediante la definición de las variables de enganche y virtuales. Una variable de enganche es una variable real que se inicia por una función nombrada cuando se accede a la correspondiente variable Ruby. Las variables virtuales son similares, pero nunca se almacenan: su valor proviene puramente de la evaluación de la función de enlace. Vea la sección API que comienza un poco más adelante para más detalles. Si se crea un objeto Ruby desde C y se almacena en una variable C global sin exportarlo a Ruby, se debe por lo menos pasar el recolector de basura sobre él, no sea que se recoja inadvertidamente. static VALUE obj; // ... obj = rb_ary_new(); rb_global_variable(obj);
La Extensión Jukebox Hemos cubierto lo suficiente sobre los fundamentos como para volver ahora a nuestro ejemplo de jukebox --la interfaz de código C con Ruby y el intercambio de datos y comportamiento entre los dos mundos.
192
Envolver Estructuras C Tenemos la biblioteca del proveedor que controla las unidades de CD de audio del jukebox, y estamos listos para conectarlo a Ruby. El archivo de cabecera del proveedor se parece a esto. typedef struct _cdjb { int statusf; int request; void *data; char pending; int unit_id; void *stats; } CDJukebox; // Allocate a new CDJukebox structure CDJukebox *new_jukebox(void); // Assign the Jukebox to a player void assign_jukebox(CDJukebox *jb, int unit_id); // Deallocate when done (and take offline) void free_jukebox(CDJukebox *jb); // Seek to a disc, track and notify progress void jukebox_seek(CDJukebox *jb, int disc, int track, void (*done)(CDJukebox *jb, int percent)); // ... others... // Report a statistic double get_avg_seek_time(CDJukebox *jb); Este vendedor tiene que organizarse. Aunque es posible que no lo admita, el código está escrito con un sabor orientado a objetos. No sabemos lo que significan todos estos campos dentro de la estructura CDJukeBox, pero OK --lo podemos tratar como un montón opaco de bits. El Vendedor del código sabe qué hacer con él, nosotros sólo tenemos que trasladarlo. Cada vez que se tenga una estructura sólo en C que se quiera manejar como un objeto Ruby, se debe envolver en una clase especial e internos de Ruby llamada DATA (tipo T_DATA). Dos macros hacen este envoltorio y una macro recupera la estructura de vuelta.
La API: Envolviendo Tipos de Datos C VALUE Data_Wrap_Struct( VALUE class, void (*mark)(), void (*free)(), void *ptr ) Envuelve el tipo dado de datos C ptr, registros de las dos rutinas de recolección de basura (ver abajo), y devuelve un puntero VALUE a un verdadero objeto Ruby. El tipo C del objeto resultante es T_DATA, y su clase de Ruby es la class. VALUE Data_Make_Struct( VALUE class, c-type, void (*mark)(), void (*free)(), c-type * ) Asignación y configuración a cero de una estructura del tipo indicado y después procede como Data_Wrap_Struct. c-type es el nombre del tipo de datos C que está envolviendo, no una variable de ese tipo.
Data_Get_Struct( VALUE obj,c-type,c-type * ) Devuelve el puntero original. Esta macro es un contenedor de tipo seguro para toda la macro DATA_PTR(obj), que evalúa el puntero.
El objeto creado por Data_Wrap_Struct es un objeto normal de Ruby, excepto que tiene un adicional tipo de datos C al que no se puede acceder desde Ruby. Como se puede ver en la figura 14 en la página siguiente, este tipo de datos C es independiente de las variables de instancia que contiene el objeto. Pero puesto que es algo separada, ¿cómo deshacerse de él cuando el recolector de basura reclama este
193
objeto? ¿Qué pasa si hay que liberar algunos recursos (cerrar algún archivo, limpiar algún mecanismo de bloqueo o IPC, etc)? Ruby utiliza un esquema de marca y barrido de recolección de basura. Durante la fase de marca, Ruby busca punteros a zonas de memoria y marca estas áreas como “en uso” (porque algo está apuntando a ellas). Si esas mismas áreas contienen más punteros, la memoria de estos punteros de referencia también son marcados, y así sucesivamente. Al final de la fase de marca, toda la memoria a la que se hace referencia se ha marcado, y las áreas huérfanas no tienen marca. En este punto se inicia la fase de barrido, la liberación de memoria que no está marcada. Para participar en el proceso Ruby de marcar y barrer de la recolección de basura, es necesario definir una rutina para liberar su estructura y, posiblemente, una rutina para marcar las referencias de su estructura a otras estructuras. Ambas rutinas reciben un puntero void, una referencia a esa estructura. La rutina de marca será llamada por el recolector de basura durante su fase de “marca”. Si su estructura referencia otros objetos Ruby, entonces su función de marca tiene que identificar estos objetos utilizando rb_gc_mark(valor). Si la estructura no hace referencia a otros objetos Ruby, simplemente puede pasar 0 como un puntero a función. Cuando el objeto tiene que ser eliminado, el recolector de basura llamará a la rutina libre para liberarlo. Si usted tiene asignada alguna memoria para usted mismo (por ejemplo, mediante el uso de Data_Make_ Struct), tendrá que pasar una función libre --incluso si es justo la rutina free de la biblioteca estándar de C. Para asignaciones de estructuras complejas, es posible que su función libre tenga la necesidad de recorrer toda la estructura para liberar toda la memoria asignada. Echemos un vistazo a nuestra interfaz del reproductor de CD. La biblioteca del vendedor pasa la información entre sus diversas funciones en una estructura CDJukebox. Esta estructura representa el estado de la máquina de discos y por lo tanto es una buena candidata para envolverla dentro de nuestra clase Ruby. Creamos nuevas instancias de esta estructura mediante la llamada al método de biblioteca CDPlayerNew . A continuación, querríamos envolver la estructura creada dentro de un nuevo objeto Ruby CDPlayer. Un fragmento de código para hacer esto puede tener el siguiente aspecto. (Vamos a hablar del parámetro mágico klass en un minuto.) CDJukebox *jukebox; VALUE obj; // Vendor library creates the Jukebox jukebox = new_jukebox(); // then we wrap it inside a Ruby CDPlayer object obj = Data_Wrap_Struct(klass, 0, cd_free, jukebox); Una vez que se ejecuta este código, obj tendría una referencia a un objeto Ruby CDPlayer recién asignado, envolviendo a una nueva estructura C CDJukebox. Por supuesto, para obtener que este código compile, tendríamos que trabajar un poco más. Tendríamos que definir la clase CDPlayer y almacenar una referencia a él en la variable cCDPlayer. También tendríamos que definir la función para liberar nuestro objeto, cdplayer_free. Esto es fácil: simplemente se llama al método de biblioteca que provee el vendedor. static void cd_free(void *p) { free_jukebox(p); } Sin embargo, los fragmentos de código no hacen un programa. Tenemos que empaquetar todas estas cosas de una manera que se integren en el intérprete. Y para hacer esto, tenemos que ver algunas de las convenciones que utiliza el intérprete.
Creación de Objetos Ruby 1.8 ha racionalizado la creación y la inicialización de objetos. A pesar de que las formas antiguas todavía funcionan, la nueva forma, utilizando las funciones de asignación, es mucho más ordenada (y es menos probable que se deje de utilizar en el futuro).
194
La idea básica es simple. Digamos que usted está creando un objeto de la clase CDPlayer en su programa Ruby:
cd = CDPlayer.new Entre bastidores, el intérprete llama al método de clase new para CDPlayer. Como CDPlayer no tiene definido un método new, Ruby busca en su clase padre, la clase Class. La implementación de new en la clase Class es bastante simple: se asigna memoria para el nuevo objeto y luego se llama al método de objeto initialize para inicializar esta memoria. Por lo tanto, si nuestra extensión CDPlayer va a ser una buena habitante de Ruby, debe trabajar en este marco. Esto significa que tendremos que implementar una función de asignación y un método initialize .
Funciones de Asignación La función de asignación es la responsable de la creación de la memoria que va a utilizar el objeto. Si el objeto que estamos implementando no utiliza ningún otro dato que las variables de instancia Ruby, entonces no es necesario escribir una función de asignación --el asignador Ruby por defecto funciona bien. Pero si su clase envuelve una estructura C, tendrá que asignar espacio para esta estructura con la función de asignación. Ésta, se pasa a la clase del objeto que viene a ser asignado. En nuestro caso, con toda probabilidad un cCDPlayer, pero vamos a utilizar el parámetro como dado, ya que esto significa que vamos a trabajar correctamente si se subclasifica. static VALUE cd_alloc(VALUE klass) { CDJukebox *jukebox; VALUE obj; // Vendor library creates the Jukebox jukebox = new_jukebox(); // then we wrap it inside a Ruby CDPlayer object obj = Data_Wrap_Struct(klass, 0, cd_free, jukebox); return obj; }
A continuación, deberá registrar su función de asignación de código de inicialización de clase.
void Init_CDPlayer() { cCDPlayer = rb_define_class(“CDPlayer”, rb_cObject); rb_define_alloc_func(cCDPlayer, cd_alloc); // ... }
195
La mayoría de los objetos, probablemente también tengan la necesidad de definir un inicializador. La función de asignación crea un objeto vacío, sin inicializar, y tendremos que rellenarlo con los valores específicos. En el caso del reproductor de CD, se llama al constructor con el número de unidad del player para ser asociado a este objeto. static VALUE cd_initialize(VALUE self, VALUE unit) { int unit_id; CDJukebox *jb; Data_Get_Struct(self, CDJukebox, jb); unit_id = NUM2INT(unit); assign_jukebox(jb, unit_id); return self; } Una de las razones para este protocolo de varios pasos de creación de objeto, es que permite al intérprete manejar situaciones donde los objetos tienen que ser creados por el “procedimiento de puerta trasera.” Un ejemplo es cuando los objetos vienen a deserializarse de su forma de cálculo de referencias. En este caso, el intérprete tiene que crear un objeto vacío (llamando al asignador), pero no puede llamar a la inicialización (ya que no tiene conocimiento de los parámetros para su uso). Otra situación común es cuando los objetos están duplicados o clonados. Aquí se esconde otro tema. Debido a que los usuarios pueden optar por omitir el constructor, es necesario asegurarse de que el código de asignación deja el objeto retornado en un estado válido. No contendrá toda la información que hubiera tenido si se hubiera establecido mediante #initialize, pero al menos tiene que ser utilizable.
196
Asignación de Objetos Anterior a la Versión 1.8 Antes de Ruby 1.8, si se quería asignar espacio adicional en un objeto, o bien había que poner el código en el método initialize, o bien había que definir un nuevo método para su clase. Guy De- coux recomienda el siguiente enfoque híbrido para maximizar la compatibilidad entre las extensiones 1.6 y 1.8. static VALUE cd_alloc(VALUE klass) { // igual que antes } static VALUE cd_new(int argc, VALUE *argv, VALUE klass) { VALUE obj = rb_funcall2(klass, rb_intern(“allocate”), 0, 0); rb_obj_call_init(obj, argc, argv); return obj; } void init_CDPlayer() { // ... #if HAVE_RB_DEFINE_ALLOC_FUNC // 1.8 allocation rb_define_alloc_func(cCDPlayer, cd_alloc); #else // define manual allocation function for 1.6 rb_define_singleton_method(cCDPlayer, “allocate”, cd_alloc, 0); #endif rb_define_singleton_method(cCDPlayer, “new”, cd_new, 1); // ... } Si está escribiendo código que debe ejecutarse en las versiones antiguas y recientes de Ruby, tendrá que adoptar un enfoque similar a este. Sin embargo, es probable que también necesite manejar la clonación y la duplicación, y demás tendrá que considerar qué sucede cuando su objeto calcula las referencias.
Clonacion de Objetos Todos los objetos Ruby pueden ser copiados usando uno de los dos métodos, dup y clone. Los dos métodos son similares: ambos producen una nueva instancia de la clase receptora llamando a la función de asignación. Luego se copian las variables de instancia del original. clone va un poco más allá y copia la clase singleton original (si la tiene) y las banderas (como la bandera que indica que un objeto está congelado). Se puede pensar en dup como una copia de los contenidos y en clone como una copia del objeto completo. Sin embargo, el intérprete de Ruby no sabe cómo manejar la copia del estado interno de los objetos que se escriben como extensiones C. Por ejemplo, si su objeto envuelve una estructura C que contiene un descriptor de fichero abierto, depende de la semántica de su implementación si el descriptor simplemente se deben copiar en el nuevo objeto, o si se debe abrir un nuevo descriptor de fichero. Para manejar esto, el intérprete delega en su código la responsabilidad de copiar el estado interno de los objetos que se implementan. Después de copiar las variables de instancia del objeto, el intérprete invoca el método initialize_copy del nuevo objeto, pasandole una referencia al objeto original. Depende de usted el implementar la semántica significativa en este método. Para nuestra clase CDPlayer vamos a adoptar un enfoque bastante simple con la cuestión de la clonación: simplemente se va a copiar la estructura CDJukebox del objeto original.
197
Hay un pedacito de código extraño en este ejemplo. Para probar que el objeto original es de hecho algo que se puede clonar desde el nuevo, el código comprueba que el original
1. tiene un tipo T_DATA (lo que significa que es un objeto no esencial), y
2. tiene una función libre con la misma dirección que nuestra función libre.
Esta es una forma relativa de alto rendimiento para verificar que el objeto original es compatible con el nuestro (siempre y cuando no se compartan las funciones libres entre clases). Una alternativa, que es más lenta, sería utilizar rb_obj_is_kind_of y hacer una prueba directa de la clase. static VALUE cd_init_copy(VALUE copy, VALUE orig) { CDJukebox *orig_jb; CDJukebox *copy_jb; if (copy == orig) return copy; // we can initialize the copy from other CDPlayers // or their subclasses only if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)cd_free) { rb_raise(rb_eTypeError, “wrong argument type”); } // copy all the fields from the original // object’s CDJukebox structure to the new object Data_Get_Struct(orig, CDJukebox, orig_jb); Data_Get_Struct(copy, CDJukebox, copy_jb); MEMCPY(copy_jb, orig_jb, CDJukebox, 1); return copy; } Nuestro método de copia no tiene que asignar una estructura envuelta para recibir los objetos originales de la estructura CDJukebox: el método cd_alloc ya se ha ocupado de eso. Tenga en cuenta que en este caso es correcto hacer la comprobación de tipos basados en las clases: necesitamos que el objeto original tenga una estructura CDJukebox envuelta, y los únicos objetos que tienen una de estas se derivan de la clase CDPlayer.
Poniéndolo Todo Junto
OK, finalmente estamos listos para escribir todo el código de nuestra clase CDPlayer.
// Helper function to free a vendor CDJukebox static void cd_free(void *p) { free_jukebox(p); } // Allocate a new CDPlayer object, wrapping // the vendor’s CDJukebox structure static VALUE cd_alloc(VALUE klass) { CDJukebox *jukebox; VALUE obj; // Vendor library creates the Jukebox jukebox = new_jukebox(); // then we wrap it inside a Ruby CDPlayer object obj = Data_Wrap_Struct(klass, 0, cd_free, jukebox); return obj; } // Assign the newly created CDPLayer to a // particular unit static VALUE cd_initialize(VALUE self, VALUE unit) {
198
int unit_id; CDJukebox *jb; Data_Get_Struct(self, CDJukebox, jb); unit_id = NUM2INT(unit); assign_jukebox(jb, unit_id); return self; } // Copy across state (used by clone and dup). For jukeboxes, we // actually create a new vendor object and set its unit number from // the old static VALUE cd_init_copy(VALUE copy, VALUE orig) { CDJukebox *orig_jb; CDJukebox *copy_jb; if (copy == orig) return copy; // we can initialize the copy from other CDPlayers or their // subclasses only if (TYPE(orig) != T_DATA || RDATA(orig)->dfree != (RUBY_DATA_FUNC)cd_free) { rb_raise(rb_eTypeError, “wrong argument type”); } // copy all the fields from the original object’s CDJukebox // structure to the new object Data_Get_Struct(orig, CDJukebox, orig_jb); Data_Get_Struct(copy, CDJukebox, copy_jb); MEMCPY(copy_jb, orig_jb, CDJukebox, 1); return copy; } // The progress callback yields to the caller the percent complete static void progress(CDJukebox *rec, int percent) { if (rb_block_given_p()) { if (percent > 100) percent = 100; if (percent < 0) percent = 0; rb_yield(INT2FIX(percent)); } } // Seek to a given part of the track, invoking the progress callback // as we go static VALUE cd_seek(VALUE self, VALUE disc, VALUE track) { CDJukebox *jb; Data_Get_Struct(self, CDJukebox, jb); jukebox_seek(jb, NUM2INT(disc), NUM2INT(track), progress); return Qnil; } // Return the average seek time for this unit static VALUE cd_seek_time(VALUE self) { double tm; CDJukebox *jb; Data_Get_Struct(self, CDJukebox, jb); tm = get_avg_seek_time(jb); return rb_float_new(tm); } // Return this player’s unit number static VALUE
199
cd_unit(VALUE self) { CDJukebox *jb; Data_Get_Struct(self, CDJukebox, jb); return INT2NUM(jb->unit_id);; } void Init_CDPlayer() { cCDPlayer = rb_define_class(“CDPlayer”, rb_cObject); rb_define_alloc_func(cCDPlayer, cd_alloc); rb_define_method(cCDPlayer, “initialize”, cd_initialize, 1); rb_define_method(cCDPlayer, “initialize_copy”, cd_init_copy, 1); rb_define_method(cCDPlayer, “seek”, cd_seek, 2); rb_define_method(cCDPlayer, “seek_time”, cd_seek_time, 0); rb_define_method(cCDPlayer, “unit”, cd_unit, 0); } Ahora podemos controlar nuestra máquina de discos desde Ruby de una forma agradable y orientada a objetos. require ‘CDPlayer’ p = CDPlayer.new(13) puts “Unit is #{p.unit}” p.seek(3, 16) {|x| puts “#{x}% done” } puts “Avg. time was #{p.seek_time} seconds” p1 = p.dup puts “Cloned unit = #{p1.unit}” produce: Unit is 13 26% done 79% done 100% done Avg. time was 1.2 seconds Cloned unit = 13 Este ejemplo demuestra la mayor parte de lo que hemos hablado hasta ahora, con una muy buena característica adicional. La biblioteca del vendedor proporciona una rutina de retorno de llamada --un puntero a función a la que se llama de vez en cuando mientras que el hardware está trabajando en el camino hacia el siguiente disco. Hemos establecido hasta aquí la ejecución de un bloque de código que se pasa como argumento a seek. En la función progress, comprobamos para ver si hay un iterador en el contexto actual y, si existe, se ejecuta con el porcentaje en curso como un argumento.
Asignación de Memoria A veces puede ser necesario asignar memoria a una extensión que no será utilizada para el almacenamiento de objetos - tal vez usted tiene un mapa de bits gigante para un filtro Bloom, una imagen o un montón de pequeñas estructuras que Ruby no utiliza directamente. Para trabajar correctamente con el recolector de basura, se deben utilizar las rutinas de asignación de memoria siguientes. Estas rutinas hacen un poco más de trabajo que el estándar malloc. Por ejemplo, si ALLOC_N determina que no puede asignar la cantidad deseada de memoria, se invoca el recolector de basura para tratar de recuperar algo de espacio. Se genera un NoMemError si no se puede o si la cantidad de memoria solicitada no es válida.
API: Asignación de Memoria type * ALLOC_N( c-type, n ) Asigna n objetos c-tipo, donde c-tipo es el nombre literal del tipo C, no una variable de ese tipo. type * ALLOC( c-type )
200
Asigna un y convierte el resultado a un puntero de ese tipo.
REALLOC_N( var, c-type, n ) Reasigna n c-tipos y asigna el resultado a var, un puntero a una variable de tipo c-tipo. type * ALLOCA_N( c-type, n ) Asigna memoria para n objetos c-tipo en la pila --esta memoria se libera automáticamente cuando la función que invoca ALLOCA_N retorna.
Sistema de Tipos Ruby En Ruby, se depende menos del tipo (o clase) de un objeto y más de sus capacidades. Esto se llama duck typing. Se describen con más detalle más adelante. Se encontrarán muchos ejemplos de esto si se examina el código fuente del propio intérprete. Por ejemplo, el siguiente código implementa el método Kernel.exec. VALUE rb_f_exec(argc, argv) int argc; VALUE *argv; { VALUE prog = 0; VALUE tmp; if (argc == 0) { rb_raise(rb_eArgError, “wrong number of arguments”); } tmp = rb_check_array_type(argv[0]); if (!NIL_P(tmp)) { if (RARRAY(tmp)->len != 2) { rb_raise(rb_eArgError, “wrong first argument”); } prog = RARRAY(tmp)->ptr[0]; SafeStringValue(prog); argv[0] = RARRAY(tmp)->ptr[1]; } if (argc == 1 && prog == 0) { VALUE cmd = argv[0]; SafeStringValue(cmd); rb_proc_exec(RSTRING(cmd)->ptr); } else { proc_exec_n(argc, argv, prog); } rb_sys_fail(RSTRING(argv[0])->ptr); return Qnil; /* dummy */ } El primer parámetro de este método puede ser una cadena, o una matriz que contiene dos cadenas. Sin embargo, el código no hace explícitamente comprobación del tipo del argumento. En su lugar, primero llama a rb_check_array_type, pasándolo en el argumento. ¿Qué es lo que hace este método? Vamos a ver. VALUE rb_check_array_type(ary) VALUE ary; { return rb_check_convert_type(ary, T_ARRAY, “Array”, “to_ary”); }
La trama se complica. Vamos a rastrear rb_check_convert_type.
201
VALUE rb_check_convert_type(val, type, tname, method) VALUE val; int type; const char *tname, *method; { VALUE v; /* always convert T_DATA */ if (TYPE(val) == type && type != T_DATA) return val; v = convert_type(val, tname, method, Qfalse); if (NIL_P(v)) return Qnil; if (TYPE(v) != type) { rb_raise(rb_eTypeError, “%s#%s should return %s”, rb_obj_classname(val), method, tname); } return v; } Ahora estamos llegando a alguna parte. Si el objeto es del tipo correcto (T_ARRAY en nuestro ejemplo), se retorna el objeto original. De lo contrario..., no nos demos por vencidos todavía. En su lugar, llamamos a nuestro objeto original y le pedimos que si se puede representar como una matriz (que llama a su método to_ary). Si se puede, nos alegramos y continuamos. El código está diciendo “yo no necesito un Array, sólo necesito algo que se pueda representar como una matriz”. Esto significa que Kernel.exec lo aceptará como un parámetro matriz cualquiera que implementa un método to_ary. Se discuten estos protocolos de conversión con más detalle (pero desde la perspectiva de Ruby) más adelante. ¿Qué significa todo esto para usted como escritor de extensión? Hay dos mensajes. En primer lugar, tratar de evitar la comprobación de los tipos para parámetros que se le pasan. En su lugar, ver si hay un método rb_check_xxx_type que cnvierte el parámetro en el tipo que se necesita. Si no, buscar si existe una función conversora (como rb_Array, rb_Float o rb_Integer) que vaya a hacer el truco para usted. En segundo lugar, si se está escribiendo una extensión que implementa algo que puede ser utilizao significativamente como una cadena o una matriz Ruby, considerar la implementación de los métodos to_str o to_ary, permitiendo así la implementación de los objetos por su extensión para ser utilizado en contextos de cadena o matriz.
La Creación de una Extensión Después de haber escrito el código fuente de una extensión, tenemos que compilarlo para que Ruby pueda utilizarlo. Podemos hacer esto como un objeto compartido que se carga dinámicamente en tiempo de ejecución, o se vinculando estáticamente la extensión en el intérprete principal mismo de Ruby. El procedimiento básico es el mismo.
1. 2. 3. 4. 5. 6.
Crear el archivo o archivos fuente de código C en un directorio determinado. Opcionalmente, crear un archivo Ruby de soporte en un subdirectorio lib. Crear extconf.rb. Ejecutar extconf.rb para crear un Makefile para los archivos C en este directorio. Ejecutar make. Ejecutar make install.
La Creación de un Makefile con extconf.rb La figura 15 muestra el flujo de trabajo en la construcción de una extensión. La clave de todo el proceso es el programa extconf.rb que usted, como desarrollador, tiene que crear. En el fichero extconf.rb , se escribe un sencillo programa que determina qué características estarán disponibles en el sistema de usuario y donde se encuentran estas características. La ejecución de extconf.rb construye un Makefile personalizado, adaptado tanto para la aplicación como para el sistema en el que está siendo compilado. Cuando se ejecuta el comando make contra este Makefile, se construye e instala (opcional) la extensión.
202
Un fichero extconf.rb puede constar en su forma más sencilla de tan sólo dos líneas de código, y para muchas extensiones, esto es suficiente. require ‘mkmf’ create_makefile(“Test”) La primera línea trae el módulo de librería mkmf (que se describe más adelante). Éste contiene todos los comandos que se van a utilizar. La segunda línea crea un Makefile para una extensión llamada “Test” (Tenga en cuenta que “Test” es el nombre de la extensión, al makefile siempre se le llamará Makefile). Test se construirá a partir de todos los archivos de código fuente C en el directorio actual . Cuando se carga el código, Ruby llamará a su método Init_Test. Digamos que ejecutamos este programa extconf.rb en un directorio que contiene un solo archivo fuente, main.c. El resultado es un makefile que construye nuestra extensión. En una máquina Linux, este makefile ejecuta los siguientes comandos. gcc -fPIC -I/usr/local/lib/ruby/1.8/i686-linux -g -O2 \ -c main.c -o main.o gcc -shared -o Test.so main.o -lc El resultado de esta recopilación es Test.so, que puede vincularse dinámicamente a Ruby en tiempo de ejecución con require. En Mac OS X, los comandos son diferentes, pero el resultado es el mismo: un objeto compartido se crea (un paquete (bundle) en Mac) . gcc -fno-common -g -O2 -pipe -fno-common \ -I/usr/lib/ruby/1.8/powerpc-darwin \ -I/usr/lib/ruby/1.8/powerpc-darwin -c main.c cc -dynamic -bundle -undefined suppress -flat_namespace \ -L’/usr/lib’ -o Test.bundle main.o -ldl -lobjc Observe cómo los comandos mkmf han situado de forma automática las bibliotecas específicas de la plataforma y las opciones específicas utilizadas para el compilador local. Impresionante, ¿eh? Aunque este programa extconf.rb básico funciona para muchas extensiones simples, puede que tenga que trabajar un poco más, si la extensión necesita los archivos de cabecera o bibliotecas que no 203
están incluidas en el entorno de compilación por defecto, o si la compilación de código condicional está basada en la presencia de bibliotecas o funciones. Un requisito común es especificar los directorios no estándar donde se puedan encontrar otros ficheros y librerías que se incluyan. Se trata de un proceso en dos pasos. En primer lugar, su fichero extconf.rb debe contener uno o más comandos dir_config. Esto especifica una etiqueta para un conjunto de directorios. A continuación, cuando se ejecuta extconf.rb, decirle en mkmf donde se encuentran los directorios físicos correspondientes del sistema en curso.
Dividir el espacio de nombres Los escritores de extensión están siendo cada día mejores ciudadanos. En lugar de instalar su directorio de trabajo en uno de los directorios de las librerías de Ruby, están usando subdirectorios para agrupar sus archivos juntos. Esto es fácil con extconf.rb. Si el parámetro para la llamada create_makefile contiene delante barras diagonales, mkmf asume que todo lo que hay antes de la última barra es un nombre de directorio y que el resto es el nombre de extensión. La extensión se instalará en el directorio dado (en relación con el árbol de directorios Ruby). En el siguiente ejemplo, la exrtensión sigue siendo denominada Test. require ‘mkmf’ create_makefile(“wibble/Test”)
Sin embargo, en caso de requerirse esta clase en un programa Ruby, hay que escribir
require ‘wibble/Test’
Si extconf.rb contiene la línea dir_config(nombre), entonces dará la ubicación de los directorios correspondientes con las opciones de línea de comandos --with-name-include=directory Añadir el directorio/include para el comando de compilación. --with-name-lib=directory Añadir el directorio/lib al comando de enlazado. Si (como es común) los directorios de inclusión y de librerías son los subdirectorios llamados include y lib de algún otro directorio, puede tomar un atajo. --with-name-dir=directory Añadir el directorio/lib y el directorio/include a los comandos de enlazado y de compilación, respectivamente. Así como especificar todas estas opciones “--with” a la hora de ejecutar extconf.rb, también se puede utilizar --with con las opciones que se especificaron cuando Ruby se construyó para su máquina. Esto significa que usted puede descubrir y utilizar la ubicación de las bibliotecas que se utilizan por Ruby mismo. Para concretar, digamos que se necesitan utilizar las bibliotecas CDJukebox del proveedor y los ficheros para el reproductor de CD que estamos desarrollando. El fichero extconf.rb puede contener: require ‘mkmf’ dir_config(‘cdjukebox’) # ... más cosas create_makefile(“CDPlayer”)
Y entonces, se ejecuta extconf.rb de forma así como
204
% ruby extconf.rb --with-cdjukebox-dir=/usr/local/cdjb El Makefile generado podría asumir que /usr/local/cdjb/lib contiene las bibliotecas y /usr/ local/cdjb/include los archivos de inclusión. El comando dir_config se añade a la lista de lugares para la búsqueda de bibliotecas y archivos a incluir. Lo que no hace, en cambio, es vincular las bibliotecas a su aplicación. Para ello, tendrá que utilizar uno o más comandos have_library o find_library. have_library busca un punto de entrada en la biblioteca dada. Si no encuentra el punto de entrada, se añade la biblioteca a la lista de las bibliotecas a utilizar cuando se enlaza la extensión. find_library es similar, pero le permite especificar una lista de directorios de búsqueda para la biblioteca. Estos son los contenidos del fichero extconf.rb que utilizamos para enlazar nuestro reproductor de CD: require ‘mkmf’ dir_config(“cdjukebox”) have_library(“cdjukebox”, “new_jukebox”) create_makefile(“CDPlayer”) Una biblioteca particular, puede estar en diferentes lugares en función del sistema del host. El sistema X Window, por ejemplo, es notorio que habita en diferentes directorios en diferentes sistemas. El comando find_library buscará en la lista de directorios suministrada, para encontrar el correcto (es diferente de have_library, que sólo utiliza la información de configuración para la búsqueda). Por ejemplo, para crear un Makefile que utiliza X Windows y una biblioteca JPEG, extconf.rb puede contener require ‘mkmf’ if have_library(“jpeg”,”jpeg_mem_init”) find_library(“X11”, “XOpenDisplay”, “/usr/X11/lib”, # “/usr/X11R6/lib”, # “/usr/openwin/lib”) # then create_makefile(“XThing”) else puts “No X/JPEG support available” end
and list of directories to check for library
Hemos añadido algunas funcionalidades adicionales a este programa. Todos los comandos mkmf retornan false si fallan. Esto significa que puede escribir un extconf.rb que genera un Makefile sólo si todo lo que necesita está presente. La distribución Ruby hace esto para tratar de compilar sólo las extensiones que son compatibles con su sistema. También puede querer que su código de extensión pueda configurar las características que utiliza en función del entorno de destino. Por ejemplo, nuestro reproductor de máquina de discos puede ser capaz de utilizar un decodificador MP3 de alto rendimiento, si el usuario final tiene uno instalado. Se puede comprobar mediante la búsqueda de su archivo de cabecera. require ‘mkmf’ dir_config(‘cdjukebox’) have_library(‘cdjb’, ‘CDPlayerNew’) have_header(‘hp_mp3.h’) create_makefile(“CDJukeBox”) También puede comprobar para ver si el entorno de destino tiene una función particular en cualquiera de las bibliotecas que va a utilizar. Por ejemplo, la llamada setpriority sería útil, pero no siempre es tá disponible. Se puede comprobar con require ‘mkmf’
205
dir_config(‘cdjukebox’) have_func(‘setpriority’) create_makefile(“CDJukeBox”) Ambos, have_header y have_func definen constantes de preprocesador si encuentran sus objetivos. Los nombres están formados por la conversión del nombre de destino en mayúsculas y anteponiendo HAVE_. El código C puede tomar ventaja de esta construcción tal como #if defined(HAVE_HP_MP3_H) # include #endif #if defined(HAVE_SETPRIORITY) err = setpriority(PRIOR_PROCESS, 0, 10) #endif Si usted tiene requisitos especiales que no se pueden cumplir con todos estos comandos mkmf, su programa puede añadir directamente las variables globales $CFLAGS y $LFLAGS, que se pasan al compilador y enlazador, respectivamente. A veces creamos un fichero extconf.rb y simplemente parece no funcionar. Se le da el nombre de una biblioteca, y jura que no se dispone de la biblioteca como que jamás ha existido en todo el planeta. Se ajusta y se hacen cambios, pero mkmf todavía no puede encontrar la biblioteca que se necesita. Sería bueno si se pudiera saber exactamente lo que está haciendo entre bambalinas. Bueno, se puede. Cada vez que se ejecuta el script extconf.rb, mkmf genera un archivo de registro que contiene los detalles de lo que hizo. Si nos fijamos en el fichero mkmf.log, podremos ver los pasos que el programa ha utilizado para tratar de encontrar las bibliotecas que se han solicitado. A veces, intentar estos pasos de forma manual ayuda a localizar el problema.
La Instalación de Destino El Makefile producido por el fichero extconf.rb incluirá una “instalación” de destino. Esto copiará la biblioteca compartida objeto en el lugar correcto en su sistema de archivos de usuario (o usuarios) local. El destino está ligado a la ubicación de la instalación del intérprete Ruby que se utiliza en primer lugar para ejecutar extconf.rb. Si se tienen varios interpretes Ruby instalados en el sistema, la extensión se instala en el árbol de directorios del que ejecutó extconf.rb. Además de instalar la biblioteca compartida, extconf.rb comprobará la presencia de un subdirectorio /lib. Si encuentra uno, se encargará de todos los archivos Ruby que haya que instalar junto con su objeto compartido. Esto es útil si desea dividir el trabajo de escritura de la extensión en código de bajo nivel C y en código de alto nivel Ruby.
Enlazamiento Estático Finalmente, si su sistema no admite la vinculación dinámica, o si usted tiene un módulo de extensión que desea tener enlazado estáticamente a Ruby, hay que editar el archivo ext/Setup de la distribución y añadir su directorio a la lista de extensiones. En el directorio de su extensión, cree un fichero llamado MANIFEST que contenga una lista de todos los archivos de su extensión (fuentes, extconf.rb, lib/, etc). A continuación, reconstruya Ruby. Las extensiones que figuren en el fichero Setup serán enlazadas estáticamente al archivo ejecutable de Ruby. Si se desea desactivar cualquier enlace dinámico, y enlazar todas las extensiones de forma estática, hay editar ext/Setup para que contenga la siguiente opción: option nodynamic
Un Atajo Si está ampliando una biblioteca ya existente escrita en C o C++, es posible que desee investigar SWIG (http://www.swig.org). SWIG es un generador de interfaz: toma una definición de biblioteca (por lo general a partir de un archivo de cabecera) y genera automáticamente el código de unión necesario para acceder a la biblioteca de otro lenguaje. SWIG soporta Ruby, lo que significa que pueden generar los ar-
206
chivos de código fuente en C que envuelven las librerías externas en las clases Ruby.
Inrtegración de un intérprete de Ruby Además de extender Ruby añadiéndole código C, también se le puede dar la vuelta al problema e incrustar Ruby dentro de una aplicación. Hay tiene dos maneras de hacer esto. La primera es dejar que el intérprete tome el control llamando a ruby_run. Este es el método más sencillo, pero tiene un inconveniente importante --el intérprete nunca regresa de una llamada a ruby_run. He aquí un ejemplo: #include “ruby.h” int main(void) { /* ... las cosas propias de la aplicación ... */ ruby_init(); ruby_init_loadpath(); ruby_script(“embedded”); rb_load_file(“start.rb”); ruby_run(); exit(0); } Para inicializar el intérprete de Ruby, es necesario llamar a ruby_init (). Sin embargo, en algunas plataformas, puede ser necesario tomar medidas especiales antes de esto. #if defined(NT) NtInitialize(&argc, &argv); #endif #if defined(__MACOS__) && defined(__MWERKS__) argc = ccommand(&argv); #endif Ver main.c en la distribución Ruby para caulquier otra definición especial o configuración necesaria para su plataforma. Es necesario incluir los archivos Ruby de inclusión y poner los de librería accesibles para compilar el código incrustado. En mi ordenador (Mac OS X) tengo el intérprete Ruby 1.8 instalado en un directorio privado, por lo que mi Makefile tiene el siguiente aspecto. WHERE=/Users/dave/ruby1.8/lib/ruby/1.8/powerpc-darwin/ CFLAGS=-I$(WHERE) -g LDFLAGS=-L$(WHERE) -lruby -ldl -lobjc embed: embed.o $(CC) -o embed embed.o $(LDFLAGS) La segunda forma de embeber Ruby permite al código Ruby y al código C participar en más de un diálogo: el código C llama a algún código Ruby, y el código Ruby responde. Para ello, se incializa el intérprete como de costumbre. Entonces, en lugar de entrar en el bucle principal del intérprete, se llama a los métodos específicos en el código Ruby. Cuando estos métodos retornan, el código C toma el control de nuevo. Sin embargo, hay una pega. Si el código Ruby genera una excepción y no es atrapada, el programa C terminará. Para superar esto, se tiene que hacer lo que el intérprete hace y proteger todas las llamadas que podrían lanzar una excepción. Esto puede causar problemas. La llamada al método rb_protect envuelve la llamada a otra función C. Esta segunda función debe invocar al método Ruby. Sin embargo, el método envuelto por rb_protect está definido para tomar un sólo único parámetro. Para pasar más implica algún feo lance en C. Veamos un ejemplo. He aquí una simple clase Ruby que implementa un método para devolver la suma de los números desde uno al max.
207
class def end end
Summer sum(max) raise “Invalid maximum #{max}” if max < 0 (max*max + max)/2
Vamos a escribir un programa en C que llama a una instancia de esta clase varias veces. Para crear el ejemplo, vamos a obtener el objeto de la clase (mediante la búsqueda de una constante de alto nivel cuyo nombre es el nombre de nuestra clase). A continuación, le pedimos a Ruby crear una instancia de esa clase --rb_class_new_instance es en realidad una llamada a Class.new. (Los dos 0 parámetros iniciales son el recuento de argumentos y un puntero ficticio a los argumentos mismos). Una vez que tengamos este objeto, se puede invocar su método sum con rb_funcall. #include “ruby.h” static int id_sum; int Values[] = { 5, 10, 15, -1, 20, 0 }; static VALUE wrap_sum(VALUE args) { VALUE *values = (VALUE *)args; VALUE summer = values[0]; VALUE max = values[1]; return rb_funcall(summer, id_sum, 1, max); } static VALUE protected_sum(VALUE summer, VALUE max) { int error; VALUE args[2]; VALUE result; args[0] = summer; args[1] = max; result = rb_protect(wrap_sum, (VALUE)args, &error); return error ? Qnil : result; } int main(void) { int value; int *next = Values; ruby_init(); ruby_init_loadpath(); ruby_script(“embedded”); rb_require(“sum.rb”); // get an instance of Summer VALUE summer = rb_class_new_instance(0, 0, rb_const_get(rb_cObject, rb_intern(“Summer”))); id_sum = rb_intern(“sum”); while (value = *next++) { VALUE result = protected_sum(summer, INT2NUM(value)); if (NIL_P(result)) printf(“Sum to %d doesn’t compute!\n”, value); else printf(“Sum to %d is %d\n”, value, NUM2INT(result)); } ruby_finalize(); exit(0); } Una última cosa: el intérprete de Ruby no fue escrito originalmente con la incrustación en mente. Probablemente, el mayor problema es que se mantiene el estado de las variables globales, por lo que no es un subproceso seguro (thread-safe). Se puede integrar Ruby --un solo intérprete por proceso. Un buen recurso para embeber Ruby en C++ se encuentra en http://metaeditor.sourceforge. net/embed/. Esta página también contiene enlaces a otros ejemplos de incrustación Ruby.
208
API Ruby Empotrado void ruby_init( ) Crea e inicializa el intérprete. Esta función debe ser llamada antes de cualquier otra relacionada a Ruby. void ruby_init_loadpath( ) Inicializa la variable $: (la ruta de carga), es necesario si el código carga cualquier módulo de librería. void ruby_options( int argc, char **argv ) Da las opciones de línea de comandos del intérprete de Ruby. void ruby_script( char *name ) Establece el nombre del script Ruby (y $0) a nombre. void rb_load_file( char *file ) Carga el archivo dado en el intérprete. void ruby_run( ) Ejecuta el intérprete. void ruby_finalize( ) Cierra el intérprete. Para otro ejemplo de incorporación de un intérprete Ruby dentro de otro programa, véase también eruby, que hemos descrito en páginas anteriores.
Puenteando Ruby y Otros Lenguajes Hasta ahora, hemos hablado de la extensión de Ruby añadiendo rutinas escritas en C. Sin embargo, puede escribir extensiones en casi cualquier lenguaje, siempre y cuando se puedan unir los dos lenguajes con C. Casi todo es posible, incluidos los matrimonios difíciles de Ruby y C++, Ruby y Java, etc. Pero se puede ser capaz de lograr lo mismo sin recurrir a código C. Por ejemplo, se puede puentear a otros lenguajes usando middleware tales como SOAP o COM. Ver la sección sobre SOAP y la sección sobre inicio de automatización de Windows en páginas anteriores para más detalles.
API Ruby C Por último, pero no menos importante, aquí están algunas de las funciones de nivel C que pueden serle de utilidad al escribir una extensión. Algunas funciones requieren un ID: se puede obtener un ID para una cadena mediante rb_intern y reconstruir el nombre de un ID mediante rb_id2name. Como la mayoría de estas funciones C tienen equivalentes Ruby que ya se han descrito en detalle en este libro, las descripciones que hagamos aquí, serán breves. La siguiente lista no es completa. Muchas más funciones están disponibles --ya que resultaría demasiado documentar todas. Si se necesita un método que no se encuentra aquí, mire en ruby.h o intern.h para posibles candidatos. Además, en o cerca de la parte inferior de cada fichero fuente hay un conjunto de definiciones de métodos que describen la unión de los métodos Ruby a las funciones C. Usted puede ser capaz de llamar a la función C directamente o buscando una función contenedora que llama a la función que usted necesita. La siguiente lista, basada en la de README.EXT, muestra los archivos fuente principales del intérprete.
209
Core del Lenguaje Ruby class.c, error.c, eval.c, gc.c, object.c, parse.y, variable.c Funciones de Utilidad dln.c, regex.c, st.c, util.c Intérprete Ruby dmyext.c, inits.c, keywords main.c, ruby.c, version.c Librería Base array.c, bignum.c, compar.c, dir.c, enum.c, file.c, hash.c, io.c, marshal.c, math.c, numeric.c, pack.c, prec.c, process.c, random.c, range.c, re.c, signal.c, sprintf.c, string.c, struct.c, time.c
API: Definición de Clases VALUE rb_define_class( char *name, VALUE superclass ) Define una nueva clase en el nivel superior con el nombre y la superclase dados (para la clase Object, utilice rb_cObject). VALUE rb_define_module( char *name ) Define un nuevo módulo en el nivel superior con el nombre dado. VALUE rb_define_class_under( VALUE under, char *name, VALUE superclass ) Define una clase anidada dentro de la clase o módulo under. VALUE rb_define_module_under( VALUE under, char *name ) Define un módulo anidada en la clase o módulo under. void rb_include_module( VALUE parent, VALUE module ) Incluye el módulo dado en la clase o padre del módulo. void rb_extend_object( VALUE obj, VALUE module ) Extender obj con el modulo. VALUE rb_require( const char *name ) Equivalente a require nombre. Retorna Qtrue o Qfalse.
API: Definición de Estructuras VALUE rb_struct_define( char *name, char *attribute..., NULL ) Define una nueva estructura con los atributos dados. VALUE rb_struct_new( VALUE sClass, VALUE args..., NULL ) Crea una instancia de sClass con los valores de atributo dados. VALUE rb_struct_aref( VALUE struct, VALUE idx ) Devuelve el elemento nombrado o indexado por idx. VALUE rb_struct_aset( VALUE struct, VALUE idx, VALUE val ) Establece el atributo nombrado o indexado desde idx a val. ... Tambien están ==> API: Defining Methods, API: Defining Variables and Constants, API: Calling Methods, API: Exceptions, API: Iterators, API: Accessing Variables, API: Object Status y API: Commonly Used Methods. Se pueden ver en el original en inglés de Programming Ruby (2nd edition): The Pragmatic Programmers’ Guide.
210
Ruby Cristalizado El Lenguaje Ruby Este capítulo es una mirada de arriba a abajo del lenguaje Ruby. La mayor parte de lo que aparece aquí es la sintaxis y la semántica del lenguaje en sí mismo --que en su mayoría se ignoran las clases y los módulos integrados (que se tratan en profundidad en la sección siguiente). Ruby a veces implementa caraacterísticas en sus librerías que en la mayoría de los lengiuajes son parte de la sintaxis básica. Hemos incluido aquí estos métodos y hemos tratado de marcar ésto con “Librería” en el margen. El contenido de este capítulo puede parecer familiar --por una buena razón. Hemos cubierto casi todo esto como tutorial en los capítulos anteriores. Considere la posibilidad de ver este capítulo como una referencia independiente para el núcleo del lenguaje Ruby.
Diseño de los Fuentes Los programas Ruby están escritos en ASCII de 7 bits, Kanji (utilizando EUC o SJIS) o UTF-8. Si se usa un conjunto de códigos que no sean ASCII de 7 bits, la opción KCODE debe ser un valor apropiado, como dijimos anteriormente. Ruby es un lenguaje orientado a línea. Las expresiones y las declaraciones Ruby se terminan al final de una línea a menos que el analizador pueda determinar que la declaración está incompleta --por ejemplo, si el último símbolo de una línea es un operador o una coma. Se puede utilizar un punto y coma para separar las expresiones múltiples en una línea. También se puede poner una barra invertida al final de una línea para continuar en la siguiente. Los comentarios comienzan con # y corren hasta el final de la línea física. Los comentarios son ignorados durante el análisis sintáctico. a b d e
= 1 = 2; c = 3 = 4 + 5 + 6 + 7 = 8 + 9 \ + 10
# no necesita ‘\’ # necesita‘\’
Las líneas físicas entre una línea que comienza con =begin y otra línea que comienza con =end son ignoradas por Ruby y pueden usarse para comentar secciones de código o para incrustar documentación. Ruby lee la entrada del programa de una sola pasada, por lo que se pueden canalizar los programas al intérprete Ruby a través de un pipe a la entrada estandar. echo ‘puts “Hello”’ | ruby Si Ruby llega a través de una línea en cualquier parte del código fuente que contiene sólo “__END__”, sin espacios en blanco iniciales o finales, esa línea se trata como el final del programa --las líneas subsiguientes no serán tratadas como código del programa. Sin embargo, estas últimas se pueden leer en el programa que se está ejecutando utilizando el objeto IO global DATA, descrito más adelante.
Bloques BEGIN / END Cada archivo de código fuente en Ruby puede declarar bloques de código que se ejecutan como un archivo que se carga (los bloques BEGIN) y también después que el programa ha terminado de ejecutar (los bloques END). BEGIN { begin code } END { end code }
211
Un programa puede incluir varios bloques BEGIN y END. Los bloques BEGIN se ejecutan en el orden en que se encuentran. Los bloques END se ejecutan en orden inverso.
Delimitadores Generales de Entrada Así como el mecanismo normal de entrecomillado, las formas alternativas de cadenas literales, matrices, expresiones regulares y comandos de la shell se especifican mediante una sintaxis de delimitacióngeneralizada. Todos estos literales comienzan con un carácter de porcentaje, seguido de un carácter único que identifica el tipo de literal. Estos carácteres se resumen en: %q %Q, % %w, %W %r %x
Cadena entre comillas simples. Cadena entre comillas dobles. Matriz de cadenas. Patrón de expresión regular. Comandos de shell.
Los literales se describen en las secciones correspondientes en este mismo capítulo.
Siguiendo al carácter de tipo hay un delimitador, que puede ser cualquier carácter no alfabético o no multibyte. Si el delimitador es uno de los carácteres (, [, { o <, el literal se compone de los caractereshasta el delimitador de cierre correspondiente, teniendo en cuenta los pares de delimitadores anidados. Para todos los demás delimitadores, el literal comprende los caracteres hasta la siguiente aparición de carácter delimitador. %q/this is a string/ %q-string%q(a (nested) string) Las cadenas delimitadas pueden continuar en varias líneas; los finales de línea y todos los espacios al principio de las líneas de continuación se incluyen en la cadena. meth = %q{def fred(a) a.each {|i| puts i } end}
Los tipos Básicos Los tipos básicos de Ruby son números, cadenas, arrays, hashes, rangos, símbolos y expresiones regulares.
Números Enteros y de Punto Flotante Los enteros Ruby son objetos de la clase Fixnum o Bignum. Los objetos Fixnum contienen enteros que caben dentro de la palabra nativa de la máquina menos 1 bit. Sin embargo, si un Fixnum supera este rango, se convierte automáticamente en un objeto Bignum, cuyo rango está limitado sólo por la memoria disponible. Si una operación con un Bignum tiene un resultado con un valor final que se ajusta a un Fixnum , este resultado será devuelto como un Fixnum. Los enteros se escriben con un signo opcional, un indicador de base opcional (0 para octal, 0d para decimal, 0x para hexadecimal o 0b para binario), seguidos de una cadena de dígitos en la base correspondiente. El caracter guión bajo es ignorado en ñla cadena de dígitos. 123456 => 0d123456 => 123_456 => -543 => 0xaabb => 0377 => -0b10_1010 =>
123456 # 123456 # 123456 # -543 # 43707 # 255 # -42 #
212
Fixnum Fixnum Fixnum Fixnum Fixnum Fixnum Fixnum
-
underscore ignored negative number hexadecimal octal binary (negated)
123_456_789_123_456_789 => 123456789123456789 # Bignum Se puede obtener el valor entero correspondiente a un caracter ASCII precediendo éste por un signo de cierre de interogación. Se pueden generar caracteres de control utilizando ?\C-x y ?\cx (la versión control de x es x&0x9f). Se pueden generar meta caracteres (x | 0x80) utilizando ?\M-x. La combinación de meta y control se genera utilizando ?\M-\C-x. Se puede obtener el valor entero de la barra invertida con la secuencia ?\\. ?a => ?\n => ?\C-a => ?\M-a => ?\M-\C-a => ?\C-? =>
97 10 1 225 129 127
# # # # # #
ASCII character code for a newline (0x0a) control a = ?A & 0x9f = 0x01 meta sets bit 7 meta and control a delete character
Un literal numérico con un punto decimal y/o un exponente se convierte en un objeto Float, que corresponde al tipo de dato double de la arquitectura nativa. Se debe seguir el punto decimal con un dígito, ya que 1.e3 trata de invocar el método e3 de la clase Fixnum. A partir de Ruby 1.8 también se debe colocar al menos un dígito antes del punto decimal. 12.34 -> -0.1234e2 -> 1234e-2 ->
12.34 -12.34 12.34
Cadenas Ruby proporciona una serie de mecanismos para la creación de cadenas literales. Cada uno genera objetos de tipo String. Estos mecanismos varían en términos de cómo una cadena está delimitada y que cantidad de sustitución se realiza en el contenido literal. Literales de cadena entre comillas simples (‘cosas’ y %q/cosas/) se sometan a la sustitución mínima. La secuencia \\ se convierte en una sola barra invertida y la forma \’ en una comilla simple. Todas las otras barras invertidas aparecen en la cadena literalmente. ‘hello’ ‘a backslash \’\\\’’ %q/simple string/ %q(nesting (really) works) %q no_blanks_here ;
-> -> -> -> ->
hello a backslash ‘\’ simple string nesting (really) works no_blanks_here
Cadenas entre comillas dobles (“cosas”, %Q/cosas / y %/cosas/) se someten a sustituciones adicionales, que se muestran en la Tabla 6 en la página siguiente. a = 123 “\123mile” “Say \”Hello\”” %Q!”I said ‘nuts’,” I said! %Q{Try #{a + 1}, not #{a - 1}} % “Try #{a + 1}, not #{a - 1}” %{ #{ a = 1; b = 2; a + b } }
-> -> -> -> -> -> ->
Smile Say “Hello” “I said ‘nuts’,” I said Try 124, not 122 Try 124, not 122 Try 124, not 122 3
Las cadenas pueden continuar a través de varias líneas de entrada, en cuyo caso contienen caracteres de nueva línea. También es posible utilizar documentos para expresar literales de cadena larga. Siempre que Ruby analiza la secuencia <
213
ser sangrado o indentado desde el margen izquierdo. Si para especificar el terminador se utiliza una cadena entre comillas, sus reglas de entrecomillado se aplican al documento, de lo contrario, se aplican comillas dobles. print < “Concatenate” Las cadenas se almacenan como secuencias de bytes de 8 bits (para su uso en Japón, la biblioteca jcode soporta un conjunto de operaciones de cadenas escritas con codificación EUC, SJIS o UTF-8. La cadena subyacente, sin embargo, sigue siendo accedida como una serie de bytes., y cada byte puede contener cualquiera de los 256 valores de 8 bits, incluyendo nulos y saltos de línea. Las secuencias de sustitución en la Tabla 6 permiten insertar de forma vonveniente y portable caracteres no imprimibles Cada vez que se utiliza una cadena literal en una asignación o como un parámetro, se crea un nuevo objeto String. 3.times do print ‘hello’.object_id, “ “ end produce: 937140 937110 937080
La documentación para las diferentes clases, entre ellas String, comienza páginas adelante.
Rangos
Fuera del contexto de una expresión condicional, expr..expr y expr...expr construyen objetos Range.
214
La forma de dos puntos es un rango inclusivo y el que tiene tres puntos es un rango que excluye su último elemento. Véase la descripción de la clase Range más adelante para más detalles. Vea también la descripción de las expresiones condicionales para otros usos de los rangos.
Matrices Los literales de la clase Array se crean mediante la colocación de una serie de objetos separados por comas referenciados entre corchetes. Una coma al final se ignora. arr = [ fred, 10, 3.14, “This is a string”, barney(“pebbles”), ] Se pueden construir matrices de cadenas usando la notación w% y %W. La forma en minúscula se obtiene con los elementos sucesivos de la matriz separados por espacios. No se realiza sustitución en las cadenas individuales. La forma en mayúscula también convierte las palabras en una matriz, pero realiza sustituciones normales de cadenas entre comilladas en cada palabra. Se puede poner un espacio ente palabras con una barra invertida. Esta es una forma de entrada delimitada generalizada. arr = %w( fred wilma barney betty great\ gazoo ) arr -> [“fred”, “wilma”, “barney”, “betty”, “great gazoo”] arr = %w( Hey!\tIt is now -#{Time.now}- ) arr -> [“Hey!\\tIt”, “is”, “now”, “-#{Time.now}-”] arr = %W( Hey!\tIt is now -#{Time.now}- ) arr -> [“Hey!\tIt”, “is”, “now”, “-Thu Aug 26 22:37:13 CDT 2004-”]
Hashes Un literal hash se crea mediante la colocación de una lista de pares clave/valor entre llaves, con una coma o la secuencia => entre la clave y el valor. Una coma final se ignora. colors = { “red” => 0xf00, “green” => 0x0f0, “blue” => 0x00f }
Las claves y/o valores de un hash particular no necesitan ser del mismo tipo.
Requisitos para una Clave Hash Las claves hash deben responder al mensajes hash retornando un código hash, y este código hash para una clave determinada no debe cambiar. Las claves utilizadas en los hashes también deben ser comparables utilizando eql?. Si eql? retorna true para dos claves, entonces estas claves también deben tener el mismo código hash. Esto significa que ciertas clases (como Array y Hash) no pueden ser convenientemente utilizadas como claves, debido a que sus valores pueden cambiar en función de su contenido. Si se mantiene una referencia externa a un objeto que se utiliza como una clave, y la utilización de esta referencia altera al objeto, cambiando así su código hash, la búsqueda hash a partir de esa clave puede no funcionar. Dado que las cadenas son las claves más utilizadas, y los contenidos de las cadenas a menudo cambian, Ruby trata las claves cadena de forma especial. Si utiliza un objeto String como clave hash, el hash duplicará la cadena internamente y utilizará la copia como clave. La copia será congelada. Cualquier cambio realizado en la cadena original no afectará al hash. Si usted escribe sus propias clases y utiliza instancias de ellas como claves hash, necesita asegurarse de que (a) los valores hash de los objetos clave no cambian una vez que los objetos han sido creados o (b) recuerda llamar al método Hash#rehash para reindexar el hash cada vez que una clave hash es cambiada.
215
Símbolos Un símbolo en Ruby es un identificador que corresponde a una cadena de caracteres, a menudo un nombre. Se construye el símbolo para un nombre precediéndole por dos puntos, y se puede construir el símbolo para una cadena arbitraria precediendo la cadena literal por dos puntos. Se producen las sustituciones en las cadenas entre comillas dobles. Un nombre o cadena en particular siempre va a generar el mismo símbolo, independientemente de cómo se usa ese nombre en el programa. :Object :my_variable :”Ruby rules” a = “cat” :’catsup’ -> :”#{a}sup” -> :’#{a}sup’ ->
:catsup :catsup :”\#{a}sup”
Otros lenguajes llaman a este proceso internado y a los símbolos átomos.
Expresiones Regulares Las expresiones regulares literales son objetos de tipo RegExp. Se crean de forma explícita mediante una llamada al constructor Regexp.new o implícitamente mediante el uso de las formas literales, /patrón/ y %r{patrón}. La construcción %r es una forma de entrada delimitada generalizada. /pattern/ /pattern/options %r{pattern} %r{pattern}options Regexp.new( ‘pattern’ [ , options ] )
Opciones de las Expresiones Regulares Una expresión regular puede incluir una o más opciones que modifican la forma en que el patrón coincide con cadenas. Si se utilizan literales para crear el objeto RegExp, entonces las opciones son uno o más caracteres colocados inmediatamente después del terminador. Si se utiliza Regexp.new, las opciones son constantes que se utilizan como segundo parámetro del constructor. i Case Insensitive. La comparación de patrón ignora si las letras del patrón y la cadena son mayúsculas o minúsculas. Poner $= para hacer comparaciones case insensitive está ya en obsoleto. o Sustituir una vez. Cualquier sustitución #... en una particular expresión regular literal se llevará a cabo sólo una vez, la primera vez que se evalúa. De otra manera, se llevarán a cabo las sustituciones cada vez que el literal genera un objeto RegExp. m Modo multilínea. Normalmente, “.” Coincide con cualquier carácter excepto con el de nueva línea. Con la opción /m, “.” coincide con cualquier carácter. x Modo extendido. Expresiones regulares complejas pueden ser de dificil lectura. La opción x le permite insertar espacios, nuevas líneas y comentarios en el patrón para que sea más legible. Otro conjunto de opciones permiten configurar la codificación de idioma de la expresión regular. Si no se especifica ninguna de estas opciones, se utiliza la codificación por defecto del intérprete (definido mediante -K o $KCODE). n: no encoding (ASCII) e: EUC s: SJIS u: UTF-8
216
Patrones de Expresión Regular caracteres regulares Todos los caracteres excepto ., |, (, ), [, \, ^, {, +, $, *, y ? coinciden con ellos mismos. Para coincidir con uno de estos caracteres, hay precederles con una barra invertida. ^
Coincide con el comienzo de una línea.
$
Coincide con el final de una línea.
\A
Coincide con el comienzo de una cadena.
\z
Coincide con el final de una cadena.
\Z Coincide con el final de la cadena a menos que la cadena termina con un \n, en cuyo caso coincide justo antes del \n. \b, \B
Coincide con los limites de palabra y no palabra respectivamente.
\G
La posición donde una búsqueda repetitiva anterior se completó (pero sólo en algunas situaciones). Consulte la información adicional justo ahora más adelante.
[caracteres] Una expresión con corchetes coincide con cualquiera de los caracteres de la lista entre los corchetes. Los caracteres ., |, (, ), [, \, ^, {, +, $, *, y ? que tienen un significado especial en otros casos en los patrones, pierden su significado especial entre corchetes. Las secuencias \nnn \xnn, \cx, \C-x, \M-x y \M-\C-x tienen el significado que se muestra en la tabla 6 tres páginas atrás. Las secuencias \d, \D, \s, \S, \w, y \W son abreviaturas de grupos de caracteres, como se muestra en la Tabla 2 en la sección clases caracter. La secuencia [:clase:] coincide con una clase de caracteres POSIX y también se muestra en la Tabla 2 (Tenga en cuenta que los corchetes de apertura y cierre son parte de la clase, por lo que el patrón /[_[:dígito:]]/ coincidiría con un dígito o un guión bajo). La secuencia de c1-c2 representa todos los caracteres entre c1 y c2, inclusives. Caracteres literales ] o - deben aparecer inmediatamente después del corchete de apertura. Un carácter de intercalación (^) inmediatamente después del corchete de apertura niega el sentido de la coincidencia --el patrón coincide con cualquiercarácter que no está en la clase caracter. \d, \s, \w
Abreviaturas para las clases caracter que coinciden con dígitos, espacios en blanco y caracteres de palabra, respectivamente. Estas abreviaturas se resumen en la Tabla 2.
\D, \S, \W
Las formas de negación de \d, \s, y \w, coincidiendo con los caracteres que no son dígitos, espacios en blanco o caracteres de palabra.
. (punto) Apareciendo fuera de los corchetes, coincide con cualquier carácter excepto una nueva línea. (Con la opción /m, coincidiría también con salto de línea). re*
Coincide con cero o más apariciones de re.
re+ Coincide con una o más apariciones de re. re{m,n}
Coincide al menos con “m” y como máximo “n” apariciones de re.
re{m,}
Coincide al menos con “m” apariciones de re.
re{m}
Coincide exactamente con “m” apariciones de re.
re?
Coincide con cero o una ocurrencia de re. Los modificadores *, + y {m, n} son codiciosospor defecto. Se añade un signo de interrogación para que sean mínimos.
217
re1|re2 Coincide con re1 o re2. La barra | tiene baja prioridad. (...) Los paréntesis se utilizan para agrupar expresiones regulares. Por ejemplo, el patrón /abc+/ coincide con una cadena que contiene una a, ab y una o más c. /(abc)+/ coincide con una o más secuencias de abc. Los paréntesis también se utilizan para recoger los resultados de las coincidencias de patrón. Para cada paréntesis de apertura, Ruby almacena el resultado de la coincidencia parcial entre éste y el paréntesis de cierrecorrespondiente como grupos sucesivos. Dentro del mismo patrón, \1 se refiere a la coincidencia del primer grupo, \2 del segundo grupo y así sucesivamente. Fuera del patrón, las variables especiales $1, $2 y sucesivos, tienen el mismo propósito. El ancla \G trabaja con los métodos de comparación repetitiva String#gsub, String#gsub!, String #index y String#scan. En ua comparación repetitiva, representa la posición en la cadena donde se ha dado la última coincidencia cuando termina la iteración. \G apunta inicialmente al comienzo de la cadena (o al caracter referenciado por el segundo parámetro de String#index). “a01b23c45 d56”.scan(/[a-z]\d+/) “a01b23c45 d56”.scan(/\G[a-z]\d+/) “a01b23c45 d56”.scan(/\A[a-z]\d+/)
-> -> ->
[“a01”, “b23”, “c45”, “d56”] [“a01”, “b23”, “c45”] [“a01”]
Sustituciones #{...} Realiza una sustitución de expresión, como con cadenas. Por defecto, se lleva a cabo la sustitución cada vez que se evalua una expresión regular literal. Con la opción /o se realiza sólo la primera vez. \0, \1, \2, ... \9, \&, \`, \’, \+ Sustituye el valor comparado con la enésima subexpresión agrupada. O por toda la coincidencia, o por la pre- o postcoincidencia, o por el grupo más alto.
Extensiones de expresiones regulares Al igual que en Perl y Python, las expresiones regulares en Ruby ofrecen algunas extensiones sobre las expresiones regulares Unix. Todas las extensiones se introducen entre los caractereses (? y ). Los paréntesis que encierran estas extensiones son grupos, pero no generan de vuelta referencias: no establecen los valores de \1 y $1, etc. (?# comentario) Inserta un comentario en el patrón. El contenido se ignora durante la comparación de patrones. (?:re) Convierte re en un grupo sin generar de vuelta referencias. Esto a menudo es útil cuando es necesario agrupar un conjunto de construcciones, pero no se quiere que el grupo establecezca el valor de $1 o lo que sea. En el ejemplo que sigue, los patronescoinciden con una fecha, ya sea con dos puntos o ya sea con espacios entreel mes, el día y el año. La primera forma almacena el carácter separador en $2 y $4, pero el segundo patrón no almacena el separador en una variable externa. date = “12/25/01” date =~ %r{(\d+)(/|:)(\d+)(/|:)(\d+)} [$1,$2,$3,$4,$5] -> [“12”, “/”, “25”, “/”, “01”] date =~ %r{(\d+)(?:/|:)(\d+)(?:/|:)(\d+)} [$1,$2,$3] -> [“12”, “25”, “01”] (?=re) Compara re en esa posición, pero no lo usa (también encantadoramente conocido como zero-width positive lookahead). Esto le permite buacar hacia adelante en el contexto de una comparación sin afectar a $&. En este ejemplo, el método scan coincide palabras seguidas de una coma, pero las comas no se incluyen en el resultado.
218
str = “red, white, and blue” str.scan(/[a-z]+(?=,)/) -> [“red”, “white”] (?!re) Compara si re no coincide en esa posición. No consume la comparación (zero-width positive lookahead). Por ejemplo, /hot(?!dog)(\w+)/ coincide con cualquier palabra que contenga las letras hot y no continúen con dog, retornando el final de la palabra en $1. (?>re) Anida una expresión regular independiente dentro de la primera expresión regular. Esta expresión se ancla en la posición de coincidencia actual. Si usa caracteres, estos no estarán ya disponibles para la expresión regular de primer nivel. Esta construcciónpor lo tanto, inhibe la marcha atrás y puede producir una mejora de rendimiento. Por ejemplo, el patrón /a.*b.*a/ toma un tiempo exponencial cuando se compara con una cadena que contiene una a seguida de una serie de b, pero sin a final. Sin embargo, esto se puede evitar mediante el uso de la expresión regular anidada /a(?>.*b).*a/. De esta forma, la expresión anidada consume toda la cadena de entrada hasta el último carácter b posible. Cuando el chequeo para una a final no ocurre entonces, no hay necesidad de dar marcha atrás, y la coincidencia de patrón falla de inmediato. require ‘benchmark’ include Benchmark str = “a” + (“b” * 5000) bm(8) do |test| test.report(“Normal:”) { str =~ /a.*b.*a/ } test.report(“Nested:”) { str =~ /a(?>.*b).*a/ } end produce: Normal: Nested:
user system total real 1.090000 0.000000 1.090000 ( 1.245818) 0.000000 0.000000 0.000000 ( 0.001033)
(?imx) Se convierte en la correspondiente opcoón. Si se utiliza dentro de un grupo, el efecto se limita a ese grupo. (?-imx) Desactiva la opción i, m o x. (?imx:re) Activa la opción i, m o x para re. (?-imx:re) Desactiva la opción i, m o x para re.
Nombres Los nombres Ruby se utilizan para referirse a constantes, variables, métodos, clases y módulos. El primer carácter de un nombre ayuda a Ruby a distinguir su propósito. Algunos nombres, que figuran en la Tabla 7 en la página siguiente, son palabras reservadas y no se deben utilizar como nombres de método, variable, clase o módulo.
Los nombres para los métodos se describen en la sección correspondiente un poco más adelante.
En las siguientes descripciones, letras minúsculas son los carácteres desde la a hasta la z, así como el guión bajo (_). Letras mayúsculas son los carácteres desde la A hasta la Z y dígitos significa desde el 0 hasta el 9. Un nombre es una letra mayúscula, minúscula o un guión bajo, seguido por carácteres de nombre: cualquier combinación de letras mayúsculas, minúsculas, guiones bajos y dígitos. Un nombre de variable local se compone de una letra minúscula seguida de caracteres de nombre. Lo normal es utilizar guiones bajos en lugar de camelCase (mayúsculas y minúsculas mezcladas) para escribir nombres con varias palabras, pero no es obligatorio.
219
fred anObject _x three_two_one Un nombre de variable de instancia comienza con una “arroba” (@) seguida de un nombre. Generalmente, es una buena idea usar una letra minúscula después de la @. @name @_ @size Un nombre de clase comienza con dos arrobas (@@) seguidas de un nombre. Un nombre de constante comienza con una letra mayúscula seguida de caracteres de nombre. Los nombres de clases y los nombres de módulos son constantes y siguen las convenciones de nombres de constante. Por convención, las referencias a objetos constantes normalmente se escriben con letras mayúsculas y guiones bajos, mientras que en los nombres de clase y de módulo se usan mayúsculas y munúsculas.
module Math ALMOST_PI = 22.0/7.0 end class BigBlob end Las variables globales y algunas variables especiales del sistema, comienzan con un signo de dólar ($) seguido por carácteres de nombre. Además, Ruby define un conjunto de variables globales con nombres de dos caracteres los en los que el segundo caracter es un signo de puntuacion. Estas variables predefinidas se enumeran en su sección un poco más adelante. Finalmente, un nombre de variable global se puede formar con $- seguido de una sóla letra o un guión bajo. Estas últimas variables suelen reflejar el establecimiento de la correspondiente opción de línea de comandos. $params $PROGRAM $! $_ $-a $-K
Ambiguedad Variable/Método Cuando Ruby ve un nombre tal como a, en una expresión, es necesario determinar si se trata de una referencia a variable local o una llamada sin parámetros a un método. Para decidir cual es el caso, Ruby utiliza un método heurístico. Cuando Ruby analiza un archivo fuente, realiza un seguimiento de los símbolos que han sido asignados. Se supone que estos símbolos son variables. Cuando posteriormente viene un símbolo que pudiera ser una variable o una llamada a método, comprueba si se ha visto una asignación anterior a este símbolo. Si es así, se trata el símbolo como una variable, de lo contrario lo trata como una llamada a método. Como un caso de esto un tanto patológico, considere el siguiente fragmento de código, presentado por Clemens Hintze. def a print “Function ‘a’ called\n” 99 end for i in 1..2 if i == 2 print “a=”, a, “\n”
220
else a = 1 print “a=”, a, “\n” end end produce: a=1 Function ‘a’ called a=99 Durante el análisis, Ruby ve el uso de a en la primera sentencia print y, como aún no había visto ninguna asignación para a, supone que se trata de una llamada a método. En el momento en que llega a la segunda sentencia print, sin embargo, ha visto una asignaciónn, por lo que la trata como una variable. Tenga en cuenta que la asignación no tiene que ser ejecutada --Ruby sólo tiene que haberla visto. Este programa no generará un error: a = 1 if false; a
Variables y Constantes Las variables y las constantes Ruby contienen referencias a objetos. Las variables en si mismas no tienen un tipo intrínseco. En su lugar, el tipo de una variable se define únicamente por los mensajes a los que responde el objeto referenciado por la variable (cuando decimos que una variable es no tipada, queremos decir que cualquier variable dada puede tener en diferentes momentos, referencias a objetos de diferentes tipos). Una constante Ruby es también una referencia a un objeto. Las constantes se crean cuando se asignan por primera vez (normalmente en una definición de clase o módulo). Ruby, a diferencia de lenguajes menos flexible, permite modificar el valor de una constante, aunque genera un mensaje de advertencia. MY_CONST = 1 MY_CONST = 2
# generates a warning
produce: prog.rb:2: warning: already initialized constant MY_CONST Tenga en cuenta que pese a que las constantes no se deben cambiar, se puede alterar el estado interno de los objetos a los que hacen referencia. MY_CONST = “Tim” MY_CONST[0] = “J” MY_CONST -> “Jim”
# alter string referenced by constant
Potencialemnte se pueden asignar alias a objetos, dando al mismo objeto diferentes nombres.
Ambito de constantes y Variables Las constantes definidas dentro de una clase o módulo pueden ser accedidas sin adornos en cualquier lugar dentro de la clase o módulo. Fuera de la clase o módulo, se pueden acceder con el operador de ámbito :: prefijado por una expresión que retorna el objeto clase o módulo apropiado. Las constantes definidas fuera de cualquier clase o módulo se puede acceder sin adornos, o utilizando el operador de ámbito :: sin prefijo. No se pueden definir constantes en los métodos. Las constantes pueden ser añadidos desde el exterior a las clases y módulos existentes, utilizando el nombre de la clase o el módulo y el operador de ámbito antes del nombre de constante.
221
OUTER_CONST = 99 class Const def get_const CONST end CONST = OUTER_CONST + 1 end Const.new.get_const -> 100 Const::CONST -> 100 ::OUTER_CONST -> 99 Const::NEW_CONST = 123 Las variables globales están disponibles a través del programa. Toda referencia a un nombre global en particular devuelve el mismo objeto. La referencia a una variable global no inicializada devuelve nil. Las variables de clase están disponibles a través de una clase o cuerpo de módulo. Las variables de clase deben inicializarse antes de ser utlizadas. Una variable de clase es compartida por todas las instancias de una clase y está disponible dentro de la propia clase. class Song @@count = 0 def initialize @@count += 1 end def Song.get_count @@count end end Las variables de clase pertenecen a la clase o módulo más interior en la que están. Las variables de clase utilizadas en el nivel superior se definen en Object y se comportan como variables globales. Las variables de clase definida dentro de los métodos singleton pertenecen al nivel superior (aunque este uso está obsoleto y genera una advertencia). En Ruby 1.9, las variables de clase serán de carácter privado a la clase que la define. class Holder @@var = 99 def Holder.var=(val) @@var = val end def var @@var end end @@var = “top level variable” a = Holder.new a.var
->
99
Holder.var = 123 a.var -> 123 # This references the top-levelobject def a.get_var @@var end a.get_var -> “top level variable”
Las variables de clase son compartidas para los hijos de la clase en que se definieron por primera vez.
222
class Top @@A = 1 def dump puts values end def values “#{self.class.name}: A = #@@A” end end class MiddleOne < Top @@B = 2 def values super + “, B = #@@B” end end class MiddleTwo < Top @@B = 3 def values super + “, B = #@@B” end end class BottomOne < MiddleOne; end class BottomTwo < MiddleTwo; end Top.new.dump MiddleOne.new.dump MiddleTwo.new.dump BottomOne.new.dump BottomTwo.new.dump produce: Top: A = 1 MiddleOne: MiddleTwo: BottomOne: BottomTwo:
A A A A
= = = =
1, 1, 1, 1,
B B B B
= = = =
2 3 2 3
Las variables de instancia están disponibles dentro de los métodos de instancia a través del cuerpo de la clase. La referencia a una variable de instancia sin inicializar retorna nil. Cada instancia de una clase tiene un conjunto único de variables de instancia. Las variables de instancia no están disponibles para los métodos de clase (aunque las clases en si mismas puedan tener variables de instancia --se verá más adelante). Las variables locales son únicas en sus ámbitos que son determinados estáticamente, aunque su existencia se haya establecido de forma dinámica. Una variable local se crea dinámicamente la primera vez que se le asigna un valor durante la ejecución del programa. Sin embargo, el alcance de una variable local es estáticamente determinado para el bloque más inmediato en el que está englobada, ya sea definición de método, definición de clase, definición de módulo o un programa de nivel superior. Referenciar una variable local para el mismo ámbito pero que aún no se ha creado aún, genera una excepción NameError. Las variables locales con el mismo nombre son variables diferentes si aparecen en ámbitos disjuntos. Los parámetros de método se consideran variables locales a ese método.
A los parámetros de bloque se les asignan valores cuando se invoca al bloque.
a = [ 1, 2, 3 ] a.each {|i| puts i } a.each {|$i| puts $i }
# i local to block # assigns to global $i
223
a.each {|@i| puts @i } a.each {|I| puts I } a.each {|b.meth| } sum = 0 var = nil a.each {|var| sum += var }
# assigns to instance variable @i # generates warning assigning to constant # invokes meth= in object b # uses sum and var from enclosing scope
Si una variable local (incluido un parámetro de bloque) es asignada primero en un bloque, es local al bloque. Si una variable del mismo nombre ya está establecida en el momento en el bloque se ejecuta, el bloque hereda esa variable. Un bloque toma el conjunto de variables locales que existen en el momento en que se crea. Esto forma parte de su ligadura. Tenga en cuenta que aunque la unión de las variables se fija en este punto, el bloque tiene acceso a los valores en curso de estas variables cuando se ejecuta. La unión preserva estas variables, aunque el ámbito englobado original se destruye. Los cuerpos while, until y los bucles for son parte del ámbito que los contiene; los locales ya existentes se pueden utilizar en el bucle y cualquier otro nuevo local creado estará disponible después fuera de los cuerpos.
Variables Predefinidas Las variables a continuación están predefinidas en el intérprete de Ruby. En las siguientes descripciones, la notación [r/o] indica que las variables son de sólo lectura. Se produce un error si un programa intenta modificar una variable de sólo lectura. Después de todo, probablemente no se desee cambiar el sentido de true a mitad de un programa (excepto, quizás, si se es un político). Las entradas marcadas [hilo] son subprocesos locales. Muchas variables globales son palabrejas como $_, $!, $&, etc. Esto es por razones “históricas”, ya que la mayoría de estos nombres de variables vienen de Perl. Si usted encuentra difícil memorizar toda esta puntuacion, es posible que desee echar un vistazo al archivo de biblioteca llamado English, documentado máss adelante, que da a las variables de uso global más comunes nombres más descriptivos. En las tablas de variables y constantes que siguen, se muestra el nombre de la variable, el tipo del objeto referenciado y una descripción.
Información de excepción $!
Excepción
El objeto de excepción pasó a raise. [hilo]
$@ Array El trazado de pila generado por la última excepción. Ver Kernel#caller más adelante para más detalles. [hilo]
Variables de Coincidencia de Patrones
Estas variables (excepto $=) se ponen a cero después de una comparación de patrón sin éxito.
$& String La cadena coincidente (siguiendo a una comparación de patrón con éxito). Esta variable es local en el ámbito actual. [r/o, hilo] $+ String El contenido del grupo numerado más alto coincidente tras un comparación de patrón con éxito. Así, en “cat” =~/(c|a)(t|z)/, $+ se establece a “t”. Esta variable es local en el ámbito actual. [r/o, hilo] $` String La cadena previa a una comparación de patrón con éxito. Esta variable es local en el ámbito actual. [r/o, hilo] $’ String La cadena siguiente a una comparación de patrón con éxito. Esta variable es local en el ámbito actual. [r/o, hilo]
224
$= Object Obsoleto. Si se establece en cualquier valor, aparte de nil o false, todas las comparaciones de patrón serán case insensitive, en las comparaciones con cadenas y los valores hash de cadena se ignorarán mayúsculas y minúsculas. $1 to $9 String El contenido de los sucesivos grupos coincidentes en una comparación de patrón con éxito. En “cat” =~/(c|a)(t|z)/, $1 se ajustará a “a” y $2 a “t”. Esta variable es local en el ámbito actual. [r/o, hilo] $~ Datos coincidentes Un objeto que encapsula los resultados de una comparación de patrón con éxito. Las variables $&, $`, $’, y $1 a $9 se derivan de $~. La asignación de $~ cambia los valores de estas variables derivadas. Esta variable es local en el ámbito actual. [hilo]
Variables de Entrada/Salida $/ String El separador de registros de entrada (salto de línea por defecto). Este es el valor que las rutinas como Kernel#gets utilizan para determinar los límites de registro. Si se establece a nil, gets va a leer todo el archivo. $-0
String
Sinónimo de $/.
$, String El separador de cadena de salida entre los parámetros a los métodos tales como Kernel#print y Array#join. Por defecto a nil que no añade el texto. $.
Fixnum El número de la última línea a leer del archivo de entrada actual.
$; String
El separador de patrón predeterminado utilizado por String#split. Se puede configurardesde la línea de comandos utilizando la opción -F.
$< Object Un objeto que permite el acceso a la concatenación de los contenidos de todos los archivosdados como argumentos de línea de comandos o $stdin (en el caso de que no haya argumentos). $< soporta los métodos similares a un objeto File: binmode, close, closed?, each, each_byte, each_line, eof, eof?, file, filename , fileno, getc, gets, lineno, lineno=, path, pos, pos=, read , readchar, readline, readlines, rewind, seek, skip, tell, to_a, to_i, to_io, to_s, junto con los métodos de Enumerable. El método de fichero retorna un objeto File para el archivo que se está leyendo. [r/o] $> IO El destino de salida para Kernel#print y Kernel#printf. El valor predeterminado es $stdout. $_ String La última línea leída por Kernel#gets o Kernel#readline. Muchas de las funciones relacionadas con cadenas en el módulo kernel operan en $_ por defecto. La variable es local en el ámbito actual. [hilo] $defout
IO
Sinónimo de $>. Obsoleto: utilizar $stdout.
$deferr IO Sinónimo de STDERR. Obsoleto: utilizar $stderr. $-F
String
Sinónimo de $;.
$stderr
IO
La salida de error estándar en curso.
$stdin
IO
La entrada estándar en curso.
$stdout IO La salida estándar actual. La asignación a $stdout está obsoleta: utilizar $stdout.reopen en su lugar.
225
Variables de entorno de ejecución $0 String El nombre del programa de nivel superior Ruby que se está ejecutando. Normalmente, este será el nombre del archivo del programa. En algunos sistemas operativos, la asignación a esta variable va a cambiar el nombre del proceso reportado (por ejemplo ) por el comando ps(1). $* Array Una matriz de cadenas que contiene las opciones de línea de comandos de la invocacióndel programa. Las opciones utilizadas por el intérprete Ruby se han eliminado . [r/o] Una matriz que contiene los nombres de los módulos cargados por require. [r/o]
$”
Array
$$
Fixnum El número de proceso del programa que se ejecuta. [r/o]
$?
Process::Status El estado de salida del último subproceso al terminar. [r/o, hilo]
$: Array Una matriz de cadenas, donde cada cadena especifica un directorio para buscar scripts de Ruby y extensiones binarias utilizados por los métodos load y require. El valor inicial es el valor de los argumentos pasados a través de la opción -I de línea de comandos, seguido de una definición de localización de instalación de la biblioteca estándar, seguido por el directorio actual (“.”). Esta variable se puede establecer dentro de un programa para modificar la ruta de búsqueda por defecto. Por lo general, los programas utilizan $: << dir para añadir dir a la ruta. [r/o] $-a
Object True si se especifica la opción -a en la línea de comandos. [r/o]
$-d
Object Sinónimo de $DEBUG.
$DEBUG Object Establecido a true si se especifica la opción -d de línea de comandos. __FILE__ String
El nombre del fichero fuente actual. [r/o]
$F Array La matriz que recibe la línea de entrada dividida si se utiliza la opción -a de línea de comandos. $FILENAME String El nombre del fichero de entrada actual. Equivalente a $<.filename. [r/o] $-i String $-I Array
Si el modo edición está activado (tal vez usando la opción -i de línea de comandos), $-i tiene la extensión utilizada al crear el archivo de copia de seguridad. Si establece un valor en $-i, activa el modo de edición. Sinónimo de $:. [r/o]
$-K String Establece el sistema de codificación multibyte para las cadenas y expresiones regulares . Equivalente a la opción -K de línea de comandos. $-l Object Establece a true si la opción -l (que permite el procesamiento de fin de línea) está presente en la línea de comandos. __LINE__ String
El número de línea actual en el fichero fuente. [r/o]
$LOAD_PATH Array
Sinónimo de $:. [r/o]
$-p Object Establece a true si la opción -p (que pone un bucle while gets ... end implícitoen torno a su programa) está presente en la línea de comandos. [r/o] $SAFE Fixnum El nivel de seguridad actual. El valor de esta variable nunca puede ser reducido por asignación. [hilo]
226
$VERBOSE Object Se establece en true si la opciónes -v, --version, -W o -w se especifican en la línea de comandos. Se establece en false si no hay opción o se da -W1. Se establece a nil si se ha especificado -W0. El establecer esta opción en true hace que el intérprete y algunas rutinas de biblioteca para proporcionen información adicional. Ajuste a nil suprime todas las advertencias (incluyendo la salida de Kernel.warn). $-v Object Sinónimo de $VERBOSE. $-w Object Sinónimo de $VERBOSE.
Objetos Estándar ARGF
Object Sinónimo de $<.
ARGV Array Sinónimo de $*. ENV Object Un objeto hash que contiene las variables de entorno del programa. Una instancia de la clase Object, ENV implementa el conjunto completo de métodos Hash. Se utiliza para consultar y establecer el valor de una variable de entorno, como en ENV[“PATH”] y ENV[“term”]=“ansi”. false
FalseClass Instancia singleton de la clase FalseClass. [r/o]
nil NilClass Instancia singleton de la clase NilClass. El valor de las instancias y las variables globales no inicializadas. [r/o] self
Object
El receptor (objeto) del método actual. [r/o]
true
TrueClass Instancia singleton del la clase TrueClass. [r/o]
Constantes Globales
Las siguientes constantes están definidas por el intérprete Ruby.
DATA IO Si el el programa principal contiene la directiva __END__, la constante DATA se iniciará de manera que la lectura de la misma retornará la líneas siguientes de __END__ del archivo de fuente. FALSE
FalseClass Sinónimo de false.
NIL NilClass Sinónimo de nil. RUBY_PLATFORM String El identificador de la plataforma de ejecución del programa. Esta cadena está en la misma forma que el identificador de la plataforma utilizada por la utilidad de configuración de GNU (no es una coincidencia). RUBY_RELEASE_DATE String La fecha de la liberación actual. RUBY_VERSION String El número de versión del intérprete. STDERR
IO
El flujo de error estándar actual del programa. El valor inicial para $stderr.
STDIN
IO
El flujo de entrada estándar actual del programa. El valor inicial para $stdin.
STDOUT
IO
El flujo de salida estándar actual del programa. El valor inicial para $stdout. 227
SCRIPT_LINES__ Hash Si se define una constante SCRIPT_LINES__ y hace referencia a un Hash, Ruby va a almacenar una entrada que contenga el contenido de cada archivo que analiza, con el nombre del archivo como clave y una matriz de cadenas como el valor. Vea Kernel.require más adelante para un ejemplo. TOPLEVEL_BINDING Binding Un objeto Binding que representa la unión en el nivel superior de Ruby --el nivel en el que los programas son ejecutados inicialmente. TRUE
TrueClass Sinónimo de true.
La constante __FILE__ y la variable $0 se utilizan a menudo en conjunto para ejecutar código sólo si aparece en el archivo ejecutado directamente por el usuario. Por ejemplo, los escritores de librería suelen utilizar esto para incluir pruebas en las bibliotecas que se ejecutarán si la fuente de la biblioteca se ejecuta directamente, pero no si la fuente es necesaria en otro programa. # library code # ... if __FILE__ == $0 # tests... end
Expresiones
Cualquiera de los siguientes pueden ser términos simples en una expresión:
• Literal. Lliterales de ruby son números, cadenas, arrays, hashes, rangos, símbolos y expresiones regulares. • El comando Shell. Un comando shell es una cadena encerrada entre apóstrofes o en un delimitador de cadena general que comienza con %x. El valor de la cadena es la salida estándar de ejecución del comando representado por la cadena del shell estándar del sistema operativo anfitrión. La ejecución también establece la variable $? con el estado de salida del comando. filter = “*.c” files = `ls #{filter}` files = %x{ls #{filter}} • Generador de símbolos. Un objeto Symbol se crea anteponiendole un nombre de operador, cadena, variable, constante, clase o módulo y dos puntos. El objeto símbolo es único para cada diferente nombre, pero no hará referncia a una particular instancia del nombre, sino que el símbolo (por ejemplo) :fred será el mismo independientemente del contexto. Un símbolo es similar al concepto de los átomos en otros lenguajes de alto nivel. • Referencia a Variable o Constante. Se hace referencia a una variable citando su nombre. Dependiendo de su ámbito, se hace referencia a una constante, ya sea citando su nombre o por la cualificación del nombre, utilizando el nombre de la clase o módulo que contiene la constante con el operador de ámbito (::). barney # variable reference APP_NAMR # constant reference Math::PI # qualified constant reference • Invocación a Método. Hay varias formas para invocar métodos. Se describirán un poco más adelante en su correspondiente sección.
228
Expresiones Operador Las expresiones pueden combinarse mediante operadores. La tabla 8 muestran los operadores Ruby por orden de precedencia. Los operadores marcados en la columna Método se implementan como métodos y se pueden obviar.
Más sobre la Asignación El operador de asignación asigna uno o más rvalues (la r significa “right” (derecha), ya que rvalues tienden a aparecer en la parte derecha de la asignación) a uno o más lvalues (valores “left”). Qué significa la asignación depende de cada individual lvalue. Si un valor a la izquierda es un nombre de variable o constante, dicha variable o constante recibe una referencia al correspondiente valor a la derecha.
a = /regexp/ b, c, d = 1, “cat”, [ 3, 4, 5 ] Si el valor a la izquierda es un atributo de objeto, se llamará al método de configuración de atributo correspondiente en el receptor, pasándole como parámetro el valor a la derecha.
229
obj = A.new obj.value = “hello” # equivalent to obj.value=(“hello”) Si el valor de la izquierda es una referencia a una matriz de elementos, Ruby llama al operador de asignación de elementos ([]=) en el receptor, pasándole como parámetros los elementos indexados que aparezcan entre los corchetes seguidos del valor de la derecha. Esto se ilustra en la siguiente tabla: Referencia a Elementos Llamada al Método Actual obj[] = “one” obj[1] = “two” obj[“a”, /^cat/] = “three”
obj.[]=(“one”) obj.[]=(1, “two”) obj.[]=(“a”, /^cat/, “three”)
El valor de una expresión de asignación es el valor a la derecha. Esto es cierto incluso si la asignación es un método de atributo que devuelve algo diferente.
Asignación Paralela Una expresión de asignación puede tener uno o más valores a la izquierda y uno o más valores a la derecha. En esta sección se explica cómo maneja Ruby la asignación con diferentes combinaciones de argumentos. • Si el valor derecho se precede con un asterisco y se implementa to_ary, es reemplazado por los elementos de la matriz, con cada elemento formando su propio valor derecho. • Si la asignación contiene varios valores a la izquierda y un solo valor a la derecha, este se convierte en un un Array y esta matriz se expande en un conjunto de valores a la derecha, tal como se explica en las páginas 55 y 56. • Valores sucesivos a la derecha se asignan a valores a la izquierda. Esta asignación efectivamente sucede en paralelo, de modo que (por ejemplo) a,b = b,a intercambia los valores de a y b.
• Si hay más valores a la izquierda que valores a la derecha, el exceso será asignado a nil.
• Si hay más valores a la derecha que a la izquierda, el exceso será ignorado.
• Estas reglas se modifican ligeramente, si el último valor a la izquierda está precedido por un asterisco. Este valor siempre recibirá una matriz durante la asignación. La matriz consistirá de cualquier valor a la derecha que normalmente habría sido asignado a este valor izquierdo, seguido por el exceso de valores a la derecha (si hay). • Si el valor a la izquierda contiene una lista entre paréntesis, la lista es tratada como una sentencia de asignación anidada, y se hace la asignación del valor a la derecha correspondiente como se describe en estas reglas.
Expresiones Bloque begin body end Las expresiones se pueden agrupar en un bloque begin - end. El valor de la expresión de bloque es el valor de la última expresión ejecutada. Las expresiones bloque también juegan un papel importante en el manejo de excepciones, que se discutirá un poco más adelante.
230
Expresiones Booleanas Ruby predefine los valores globales false y nil. Ambos son tratados como si fueran falso en un contexto booleano. Todos los demás valores son tratados como true. La constante true está disponible para cuando se necesita un valor “verdadero” explícito.
And, Or, Not, y Defined? Los operadores and y && evaluan su primer operando. Si es falso, la expresión devuelve el valor del primer operando, de lo contrario, la expresión devuelve el valor del segundo operando. expr1 and expr2 expr1 && expr2 Los operadores or y || evaluan su primer operando. Si es verdadero, la expresión devuelve el valor de su primer operando, de lo contrario, la expresión devuelve el valor del segundo operando. expr1 or expr2 expr1 || expr2 Los operadores not y ! evaluan su operando. Si es verdadero, la expresión devuelve false. Si es falso, la expresión devuelve true. Por razones históricas, una cadena, expresion regular o rango no puede aparecer como el único argumento para not o !. La formas de palabra de estos operadores (AND, OR y NOT) tienen una menor prioridad que las formas símbolo correspondiente (&&, || y !). Véase la Tabla 8 dos páginas atrás para más detalles. El operador defined? devuelve nil si su argumento, que puede ser una expresión arbitraria, no está definido. De lo contrario, devuelve una descripción de ese argumento.
Operadores de Comparación La sintaxis de Ruby define los operadores de comparación ==, ===, <=>, <, <=, >, >= y =~. Todos estos operadores se implementan como métodos. Por convención, el lenguaje también utiliza los métodos estándar eql? y equal?. Aunque los operadores tienen un significado intuitivo, le corresponde a las clases que los implementan el producir una semántica de comparación significativa. La referencia de biblioteca más adelante describe la semántica de comparación de las clases integradas. El módulo Comparable proporciona soporte para la implementación de los operadores ==, <, <=, >, >= y el método between? en términos de <=>. El operador === se utiliza en expresiones case, que se describen un poco más adelante. Los operadores == y =~ tienen las formas negadas != y !~. Ruby los convierte durante el análisis sintáctico: a != b es mapeado a !(a == b), y a !~ b es mapeado a !(a =~ b). No hay métodos para != y !~.
Rangos en las Expresiones Booleanas if expr1 .. expr2 while expr1 ... expr2 Un rango utilizado en una expresión lógica actúa como un flip-flop. Tiene dos estados, establecido y no establecido y está no establecido inicialmente. En cada llamada, el rango ejecuta una transición en la máquina de estados como se muestra en la Figura 16 en la página siguiente. La expresión rango retorna true si la máquina de estados está en estado establecido al final de la llamada, y en caso contrario, retorna false. La forma de dos puntos de un rango se comporta de forma ligeramente diferente que la forma de tres puntos. La forma de dos puntos hace primero la transición de no establecido a establecido, evalúa de inmediato la condición final y hace la transición en consecuencia. Esto significa que si expr1 y expr2 se evalúan ambas como true en la misma llamada, la forma de dos puntos termina la llamada en el estado no establecido. No obstante, todavía devuelve true para esta llamada.
231
La forma de tres puntos no hace la evaluación de la condición final de inmediato al entrar en el estado establecido.
La diferencia se ilustra con el siguiente código:
a = (11..20).collect {|i| (i%4 == 0)..(i%3 == 0) ? i : nil} a -> [nil, 12, nil, nil, nil, 16, 17, 18, nil, 20] a = (11..20).collect {|i| (i%4 == 0)...(i%3 == 0) ? i : nil} a -> [nil, 12, 13, 14, 15, 16, 17, 18, nil, 20]
Expresiones Regulares en Expresiones Booleanas En las versiones de Ruby anteriores de la 1.8, una expresión regular simple en la expresión booleana se comparaba con el valor en curso de la variable $_. Este comportamiento ahora sólo se admite si la condición aparece en el parámetro de línea de comandos -e. En código regular, el uso de $_ y operandos implícitos está siendo eliminado poco a poco, por lo que es mejor usar una comparación explícita con una una variable. Si es necesaria una comparación con $_, utilice if ~/re/ ... o if $_ =~ /re/ ...
Expresiones if y unless if boolean-expression [ then | : ] body [ elsif boolean-expression [ then | : ] body , ... ] [ else body ] end unless boolean-expression [ then | : ] body [ else body ] end La palabra clave then (o el signo :) separa el cuerpo de la condición. No es necesario si el cuerpo comienza en una nueva línea. El valor de una expresión if o unless es el valor de la última expresión evaluada en la ejecución del cuerpo.
Modificadores if y unless expression if boolean-expression expression unless boolean-expression
232
Evalúa la expresión booleana sólo si la expresión es verdadera (para if) o falsa (para unless).
Operador Ternario boolean-expression ? expr1 : expr2 retorna expr1 si la expresión booleana es verdadera y expr2 de lo contrario.
Expresión case Ruby tiene dos formas de declaración case. La primera permite una serie de condiciones a evaluar, ejecutando de código correspondiente a la primera condición si esta es verdadera. case when condition [, condition ]... [ then | : ] body when condition [, condition ]... [ then | : ] body ... [ else body ] end La segunda forma de una expresión case toma una expresión de destino después de la palabra clave case. Se busca una coincidencia, comenzando en la primera comparación (arriba a la izquierda), realizando:
comparison === destino.
case destino when comparison [, comparison ]... [ then | : ] body when comparison [, comparison ]... [ then | : ] body ... [ else body ] end La comparación puede ser una referencia a matriz precedida por un asterisco, en cuyo caso se expande en los elementos de dicha matriz antes de que se realicen las pruebas en cada uno. Cuando la comparación devuelve true, se detiene la búsqueda y se realiza el cuerpo asociado con la comparación (no es necesario un break). case devuelve entonces el valor de la última expresión ejecutada. Si no hay coincidencia con la comparación: si una cláusula else está presente, se ejecuta su cuerpo, de lo contrario, silenciosamente case retorna nil. La palabra clave then (o el signo :) separa las comparaciones cuando (when comparison)de los cuerpos y no es necesario si el cuerpo comienza en una nueva línea.
Bucles while boolean-expression [ do | : ] body end
Ejecuta el cuerpo, cero o más veces, siempre y cuando la expresión booleana es verdadera.
until boolean-expression [ do | : ] body end
233
Ejecuta el cuerpo, cero o más veces, siempre y cuando la expresión booleana es falsa.
En ambas formas, el do o dos puntos separan la expresión booleana del cuerpo y se pueden omitir cuando el cuerpo empieza en una línea nueva. for name [, name ]... in expression [ do | : ] body end El bucle for se ejecuta como si fuera el siguiente bucle each, excepto que las variables locales definidas en el cuerpo del bucle estarán disponible fuera del mismo, mientras que no lo estarán para las definidas dentro de un bloque iterador. expression.each do | name [, name ]... | body end loop, que itera su bloque asociado, no es una construcción del lenguaje --es un método en el módulo Kernel. loop do print “Input: “ break unless line = gets process(line) end
Modificadores while y until expression while boolean-expression expression until boolean-expression Si la expresión es otra cosa que un bloque begin/end, se ejecuta la expresión cero o más veces mientras que la expresión booleana es verdadera (para while) o falsa (para until).
Si la expresión es un bloque begin/end, el bloque se ejecutará siempre al menos una vez.
break, redo, next y retry break, redo, next, y retry alteran el flujo normal a través de un while, until, for o un iterador que controla el bucle. break termina el bucle inmediatamente englobado --reanuda el control en la siguiente declaración del bloque. redo repite el bucle desde el principio, pero sin volver a evaluar la condición o ir al siguiente elemento (en un iterador). La palabra clave next salta al final del bucle, comienza efectivamente en la siguiente iteración. retry reinicia el bucle, reevaluando la condición. Opcionalmente, break y next pueden tener uno o más argumentos. Si se utiliza dentro de un bloque, el o los argumentos dados se retornan como el valor de producción. Si se usa dentro de un bloque while, until o for, el valor dado a se retorna el valor dado al break como el valor de la declaración, y el valor dado al next se ignora silenciosamente. Si no se llama nunca al break, o si se le llama sin ningún valor, el bucle retorna nil. match = while line = gets next if line =~ /^#/ break line if line =~ /ruby/ end match = for line in ARGF.readlines next if line =~ /^#/ break line if line =~ /ruby/ end 234
Definición de Método def defname [ ( [ arg [ =val ], ... ] [ , *vararg ] [ , &blockarg ] ) ] body end
defname es tanto el nombre del método como opcionalmente, el contexto en el que es válido.
Un nombre-método es otro operador redefinible (véase la tabla 8) o un nombre. Si nombre-método es un nombre, debe comenzar con una letra minúscula (o guión bajo), seguido opcionalmente por letras mayúsculas y minúsculas, guiones bajos y dígitos. Opcionalmente un nombre-método puede terminar con un signo de interrogación (?), signo de exclamación (!), o signo igual (=). El signo de interrogación y de exclamación son simplemente parte del nombre. El signo de igualdad es también parte del nombre, pero, además, señala que este método puede ser utilizado como un valor a la izquierda que tiene un correspondiente valor asignado a la derecha. Una definición de método utilizando un nombre sin adornos dentro de una definición de clase o módulo crea un método de instancia, que sólo puede ser invocado mediante el envío de este nombre a un receptor que es una instancia de la clase que lo define (o una de las subclases de esa clase). Fuera de una definición de clase o módulo, una definición con un nombre de método sin adornos, se añade como un método privado a la clase Object, y por lo tanto puede ser llamado en cualquier contexto sin un receptor explícito. Una definición que utiliza un nombre de método de la forma constante.nombremetodo o la más general (expr).nombremetodo crea un método asociado con el objeto que es el valor de la constante o de la expresión. A este método sólo se le podrá llamar suministrandole como receptor el objeto referenciado por la expresión. Este es el estilo de definición creada por objeto o método singleton. class MyClass def MyClass.method end end MyClass.method obj = Object.new def obj.method end obj.method def (1.class).fred end Fixnum.fred
# definición # llamada # definición # llamada # el receptor puede ser una expresión # llamada
Las definiciones de métodos no pueden contener definiciones de clase o módulo. Pueden contener definiciones de instancia anidada o de métodos singleton. Se define un método interno cuando se ejecuta el método que lo contiene. El método interno no actúa como un cierre en el contexto del método anidado --es autónomo. def toggle def toggle “subsequent times” end “first time” end toggle toggle toggle
-> -> ->
“first time” “subsequent times” “subsequent times”
El cuerpo de un método actúa como si se tratara de un bloque de begin/end, ya que puede contener declaraciones de manejo de excepción (rescue, else y ensure).
235
Argumentos de Método Una definición de método puede tener cero o más argumentos regulares, un argumento matriz opcional y un argumento bloque opcional. Los argumentos están separados por comas y se puede encerrar entre paréntesis una lista de argumentos. Un argumento regular es un nombre de variable local opcionalmente seguido por un signo igual y una expresión que da un valor por defecto. La expresión se evalúa en el momento que se llama al método. Las expresiones se evalúan de izquierda a derecha y una expresión puede hacer referencia a un parámetro que le precede en la lista de argumentos. def options(a=99, [ a, b ] end options -> options 1 -> options 2, 4 ->
b=a+1) [99, 100] [1, 2] [2, 4]
El argumento matriz opcional debe seguir a cualquier argumento regular y no pueden haber uno predeterminado. Cuando se invoca al método, Ruby establece el argumento matriz para que referencie a un nuevo objeto de la clase Array. Si la llamada al método especifica algún parámetro de más en la cuenta de argumentos regulares, todos estos parámetros adicionales se recogen en esta matriz recién creada. def varargs(a, *b) [ a, b ] end varargs 1 -> varargs 1, 2 -> varargs 1, 2, 3 ->
[1, []] [1, [2]] [1, [2, 3]]
Si un argumento matriz sigue a los argumentos con valores por defecto, los primeros parámetros que se pasen se utilizarán para reemplazar a los valores por defecto. El resto será utilizado para rellenar la matriz. def mixed(a, b=99, *c) [ a, b, c] end mixed 1 -> [1, mixed 1, 2 -> [1, mixed 1, 2, 3 -> [1, mixed 1, 2, 3, 4 -> [1,
99, []] 2, []] 2, [3]] 2, [3, 4]]
El argumento bloque opcional debe ser el última en la lista. Cada vez que se llama a un método, Ruby busca un bloque asociado. Si hay un bloque, se convierte en un objeto de la clase Proc y se asigna al argumento bloque. Si no hay ningún bloque, el argumento se establece a nil. def example(&block) puts block.inspect end example example { “a block” } produce: nil #
236
Invocación de un Método [ receiver. ] name [ parameters ] [ block ] [ receiver:: ] name [ parameters ] [ block ] parameters
<--
block <-
( [ param, ... ] [ , hashlist ] [ *array ] [ &a_proc ] ) { blockbody } do blockbody end
Se asignan parámetros iniciales a los argumentos reales del método. Lo siguiente a estos parámetros puede ser una lista de pares clave => valor. Estos pares se recogen en un único objeto Hash nuevo y se pasan como un parámetro único. Y siguiendo a estos parámetros puede haber un parámetro único precedido de un asterisco. Si este parámetro es una matriz, Ruby lo reemplaza con cero o más parámetros correspondientes a los elementos de la matriz. def regular(a, b, *c) # ... end regular 1, 2, 3, 4 regular(1, 2, 3, 4) regular(1, *[2, 3, 4]) Un bloque puede estar asociado con una llamada al método utilizando ya sea un bloque literal (que debe comenzar en la misma línea fuente que la última línea de la llamada al método), o ya sea un parámetro que contiene una referencia a un objeto Proc o Method prefijado con un signo &. Independientemente de la presencia de un argumento bloque, Ruby se encarga del valor de la función global Kernel.block_given? para reflejar la disponibilidad de un bloque asociado con la llamada. a_proc = lambda { 99 } an_array = [ 98, 97, 96 ] def block yield end block { } block do end block(&a_proc) def all(a, b, c, *d, &e) puts “a = #{a.inspect}” puts “b = #{b.inspect}” puts “c = #{c.inspect}” puts “d = #{d.inspect}” puts “block = #{yield(e).inspect}” end all(‘test’, 1 => ‘cat’, 2 => ‘dog’, *an_array, &a_proc) produce: a = “test” b = {1=>”cat”, 2=>”dog”} c = 98 d = [97, 96] block = 99 Se llama a un método pasando su nombre a un receptor. Si no se especifica un recptor, se asume self. El receptor comprueba la definición del método en su propia clase y luego sus clases antecesoras de forma secuencial. Los métodos de instancia de módulos incluidos actúan como si estuvieran en
237
superclases anónimas de la clase que los incluye. Si no se encuentra el método, Ruby invoca al método method_missing en el receptor. El comportamiento predeterminado que se define en Kernel.method_ missing es para informar de un error y terminar el programa. Cuando se especifica un receptor explícitamente en una invocación a método, puede ser separado del nombre del método utilizando un punto “.” o un signo dos puntos doble “::”. La única diferencia entre estas dos formas se produce si el nombre del método comienza con una letra mayúscula. En este caso, Ruby asume que esta llamada a método receptor::Loquesea es realmente un intento de acceder a una constante llamada Loquesea en el receptor a menos que la invocación al método lleve una lista de parámetros entre paréntesis. Foo.Bar() Foo.Bar Foo::Bar() Foo::Bar
# # # #
llamada a método llamada a método llamada a método acceso a constante
El valor de retorno de un método es el valor de la última expresión ejecutada.
return [ expr, ... ] Una expresión return sale inmediatamente de un método. Si se llama sin parámetros, el valor de return es nil. Si se llama con un parámetro es el valor de este parámetro y si se llama con más de un parámetro es una matriz que contiene todos los parámetros.
super super [ ( [ param, ... ] [ *array ] ) ] [ block ] Dentro del cuerpo de un método, una llamada a super actúa igual que una llamada a ese método original, excepto que la búsqueda de un cuerpo de método se inicia en la superclase del objeto que contiene el método original. Si no se pasan parámetros a super (y no hay paréntesis), se pasarán los parámetros del método original, de lo contrario, se pasarán los parámetros de super.
Métodos de Operador expr1 operator operator expr1 expr1 operator expr2 Si el operador en una expresión de operador corresponde a un método redefinible (véase la tabla 8), Ruby ejecutará la expresión de operador como si se hubiera escrito (expr1).operator(expr2)
Asignación de Atributos receptor.nombreattr = rvalue Cuando la forma receptor.nombreattr aparece como un valor a la izquierda, Ruby invoca a un método llamado nombreattr= en el receptor, pasandole el valor a la derecha como un único parámetro. El valor devuelto por esta asignación es siempre el valor derecho --se descarta el valor de retorno del método nombreatrr=. Si desea acceder el valor de retorno (en el improbable caso de que éste no sea el valor derecho), envíe un mensaje explícito al método. class Demo attr_reader :attr def attr=(val) @attr = val “return value”
238
end end d = Demo.new # En todos estos casos, d.attr = 99 -> d.attr=(99) -> d.send(:attr=, 99) -> d.attr ->
@attr es establecido a 99 99 99 “return value” 99
Operador de Referencia a Elemento receptor[ expr [, expr ]... ] receptor[ expr [, expr ]... ] = rvalue Cuando se utiliza como un valor a la derecha, el elemento de referencia invoca el método [] en el receptor, pasando como parámetros las expresiones entre paréntesis. Cuando se utiliza como un valor a la izquierda, el elemento de referencia invoca al método []= en el receptor, pasando como parámetros las expresiones entre paréntesis, seguido por el valor asignado a la derecha.
Alias alias new_name old_name Crea un nombre nuevo que se refiere a un método, operador, variable global o referencia a expresión regular ($&, $`, $’ y $+) ya existentes. Las variables locales, variables de instancia, variables de clase, y las constantes no pueden ser un alias. Los parámetros para alias pueden ser nombres o símbolos. class Fixnum alias plus + end 1.plus(3) -> alias $prematch $` “string” =~ /i/ -> $prematch -> alias :cmd :` cmd “date”
->
4 3 “str” “Thu Aug 26 22:37:16 CDT 2004\n”
Cuando se crea un alias de un método, el nombre nuevo se refiere a una copia del cuerpo del método original. Si se redefine el método posteriormente, el alias aún invoca a la implementación original. def meth “original method” end alias original meth def meth “new and improved” end meth -> “new and improved” original -> “original method”
Definición de Clase class [ scope:: ] classname [ < superexpr ] body end
239
class << obj body end Una definición de clase crea o extiende un objeto de la clase Class mediante la ejecución del código en el cuerpo. En la primera forma, se crea o extiende una clase llamada class. El Objeto Class resultante se asigna a una constante llamada nombreclase (ver más abajo las normas de ambito). Este nombre debe comenzar con una letra mayúscula. En la segunda forma, una clase anónima (singleton) se asocia con el objeto específico. Si está presente, superexpr debería ser una expresión que se evalúa como un objeto Class que será la superclase de la clase que se define. Si se omite, es de la clase Object por defecto. Dentro del cuerpo, la mayoría de las expresiones Ruby se ejecutan como la definición que se lee. Sin embargo:
• Las definiciones de método registrarán los métodos en una tabla en la clase del objeto.
• Las definiciones de clase y módulo anidadas se guardarán en constantes dentro de la clase, pero no sonconstantes globales. Estas clases y módulos anidados se pueden acceder desde fuera de la clase que las define utilizando “::” para calificar sus nombres. module NameSpace class Example CONST = 123 end end obj = NameSpace::Example.new a = NameSpace::Example::CONST • El método Module#include añade los módulos nombrados como superclases anónimas de la clase que se define. En una definición de clase, el nombreclase puede ser precedido por los nombres de las clases o módulos existentes utilizando el operador de ámbito (::). Esta sintaxis inserta la nueva definición en el espacio de nombres del módulo o módulos, clase o clases prefijados, pero no interpreta la definición en el ámbito de estas clases externas. Un nombreclase con un operador inicial de ámbito coloca a la clase o módulo en el ámbito de nivel superior. En el ejemplo siguiente, la clase C se inserta en el espacio de nombres del módulo A, pero no se interpreta en el contexto de A. Como resultado de ello, la referencia a CONST se resuelve a la constante de nivel superior de ese nombre. También hay que calificar completamente el nombre del método singleton, ya que C por sí sola no es una constante conocida en el contexto de A::C. CONST = “outer” module A CONST = “inner” # This is A::CONST end module A class B def B.get_const CONST end end end A::B.get_const
->
“inner”
class A::C
240
def (A::C).get_const CONST end end A::C.get_const
->
“outer”
Vale la pena destacar que una definición de clase es el código ejecutable. Muchas de las directrices utilizadas en la definición de clase (como attr e include) son en realidad simples métodos privados de instancia de la clase Module (documentada más adelante). El capítulo sobre Clases y Objetos que comienza un poco más adelante, describe con más detalle cómo los objetos Class interactuan con el resto del entorno.
Creación de Objetos de Clases obj = classexpr.new [ ( [ args, ... ] ) ] La clase de Class define el método de instancia Class#new, que crea un objeto de la clase del receptor (classexpr en el ejemplo de sintaxis). Esto se hace llamando al método classexpr.allocate. Se puede obviar este método, pero su implementación debe devolver un objeto de la clase correcta. A continuación se invoca a initialize en el objeto recién creado pasándole a new los argumentos originales. Si una definición de clase anula el método de clase new sin llamar a super, no se pueden crear los objetos de esa clase y las llamadas a new silenciosamente retornaran nil. Al igual que cualquier otro método, initialize debe llamar a super si se quiere garantizar que las clases padres han sido correctamente inicializadas. Esto no es necesario cuando el padre es Object, ya que la clase Object no hace inicialización de una específica instancia.
Declaraciones de Atributos de Clase Las declaraciones de atributos de clase no forman parte de la sintaxis de Ruby: son simplemente métodos definidos en la clase Module que crea métodos accesores de forma automática. class name attr attribute [ , writable ] attr_reader attribute [, attribute ]... attr_writer attribute [, attribute ]... attr_accessor attribute [, attribute ]... end
Definiciones de Módulo module name body end Un módulo es básicamente una clase que no puede ser instanciada. Al igual que una clase, su cuerpo se ejecuta durante la definición y el objeto Module resultante se almacena en una constante. Un módulo puede contener métodos de clase y de instancia y puede definir constantes y variables de clase. Al igual que con las clases, los métodos de módulo se invoca utilizando el objeto Module como un receptor y las constantes son accesibles usando el operador “::” de resolución de ámbito. El nombre de una definición de módulo opcionalmente puede ser precedido por los nombres de la clase o clases y/o módulo o módulos que lo engloban. CONST = “outer” module Mod CONST = 1
241
def Mod.method1 # module method CONST + 1 end end module Mod::Inner def (Mod::Inner).method2 CONST + “ scope” end end Mod::CONST -> 1 Mod.method1 -> 2 Mod::Inner::method2 -> “outer scope”
Mixins --Incluyendo los Módulos class|module name include expr end Un módulo puede ser incluido en la definición de otro módulo o clase utilizando el método include. El módulo o clase que contiene el include gana el acceso a las constantes, variables de clase y los métodos de instancia del módulo que incluye. Si un módulo se incluye dentro de una definición de clase, las constantes del módulo, las variables de clase y los métodos de instancia son efectivamente ligados en un anónima (e inaccesible) superclase de esa clase. Los objetos de la clase responderán a los mensajes enviados a los métodos de instancia del módulo. Las llamadas a métodos no definidos en la clase se pasan al módulo (o módulos) mezclado en la clase, antes de ser enviados a cualquier clase padre. Un módulo puede optar por definir un método initialize , que llamará a la creación de un objeto de una clase mezclada con el módulo si: (a) la clase no define su método initialize propio, o (b) el método initialize de la clase invoca a super. Un módulo puede ser incluido también en el nivel superior, en cuyo caso las constantes, las variables de clase y los métodos de instancia del módulo estarán disponibles en el nivel superior.
Funciones de Módulo A pesar de que include es útil para proporcionar la funcionalidad de mixin, también es una forma de llevar las constantes, variables de clase y los métodos de instancia de un módulo, a otro espacio de nombres. Sin embargo, la funcionalidad definida en un método de instancia no estará disponible como un método de módulo. module Math def sin(x) # end end # La única forma de acceder a Math.sin es... include Math sin(1) El método Module#module_function resuelve este problema tomando uno o más métodos de instancia del módulo y copiando sus definiciones en los correspondientes métodos de módulo. module Math def sin(x) # end module_function :sin end Math.sin(1)
242
include Math sin(1) El método de instancia y el método de módulo son dos métodos diferentes: la definición del método ha sido copiada por module_function, no es un alias.
Control de Acceso
Ruby define tres niveles de protección para las constantes y métodos de clase y de módulo:
• Público. Accesibles a cualquiera.
• Protegido. Sólo pueden ser invocados por los objetos de la clase que define y sus subclases.
• Privado. Sólo se pueden llamar en forma funcional (es decir, con un self implícito como receptor). Los métodos privados por lo tanto, sólo se pueden llamar en la clase que define y por los descendientes directos dentro del mismo objeto. Véase la descripción que comienza en la página 17 para ver ejemplos. private [ symbol, ... ] protected [ symbol, ... ] public [ symbol, ... ]
Cada función se puede utilizar de dos maneras diferentes.
1. Si se utiliza sin argumentos, las tres funciones establecen el control de acceso por defecto de los métodos definidos posteriormente. 2. Con argumentos, las funciones establecen el control de acceso de los métodos y constantes nombrados.
El control de acceso se aplica cuando se invoca un método.
Bloques, Cierres y Objetos Proc Un bloque de código es un conjunto de declaraciones y expresiones Ruby entre llaves o entre un par do/end. El bloque puede comenzar con una lista de argumentos entre barras verticales. Un bloque de código sólo puede aparecer inmediatamente después de una invocación de método. El inicio del bloque (la llave o el do) debe estar en la misma línea lógica que el final de la invocación. invocation do | a1, a2, ... | end invocation { | a1, a2, ... | } Las llaves tienen prioridad alta y el do tiene baja prioridad. Si la invocación del método tiene parámetros que no se están entre paréntesis, la forma de bloque entre llaves se unirá al último parámetro, no a la invocación total. La forma do se unirá a toda la invocación. Dentro del cuerpo del método invocado, se puede llamar al bloque de código con la palabra clave yield. Los parámetros pasados a yield se asignarán a los argumentos en el bloque. Se generará una advertencia si yield pasa varios parámetros a un bloque que tiene sólo uno. El valor de retorno de yield es el valor de la última expresión evaluada en el bloque o el valor pasado a la sentencia next ejecutada en el bloque. Un bloque es un cierre; recuerda el contexto en el que se define y utiliza ese contexto cada vez que se le llama. El contexto incluye el valor de self, las constantes, las variables de clase, las variables locales y de cualquier bloque capturado.
243
class Holder CONST = 100 def call_block a = 101 @a = 102 @@a = 103 yield end end class Creator CONST = 0 def create_block a = 1 @a = 2 @@a = 3 proc do puts “a = #{a}” puts “@a = #@a” puts “@@a = #@@a” puts yield end end end block = Creator.new.create_block { “original” } Holder.new.call_block(&block) produce: a = 1 @a = 2 @@a = 3 original
Objetos Proc, break y next Los bloques de Ruby son trozos de código asociados a un método que operan en el contexto del llamador. Los bloques no son objetos pero pueden ser convertidos en objetos de la clase Proc. Hay tres maneras de convertir un bloque en un objeto Proc. 1. Pasando el bloque a un método cuyo último parámetro se precede con un signo &. Ese parámetro recibe el bloque como un objeto Proc. def meth1(p1, p2, &block) puts block.inspect end meth1(1,2) { “a block” } meth1(3,4) produce: # nil
2. Llamando Proc.new, asociándole de nuevo con un bloque
block = Proc.new { “a block” } block -> # 3. Al llamar al método Kernel.lambda (o el equivalente Kernel.proc) por la asociación de un
244
bloquecon la llamada. block = lambda { “a block” } block -> # Los dos primeros estilos de objeto Proc son idénticos en su uso. Vamos a llamar a estos objetos procs crudos. El tercer estilo, generado por lambda, añade algunas funciones adicionales al objeto Proc, como veremos en un minuto. Llamaremos a estos objetos lambdas. Dentro de cada tipo de bloque, la ejecución de next causa la salida del bloque. El valor del bloque es el valor (o valores) pasados a next o nil si no se pasan valores. def meth res = yield “The block returns #{res}” end meth { next 99 }
->
“The block returns 99”
pr = Proc.new { next 99 } pr.call -> 99 pr = lambda { next 99 } pr.call -> 99 Dentro de un proc crudo, el break termina el método que invocó el bloque. El valor de retorno del método son los parámetros pasados a break.
Bloques y Retorno Un retorno desde el interior de un bloque que aún está en en un ámbito actúa como un retorno de ese ámbito. El retorno de un bloque cuyo contexto original ya no es válido lanza una excepción (LocalJumpError o ThreadError dependiendo del contexto). El siguiente ejemplo ilustra el primer caso. def meth1 (1..10).each do |val| return val end end meth1 -> 1
# returns from method
Este ejemplo muestra un retorno fallido porque el contexto de su bloque ya no existe.
def meth2(&b) b end res = meth2 { return } res.call produce: prog.rb:5: unexpected return (LocalJumpError) from prog.rb:5:in `call’ from prog.rb:6
Y aquí hay un retorno fallido porque el bloque se crea en un hilo y la llamada en otro.
def meth3 yield end
245
t = Thread.new do meth3 { return } end t.join produce: prog.rb:6: return can’t jump across threads (ThreadError) from prog.rb:9:in `join’ from prog.rb:9 La situación de los objetos Proc es un poco más complicada. Si utiliza Proc.new para crear un proc de un bloque, este proc actúa como un bloque y se aplican las reglas anteriores. def meth4 p = Proc.new { return 99 } p.call puts “Never get here” end meth4
->
99
Si el objeto Proc es creado usando Kernel.proc o Kernel.lambda, se comporta más como un cuerpo de método independiente: un return simplemente retorna desde el bloque a la llamada del bloque. def meth5 p = lambda { return 99 } res = p.call “The block returned #{res}” end meth5
->
“The block returned 99”
Debido a esto, si se utiliza Module#define_method, es probable que se le quiera pasar un proc creado con lambda, no con Proc.new, ya que el retorno funcionará como se espera en el primero y generará un LocalJumpError en el segundo.
Excepciones Las excepciones Ruby son objetos de la clase Exception y sus descendientes (la lista completa de las excepciones integradas se dá más adelante).
Levantamiento de Excepciones
El método Kernel.raise lanza una excepción.
raise raise string raise thing [ , string [ stack trace ] ]
La primera forma resube la excepción en $! o una nueva RuntimeError si $! es nil.
La segunda forma crea una nueva excepción RuntimeError, estableciendo su mensaje en la cadena dada. La tercera forma crea un objeto excepción invocando al metodo exception en su primer argumento. A continuación, establece el mensaje de esta excepción y hace la traza de sus argumentos segundo y tercero.
246
La clase Exception y los objetos de esta clase contienen un método de fábrica denominado exception , por lo que el mismo nombre de clase o instancia se puede utilizar como primer parámetro para levantar una excepción. Cuando se lanza una excepción, Ruby coloca una referencia al objeto excepcion en la variable global $!.
Manejo de Excepciones
Las excepciones se pueden manejar:
• en el ámbito de un bloque begin/end,
begin code... code... [ rescue [ parm, ... ] [ => var ] [ then ] error handling code... , ... ] [ else no exception code... ] [ ensure always executed code... ] end
• dentro del cuerpo de un método
def method and args code... code... [ rescue [ parm, ... ] [ => var ] [ then ] error handling code... , ... ] [ else no exception code... ] [ ensure always executed code... ] end
• y después de la ejecución de una sentencia única.
statement [ rescue statement, ... ] Un bloque o método puede tener varias cláusulas de rescate, y cada cláusula de rescate puede especificar cero o más parámetros de excepción. Una cláusula de rescate sin parámetros se trata como si fuera un parámetro de StandardError. Esto significa que algunas excepciones de menor nivel no serán capturadas por una clase rescue sin parámetros. Si se quiere rescatar todas las excepciones hay que utilizar rescue Exception => e Cuando se lanza una excepción, Ruby escanea la pila de llamadas hasta que encuentra un bloque begin/end, un cuerpo de método o la declaración con un modificador de rescate. Para cada cláusula de rescate en ese bloque, Ruby compara la excepción levantada contra cada uno de los parámetros de la cláusula de rescate. Cada parámetro se pone a prueba utilizando parámetro===$!. Si la excepción planteada coincide con un parámetro de rescue, Ruby ejecuta el cuerpo del rescue y deja de buscar. Si una cláusula de rescate encontrada termina con => y un nombre de variable, la variable se establece en $!. Aunque los parámetros de la cláusula de rescate suelen ser los nombres de las clases Exception, en realidad pueden ser expresiones arbitrarias (incluyendo llamadas a métodos) que retornan una clase apropiada.
247
Si no se encuentra cláusula de rescate coincidente con la excepción levantada, Ruby se mueve hacia arriba de la pila buscando un bloque begin/end de nivel superior que coincida. Si una excepción se propaga al nivel superior del hilo principal sin ser rescatada, el programa termina con un mensaje. Si está presente una cláusula else, se ejecuta su cuerpo si no se plantearon excepciones en el código. Las excepciones lanzadas durante la ejecución de la cláusula else no son capturadas por cláusulas de rescate en el mismo bloque del else. Si está presente una cláusula ensure, se ejecuta siempre su cuerpo cuando se abandona el bloque (incluso si hay una excepción no capturada en proceso de propagarse).
Dentro de una cláusula rescue, raise sin parámetros relanza la excepción en $!.
Modificadores de Rescate de Declaración Una declaración puede tener un modificador rescue opcional seguido de otra sentencia (y por extensión un nuevo modificador rescue y así sucesivamente). El modificador rescue no tiene parámetro de excepción y rescata StandardError y sus subprocesos hijos. Si se lanza una excepción a la izquierda de un modificador rescue, se abandona la declaración de la izquierda y el valor de la línea total es el valor de la declaración de la derecha. values = [ “1”, “2.3”, /pattern/ ] result = values.map {|v| Integer(v) rescue Float(v) rescue String(v) } result
->
[1, 2.3, “(?-mix:pattern)”]
Intentando de nuevo un bloque La declaración retry se puede utilizar dentro de una cláusula rescue para reiniciar el bloque begin/end englobado desde el principio.
Agarrar y Tirar
El método Kernel.catch ejecuta su bloque asociado.
catch ( symbol | string ) do block... end
El método Kernel.throw interrumpe el proceso normal de una sentencia.
throw( symbol | string [ , obj ] ) Cuando se ejecuta throw, Ruby busca en la pila de llamadas el primer bloque catch con un símbolo o cadena coincidente. Si se encuentra, la búsqueda se detiene y se reanuda la ejecución más allá del final del bloque de la capturado. Si al throw se le pasó un segundo parámetro, se devuelve ese valor como el valor del catch. Ruby hacer honor a las cláusulas ensure de cualquier bloque de expresiones que atraviesa en la búsqueda de un correspondiente catch. Si no hay ningún bloque catch que coincide con el throw, Ruby lanza una excepción NameError en la ubicación del throw.
Duck Typing Usted habrá notado que en Ruby no se declaran los tipos de variables o de métodos --todo es simplemente un tipo de objeto.
248
Ahora, parece que la gente reacciona a esto de dos maneras. A algunos les gusta este tipo de flexibilidad y se sienten cómodos escribiendo código con variables y métodos de tipados dinámicamente. Si eres uno de estos, es posible que desees pasar a la sección llamada “Clases no son Tipos” en la página siguiente. Algunos sin embargo, se ponen nerviosos cuando piensan en todos los objetos que flotan alrededor sin restricciones. Si usted ha llegado a Ruby a partir de un lenguaje como C# o Java, en los que estamos acostumbrados a dar a todas los métodos y variables un tipo, puede sentir que Ruby es demasiado descuidado para utilizarlo en la escritura “real” de aplicaciones.
No lo es.
Nos gustaría pasar un par de párrafos tratando de convencerle de que la falta de tipos estáticos no es un problema cuando se trata de escribir aplicaciones confiables. No estamos tratando de criticar a otros lenguajes aquí. En su lugar, sólo dar un contraste de enfoques. La realidad es que los sistemas de tipo estático en la mayoría de los principales lenguajes, no ayudan mucho en términos de seguridad del programa. Si el sistema de tipos Java fuera fiable, por ejemplo, no sería necesario implementar ClassCastException. La excepción es necesaria, sin embargo, porque en Java hay incertidumbre de tipo en tiempo de ejecución (como lo hay en C++, C# y otros). El tipado estáticos puede ser bueno para la optimización de código, y puede ayudar a los IDEs a hacer cosas inteligentes con herramientas de información, pero no hemos visto mucha evidencia de que promueva un código más seguro. Por otro lado, una vez que se utiliza Ruby por un tiempo, se cae en cuenta de que el tipado dinámico de las variables en realidad agrega productividad de muchas maneras. También se sorprende uno al descubrir que sus temores sobre el caos de tipos eran infundados. Grandes y de larga duración, los programas Ruby ejecutan aplicaciones importantes y simplemente no arrojan ningún tipo de error relacionado. ¿Por qué es esto? En parte, es una cuestión de sentido común. Si se ha codificado en Java (Java pre 1.5), todos los contenedores efectivamente no eran tipados: todo en un contenedor era sólo un Object, y se conviertía al tipo necesario en la extracción de un elemento. Y sin embargo, no se mostraba un ClassCastException al ejecutar estos programas. La estructura del código simplemente no lo permite: se ponen en objetos Person y más tarde se toman o se llevan a cabo los objetos Person. Simplemente no se escriben programas que trabajan de otra manera. Bueno, es igual en Ruby. Si se utiliza una variable para algún propósito, hay muchas posibilidades de que se estará usando para la misma finalidad cuando se acceda de nuevo tres líneas más adelante. El tipo de caos que podría producirse simplemente no ocurre. Además de eso, mucha de la gente que codifica en Ruby tiende a adoptar un cierto estilo de codificación. Escriben un montón de métodos en corto y los prueban sobre la marcha. Métodos en corto significa que el alcance de la mayoría de las variables es limitado: simplemente no hay mucho tiempo para que las cosas vayan mal con su tipo. Y las pruebas capturan los errores tontos cuando suceden: errores ortográficos y demás simplemente no tienen la oportunidad de propagarse a través del código. El resultado es que la “seguridad” en “la seguridad de tipos” es a menudo ilusoria y que la codificación en un lenguaje más dinámico como Ruby es segura y productiva. Por lo tanto, si nos hemos puesto nerviosos por la falta de tipos estáticos en Ruby, le sugerimos que trate de poner estas preocupaciones en un segundo plano por un tiempo para darle una oportunidad a Ruby. Creemos que usted se sorprenderá de cuán raramente aparecen errores debido a problemas de tipo, y en lo productivo que será una vez que comience a explotar el poder del tipado dinámico.
Clases no son Tipos La cuestión de los tipos es en realidad algo más profunda que un debate entre los defensores del tipado fuerte y la gente hippie-freak del tipado dinámico. El verdadero problema es la cuestión de que es un tipo en primer lugar? Si usted ha estado programando en lenguajes con tipos convencionales, lo que le ha sido enseñado es que el tipo de un objeto es su clase --todos los objetos son instancias de alguna clase y la clase es el tipo
249
de objeto. La clase define las operaciones (métodos) que el objeto soporta, junto con el estado (variables de instancia) en la que los métodos funcionan. Vamos a ver algo de código Java. Customer c; c = database.findCustomer(“dave”);
/* Java */
En este fragmento se declara la variable c para ser de tipo Customer, y se establece como referencia el objeto customer para Dave que hemos creado a partir de un registro de base de datos. Por lo que el tipo de objeto en c es Customer, ¿verdad? Tal vez. Sin embargo, incluso en Java, el tema es un poco más profundo. Java soporta el concepto de interfaces, que son una especie de clase base abstracta mutilada. Una clase Java puede ser declarada como la implementación de múltiples interfaces. Al utilizar esta característica, es posible que se hayan definido las clases de la siguiente manera: public interface Customer { long getID(); Calendar getDateOfLastContact(); // ... } public class Person implements Customer { public long getID() { ... } public Calendar getDateOfLastContact() { ... } // ... } Así que incluso en Java, la clase no siempre es el tipo --a veces el tipo es un subconjunto de la clase, y en ocasiones objetos implementan varios tipos. En Ruby, la clase no es (bueno, casi nunca) el tipo. En cambio, el tipo de un objeto se define más por lo que ese objeto puede hacer. En Ruby, llamamos a esto duck typing. Si un objeto camina como un pato y habla como un pato, entonces, el intérprete se siente feliz de tratarlo como si fuera un pato. Veamos un ejemplo. Tal vez hemos escrito un método para escribir el nombre de nuestros clientes al final de un archivo abierto. class def end def end end
Customer initialize(first_name, last_name) @first_name = first_name @last_name = last_name append_name_to_file(file) file << @first_name << “ “ << @last_name
Para ser buenos programadores, vamos a escribir una prueba unitaria para esto. Hay que ser prevenido --es complicado (pero vamos a mejorar en breve). require ‘test/unit’ require ‘addcust’ class TestAddCustomer < Test::Unit::TestCase def test_add c = Customer.new(“Ima”, “Customer”) f = File.open(“tmpfile”, “w”) do |f| c.append_name_to_file(f) end f = File.open(“tmpfile”) do |f| assert_equal(“Ima Customer”, f.gets)
250
end ensure File.delete(“tmpfile”) if File.exist?(“tmpfile”) end end produce: Finished in 0.003473 seconds. 1 tests, 1 assertions, 0 failures, 0 errors Tenemos que hacer todo este trabajo para crear un archivo para escritura, volverlo a abrir y leer el contenido para verificar que se ha escrito la cadena correcta. También tenemos que eliminar el archivo cuando se haya terminado (pero sólo si existe). En su lugar, sin embargo, podemos confiar en el tipado pato. Todo lo que necesitamos es algo que camina como un archivo y habla como un archivo que puede pasar al método bajo prueba. Y todo esto significa que en esta circunstancia necesitamos un objeto que responde al método << añadiendo algo. ¿Tenemos algo que hace esto? ¿Qué tal un humilde String? require ‘test/unit’ require ‘addcust’ class TestAddCustomer < Test::Unit::TestCase def test_add c = Customer.new(“Ima”, “Customer”) f = “” c.append_name_to_file(f) assert_equal(“Ima Customer”, f) end end produce: Finished in 0.001951 seconds. 1 tests, 1 assertions, 0 failures, 0 errors El método bajo prueba piensa que está escribiendo en un archivo, pero en su lugar simplemente está añadiendo a una cadena. Al final, podemos entonces sólo prober que el contenido es correcto. No tenemos que usar una cadena --para el objeto que estamos probando aquí, una matriz va a funcionar igual de bien. require ‘test/unit’ require ‘addcust’ class TestAddCustomer < Test::Unit::TestCase def test_add c = Customer.new(“Ima”, “Customer”) f = [] c.append_name_to_file(f) assert_equal([“Ima”, “ “, “Customer”], f) end end produce: Finished in 0.001111 seconds. 1 tests, 1 assertions, 0 failures, 0 errors De hecho, esta forma puede ser más conveniente si queremos comprobar que las cosas individuales se insertan correctamente.
251
Por tanto, el tipado pato es conveniente para las pruebas, pero, ¿qué pasa en el cuerpo de las propias aplicaciones? Bueno, resulta que lo mismo que hizo fácil las pruebas del ejemplo anterior también hace fácil la escritura de código de aplicación flexible. De hecho, Dave tuvo una experiencia interesante donde el tipado pato le sacó (y a un cliente) de un agujero. Había escrito una gran aplicación Ruby basada en Web que (entre otras cosas) mantiene una tabla de base de datos completa de los detalles de los participantes en una competición. El sistema proporciona valores separados por comas (CSV) con la capacidad de poderlos descargar, lo que permite a los administradores importar esta información en sus hojas de cálculo de forma local. Justo antes de la competición, el teléfono empieza a sonar. La descarga, que había estado trabajando muy bien hasta ese momento, estaba tomando tanto tiempo que las solicitudes agotaban el tiempo de espera. La presión fue intensa, ya que los administradores tenían que usar esa información para hacer planificaciones y enviar correos. Un poco de experimentación demostró que el problema estaba en la rutina que tomaba los resultados de la consulta de la base de datos y generaba la descarga CSV. El código se veía algo así como def csv_from_row(op, row) res = “” until row.empty? entry = row.shift.to_s if /[,”]/ =~ entry entry = entry.gsub(/”/, ‘””’) res << ‘”’ << entry << ‘”’ else res << entry end res << “,” unless row.empty? end op << res << CRLF end result = “” query.each_row {|row| csv_from_row(result, row)} http.write result Cuando este código corre con conjuntos de datos de tamaño moderado, desempeña bien. Pero a un tamaño de entrada determinado, se desacelera de repente y se viene abajo. ¿El culpable? La recolección de basura. El enfoque estaba generando miles de cadenas intermedias y la construcción de una gran cadena de resultado, una línea a la vez. Como esta gran cadena crece, se necesita más espacio y se invoca la recolección de basura, que requiere el escaneo y eliminación de todas las cadenas itermedias. La respuesta era simple y sorprendentemente efectiva. En lugar de construir la cadena de resultado a medida que avanzaba, el código se cambió para almacenar cada fila CSV como un elemento de una matriz. Esto significaba que las líneas intermedias seguian siendo todavía referenciadas y por lo tanto ya no eran basura. También significaba que ya no se contruía una gran cadena cada vez mayor que obligaba a la recolección de basura. Gracias al tipado pato, el cambio fue trivial. def csv_from_row(op, row) # como antes end result = [] query.each_row {|row| csv_from_row(result, row)} http.write result.join Lo único que cambia es que pasamos una matriz en el método csv_from_row. Debido a que (implícitamente) utilizamos tipado pato, el método en sí no fué modificado: continua añadiendo los datos que genera a su parámetro, sin importarle el tipo de parámetro que es. Después que el método devuelve el resultado, se unen todas las líneas individuales en una gran cadena. Este cambio trajo una reducción del tiempo de ejecución desde más de 3 minutos a unos pocos segundos.
252
Codificando Como un Pato Si desea escribir sus programas con la filosofía tipado pato, sólo se necesita recordar una cosa: un tipo de objeto está determinado por lo que puede hacer, no por su clase. (De hecho, Ruby 1.8 ahora desaprueba el método Objeto#type a favor de Objeto#class por esta razón: el método devuelve la clase del receptor, por lo que la palabra type es engañosa). ¿Qué significa esto en la práctica? En un nivel, simplemente significa que a menudo hay poco valor para probar la clase de un objeto. Por ejemplo, es posible que vaya a escribir una rutina para añadir información de una canción en una cadena. Si usted viene de C# o Java, puede estar tentado a escribir: def append_song(result, song) # test para probar que se dan los parámetros correctos unless result.kind_of?(String) fail TypeError.new(“String expected”) end unless song.kind_of?(Song) fail TypeError.new(“Song expected”) end result << song.title << “ (“ << song.artist << “)” end result = “” append_song(result, song)
->
“I Got Rhythm (Gene Kelly)”
Adoptanto el tipado pato de Ruby se escribe código mucho más simple.
def append_song(result, song) result << song.title << “ (“ << song.artist << “)” end result = “” append_song(result, song)
-> “I Got Rhythm (Gene Kelly)”
No es necesario comprobar el tipo de los argumentos. Si soportan << (en el caso de result) o title y artist (en el caso de song), todo va a funcionar. Si no, el método dará una excepción de todos modos (como lo habría hecho si hubiera revisado los tipos). Pero sin la comprobación de tipos, su método de repente es mucho más flexible: se puede pasar una matriz, una cadena, un archivo o cualquier otro objeto usando << y vá a funcionar. Ahora, a veces usted puede querer utilizar más este estilo de programación de laissez-faire. Puede haber buenas razones para comprobar que un parámetro puede hacer lo que se necesita. ¿Será expulsado fuera del club de tipado pato si comprueba el parámetro con una clase? No (El club tipado pato no comprueba para ver si usted es miembro...). Sin embargo, es posible que prefiera considerar la comprobación sobre la base de las capacidades del objeto, en lugar de su clase. def append_song(result, song) # test para probar que se dan los parámetros correctos unless result.respond_to?(:<<) fail TypeError.new(“’result’ needs `<<’ capability”) end unless song.respond_to?(:artist) && song.respond_to?(:title) fail TypeError.new(“’song’ needs ‘artist’ and ‘title’”) end result << song.title << “ (“ << song.artist << “)” end
253
result = “” append_song(result, song)
->
“I Got Rhythm (Gene Kelly)”
Sin embargo, antes de ir por este camino, asegúrese de que está obteniendo un beneficio real --ya que hay que escribir y mantener una gran cantidad de código extra.
Sobre Protocolos y Coerciones Estándares Aunque técnicamente no es parte del lenguaje, el intérprete y la biblioteca estándar utilizan varios protocolos para manejar los asuntos en los que se ocupan otros lenguajes para el uso de tipos. Algunos objetos tienen más de una representación natural. Por ejemplo, usted puede querer escribir una clase para representar los números romanos (I, II, III, IV, V, etc). Esta clase no es necesariamente una subclase de Integer, debido a que sus objetos son representaciones de números, no números en sí mismos. Al mismo tiempo, ellos tienen una calidad similar a entero. Sería bueno poder utilizar los objetos de nuestra clase número Romano donde Ruby espera ver un entero. Para ello, Ruby tiene el concepto de protocolos de conversión --un objeto puede optar por convertirse en un objeto de otra clase. Ruby tiene tres formas estándar de hacer esto. Ya hemos encontrado la primera. Métodos tales como to_s y to_i convierten su receptor en cadenas y enteros. Estos métodos de conversión no son muy estrictos: si un objeto tiene algún tipo de representación decente como una cadena, por ejemplo, es probable que tenga un método to_s. Nuestra clase romana, probablemente implementaría to_s con el fin de devolver la representación de cadena de un número (VII, por ejemplo). La segunda forma de la función de conversión utiliza métodos con nombres como to_str y to_int. Estas son las funciones estrictas de conversión: sólo se pueden poner en práctica si el objeto, de forma natural, se puede utilizar en todos los lugares donde una cadena o un entero podrían ser utilizados. Por ejemplo, nuestros objetos de números romanos tienen una clara representación de número entero y por lo tanto deben implementar to_int. Cuando se trata de la cadeneidad, sin embargo, la cosa es más dificil. Números romanos claramente tienen una representación de cadena, pero ¿son cadenas? ¿debemos utilizar una cadena siempre que podamos utilizar una cadena en sí misma? No, probablemente no. Lógicamente, son una representación de un número. Se pueden representar como cadenas pero no son conectables a las cadenas. Por esta razón, un número romano no ejecutará to_str --en realidad no es una cadena. Para volver al principio: Los números romanos se pueden convertir en cadenas utilizando to_s, pero no son inherentemente cadenas, por lo que no implementan to_str. Para ver cómo funciona esto en la práctica, echemos un vistazo a la apertura de un archivo. El primer parámetro de File.new puede ser un desciptor de fichero existente (representado por un número entero) o un nombre de archivo para abrirlo. Sin embargo, Ruby no se limita a mirar el primer parámetro y comprobar si su tipo es Fixnum o String. Por el contrario, da al objeto pasado la oportunidad de presentarse como un número o una cadena. Escribirlo en Ruby, puede parecer algo así como class File def File.new(file, *args) if file.respond_to?(:to_int) IO.new(file.to_int, *args) else name = file.to_str # llamada al sistema operativo para abrir el archivo ‘name’ end end end Así que vamos a ver qué pasa si queremos pasar a File.new un entero descriptor de archivo almacenado como un número romano. Debido a que nuestra clase implementa to_int, el primer test respond_to? tendrá éxito. Vamos a pasar una representación de entero de nuestro número a IO.open y será devuelto el descriptor de fichero, todo ello envuelto en un nuevo objeto IO.
254
La librería estándar tiene incorporadas un pequeño número de funciones de conversión estricta:
to_ary → Array Se usa cuando el intérprete necesita convertir un objeto en una matriz para el paso de parámetros o la asignación múltiple. class OneTwo def to_ary [ 1, 2 ] end end ot = OneTwo.new a, b = ot puts “a = #{a}, b = #{b}” printf(“%d -- %d\n”, *ot) produce: a = 1, b = 2 1 -- 2 to_hash → Hash Se utiliza cuando el intérprete espera ver un Hash. (El único uso conocido es el segundo parámetro de Hash#replace). to_int → Integer Se utiliza cuando el intérprete espera ver un valor entero (tal como un descriptor de fichero o un parámetro para Kernel.Integer). to_io → IO Se utiliza cuando el intérprete está esperando objetos IO (tales como parámetros para IO#reopen o IO.select). to_proc → Proc Se utiliza para convertir un objeto precedido de un signo & en una llamada a método. class OneTwo def to_proc proc { “one-two” } end end def silly yield end ot = OneTwo.new silly(&ot) -> “one-two” to_str → String Se utiliza en casi cualquier lugar cuando el intérprete busca un valor String. class OneTwo def to_str “one-two” end end ot = OneTwo.new puts(“count: “ + ot) File.open(ot) rescue puts $!.message
255
produce: count: one-two No such file or directory - one-two Nótese, sin embargo, que el uso de to_str no es universal - algunos métodos que quieren argumentos de cadena no llaman a to_str. File.join(“/user”, ot)
->
“/user/#”
to_sym → Symbol Expresan el receptor como un símbolo. No utilizado por el intérprete para las conversiones y probablemente no sea útil en el código de usuario. Un último punto: las clases como Integer y Fixnum implementan el método to_int y la clase String implementa to_str. De esa manera se pueden llamar a las funciones de conversión estricta polimórficamente: # No importa si obj es un Fixnum o un número Romano, # la conversión todavía tiene éxito num = obj.to_int
Coerción Numérica Un par de páginas atras se dijo que el intérprete realiza tres tipos de conversión. Cubrimos la conversión flexible y la estricta. La tercera es la coerción numérica. He aquí el problema. Cuando se escribe “1 + 2”, Ruby sabe llamar al + en el objeto 1 (un Fixnum), pasándole el Fixnum 2 como parámetro. Sin embargo, cuando se escribe “1+2.3”, el método + recibe ahora un parámetro Float. ¿Cómo puede saber qué hacer (en particular, como comprueba las clases de sus parámetros si va en contra del espíritu de tipado pato)? La respuesta está en el protocolo de coerción de Ruby, basado en el método coerce. El funcionamiento básico de coerce es simple. Necesitan dos números (uno como su receptor, el otro como un parámetro). Devuelve una matriz de dos elementos que contiene representaciones de estos dos números (pero con el parámetro primero, seguido por el receptor). El método coerce garantiza que estos dos objetos tienen la misma clase y por tanto, que se pueden sumar (o multiplicar, o comparar o lo que sea). 1.coerce(2) 1.coerce(2.3) (4.5).coerce(2.3) (4.5).coerce(2)
-> -> -> ->
[2, 1] [2.3, 1.0] [2.3, 4.5] [2.0, 4.5]
El truco es que el receptor llama al método coerce de su parámetro para generar esta matriz. Esta técnica, llamada double dispatch, permite a un método cambiar el comportamiento basado no sólo en su clase, sino también en la clase de su parámetro. En este caso, estamos dejando que el parámetro decida exactamente qué clases de objetos se suman (o multiplican, dividen, etc.) Digamos que estamos escribiendo una nueva clase con la intención de tomar parte en la aritmética. Para participar en la coerción, es necesario implementar el método coerce. Éste toma algún otro tipo de número como parámetro y devuelve una matriz que contiene dos objetos de la misma clase cuyos valores son equivalentes. Para nuestra clase número Romano es bastante fácil. Internamente, cada objeto número Romano tiene su valor real como un Fixnum en una variable de instancia @value. El método coerce comprueba si la clase de su parámetro es también un Integer. Si es así, retorna su parámetro y su valor interno. Si no es así, convierte primero a ambos en coma flotante. class Roman def initialize(value)
256
@value = value end def coerce(other) if Integer === other [ other, @value ] else [ Float(other), Float(@value) ] end end # .. otras cosas de Roman end iv = Roman.new(4) xi = Roman.new(11) 3 * iv 1.1 * xi
-> ->
12 12.1
Por supuesto, la clase Roman tal como se implementa no sabe que hacer sumas de si misma: no se podría haber escrito “xi + 3” en el ejemplo anterior, ya que Roman no tiene un método “mas”. Y así es probablemente como debe ser. Pero ahora vamos a implementar la suma para números romanos. class Roman MAX_ROMAN = 4999 attr_reader :value protected :value def initialize(value) if value <= 0 || value > MAX_ROMAN fail “Los valores romanos deben ser > 0 y <= #{MAX_ROMAN}” end @value = value end def coerce(other) if Integer === other [ other, @value ] else [ Float(other), Float(@value) ] end end def +(other) if Roman === other other = other.value end if Fixnum === other && (other + @value) < MAX_ROMAN Roman.new(@value + other) else x, y = other.coerce(@value) x + y end end FACTORS = [[“m”, 1000], [“cm”, 900], [“d”, 500], [“cd”, 400], [“c”, 100], [“xc”, 90], [“l”, 50], [“xl”, 40], [“x”, 10], [“ix”, 9], [“v”, 5], [“iv”, 4], [“i”, 1]] def to_s value = @value roman = “” for code, factor in FACTORS count, value = value.divmod(factor)
257
roman << (code * count) end roman end end iv = Roman.new(4) xi = Roman.new(11) iv iv iv xi xi
+ + + + +
3 3 + 4 3.14159 4900 4990
-> -> -> -> ->
vii xi 7.14159 mmmmcmxi 5001
Por último, tenga cuidado con coerce --siempre tratar de coercionar a un tipo más general, o se puede terminar generando bucles de coerción, donde A intenta coercionar a B, y B trata de coercionar de vuelta a A.
Walk the Walk, Talk the Talk (Recorrer el Camino, Predicar con el Ejemplo)
Duck typing puede generar controversia. De vez en cuando se un hilo arde en las listas de correo o algún blog con alguien favor o en contra del concepto. Muchos colaboradores en estas discusiones tienen algunas posiciones bastante extremas. En última instancia, sin embargo, duck typing no es un conjunto de reglas, sólo es un estilo de programación. Diseñe sus programas para equilibrar la paranoia y la flexibilidad. Si usted siente la necesidad de restringir los tipos de los objetos que los usuarios de un método pasan, pregúntese por qué. Trate de determinar qué podría salir mal si usted esperaba una cadena y en su lugar obtiene una matriz. A veces, la diferencia es de vital importancia. A menudo, sin embargo, no lo es. Trate de errar en el lado más permisivo por un tiempo y observe si suceden cosas malas. Si no, tal vez duck typing no sea sólo para las aves.
Clases y Objetos Las clases y los objetos son, obviamente, el centro de Ruby, pero a primera vista puede parecer un poco confuso. Parece que hay un montón de conceptos: clases, objetos, objetos de clase, métodos de instancia, métodos de clase, clases singleton y clases virtuales. En realidad, sin embargo, Ruby tiene sólo una sola estructura de clase y objeto subyacente, de la que hablaremos en este capítulo. De hecho, el modelo básico es muy simple, podemos describirlo en un solo párrafo. Un objeto en Ruby tiene tres componentes: un conjunto de banderas, algunas variables de instancia y una clase asociada. Una clase en Ruby es un objeto de la clase Class, que contiene todas las cosas de un objeto además de una lista de métodos y una referencia a una superclase (que a su vez es otra clase). Todas las llamadas a métodos en Ruby nombran a un receptor (que por propio defecto es self, el objeto en curso). Ruby encuentra el método a invocar buscando en la lista de métodos de la clase del receptor. Si no encuentra el método allí, mira en todos los módulos incluidos, luego en su superclase, los módulos de la superclase, luego en la superclase de la superclase y así sucesivamente. Si el método no se puede encontrar en la clase del receptor o de cualquiera de sus ascendientes, entonces invoca el método method_missing en el receptor original.
Y eso es todo, la explicación completa. Siguiente capítulo.
“Pero espere”, grita usted: “Gasté un buen dinero en este capítulo. ¿Qué pasa con todas esas otras cosas --las clases virtuales, los métodos de clase, etc. ¿Cómo funcionan?” Buena pregunta.
Cómo Interactúan las Clases y los Objetos Todas las interacciones clase/objeto se explican utilizando el modelo simple dado anteriormente: objetos referencian clases, y las clases referencian cero o más superclases. Sin embargo, conseguir los detalles de implementación puede ser un poco complicado.
258
Hemos encontrado que la forma más sencilla de visualizar todo esto es dibujando las estructuras reales que implementa Ruby. Por lo tanto, en las páginas siguientes vamos a ver todas las posibles combinaciones de clases y objetos. Tenga en cuenta que estos no son diagramas de clases en sentido UML, estamos mostrando las estructuras en memoria y los punteros entre ellas.
Su Objeto Básico, de uso diario Vamos a empezar viendo un objeto creado a partir de una clase simple. La figura 17 muestra un objeto referenciado por una variable, lucille, la clase del objeto, Guitar y la superclase de la clase, Object. Observe como la referencia a la clase del objeto, klass, apunta a la clase de objeto y cómo el puntero super hace referencia a la clase padre. Si invocamos el método lucille.play(), Ruby va al receptor, Lucille, y sigue la referencia klass al objeto de clase Guitar. Busca en la tabla el método, encuentra play y lo invoca. Si en su lugar se llama a lucille.display(), Ruby comienza de la misma manera, pero no puede encontrar display en la tabla de métodos de la clase de Guitar. A continuación sigue la referencia supera la superclase de Guitar, Object, donde encuentra el método y lo ejecuta.
Metaclases
Los lectores astutos (sí, pensamos que son todos ustedes) se habran dado cuenta de que los miembros klass de objetos class no apuntan a nada significativo en la figura 17. Ahora tenemos toda la información que necesitamos para resolver a lo que debe hacer referencia. Cuando usted dice lucille.play(), Ruby sigue puntero klass para encontrar un objeto de clase en el que buscar métodos. Entonces, ¿qué sucede cuando se invoca un método de clase, como Guitar.strings(...)? Aquí, el receptor es el objeto mismo de la clase, Guitar. Por lo tanto, para que haya consistencia, es necesario atenerse a los métodos de otra clase, referenciada por el puntero klass de Guitar. Esta nueva clase contendrá todos los métodos de clase de Guitar. Aunque la terminología es un poco dudosa, vamos a llamar a esto una metaclase (vea el recuadro en la página siguiente). Vamos a denotar la metaclase de Guitar como Guitar’. Pero esto no es toda la historia. Debido a que Guitar es una subclase de Object, su metaclase Guitar’será una subclase de la metaclase de Object, 259
Object’. En la figura 18, se muestran estas metaclases adicionales. Cuando Ruby ejecuta Guitar.strings(), se sigue el mismo proceso que antes: se va al receptor, la clase Guitar, se sigue la referencia klass para la clase Guitar’ y encuentra el método. Por último, señalar que un V se ha colado en las banderas de la clase Guitar’. Las clases que Ruby crea automáticamente se marcan internamente como clases virtuales. estas clases virtuales se tratan de forma ligeramente diferente en Ruby. La diferencia más obvia desde el exterior es que son efectivamente invisibles: nunca aparecerán en una lista de objetos retornados desde métodos tales como Module#ancestors o ObjectSpace.each_object, y no se pueden crear instancias de ellos utilizando new.
Objetos Específicos de Clase Ruby le permite crear una clase ligada a un objeto particular. En el siguiente ejemplo, creamos dos objetos String. A continuación, asociamos una clase anónima con uno de ellos, anulando uno de los métodos de la clase base del objeto y añadiéndole un nuevo método. a = “hello” b = a.dup class <
260
def two_times self + self end end a.to_s a.two_times b.to_s
-> -> ->
“The value is ‘hello’” “hellohello” “hello”
Metaclases y Clases Singleton Durante la revisión de este libro, el uso del término metaclase generó una gran discusión, ya que las metaclases Ruby son diferentes a las de lenguajes como Smalltalk. Pasado un tiempo, Matz intervino con lo siguiente: Se le puede llamar metaclase, pero, a diferencia de Smalltalk, no es una clase de una clase, es una clase singleton de una clase. • Todos los objetos en Ruby tienen sus propios atributos (métodos, constantes, etc) que en otros idiomas están en manos de las clases. Es como que cada objeto tiene su propia clase. • Para manejar atributos por objeto, Ruby proporciona algo similar a una clase para cada objeto que se llama a veces clase singleton. • En la implementación actual, las clases son objetos de la clase singleton especialmente señalada entre los objetos y su clase. Estas pueden ser clases “virtuales” si lo decide el implementador del lenguaje. • Las clases Singleton se comportan para las clases como metaclases de Smalltalk. En este ejemplo se utiliza la notación class < “The value is ‘hello’” a.two_times -> “hellohello” b.to_s -> “hello” El efecto es el mismo en ambos casos: una clase se añade al objeto a. Esto nos da un fuerte indicio sobre la implementación Ruby: se crea una clase virtual y se inserta como clase directa de a. La clase original de a, String, se hace superclase esta clase virtual. La imagen del antes y el después se muestra en la figura 19 en la página siguiente: Hay que recordar que las clases en Ruby nunca se cierran. Siempre se puede abrir una clase y añadir nuevos métodos a la misma. Lo mismo se aplica a las clases virtuales. Si la referencia de un objeto klass ya apunta a una clase virtual, no se creará una nueva. Esto significa que la primera de las dos definiciones de método en el ejemplo anterior crea una clase virtual, pero el segundo simplemente añadir un método a la misma. El método Object#extend añade los métodos en su parámetro a su receptor, por lo que si es necesario, también crea una clase virtual. obj.extend(Mod) es básicamente equivalente a:
261
class <
Módulos Mixin Cuando una clase incluye un módulo, los métodos de instancia del módulo están disponibles como métodos de instancia de la clase. Es casi como si el módulo se conviertiera en una superclase de la clase que lo utiliza. No es de extrañar que sea así cómo funciona. Cuando se incluye un módulo, Ruby crea una clase proxy1 anónima que hace referencia a ese módulo y se inserta el proxy como la superclase directa de la clase que incluyó al módulo. La clase proxy contiene referencias a las variables de instancia y los métodos del módulo. Esto es importante: el mismo módulo puede ser incluido en muchas diferentes clases y va a aparecer en muchas cadenas de herencia diferentes. Sin embargo, gracias a la clase proxy, seguirá habiendo un solo módulo de fondo: modificar una definición de método en ese módulo, hará que se modifique en todas las clases que incluyen el módulo, tanto en el pasado como en el futuro. module SillyModule def hello 1
Proxy: programa o dispositivo que realiza una acción en representación de otro.
262
“Hello.” end end class SillyClass include SillyModule end s = SillyClass.new s.hello -> “Hello.” module SillyModule def hello “Hi, there!” end end s.hello
->
“Hi, there!”
La relación entre las clases y los módulos mixin que incluyen se muestra en la figura 20 en la página siguiente. Si se incluyen varios módulos se añaden a la cadena en orden. Si un módulo en si mismo incluye otros módulos, se va a añadir una cadena de clases proxy a cualquier clase que incluya este módulo. Un proxy para cada módulo que está directa o indirectamente incluido.
Extender Objetos Así como se puede definir una clase anónima para un objeto mediante class < “hee, hee!” Hay un truco interesante con extend. Si lo usa en una definición de clase, los métodos del módulo se convierten en los métodos de clase. Esto se debe a que llamar a extend es equivalente a self.extend, por lo que los métodos se añaden a self, que en una definición de clase es la clase misma.
He aquí un ejemplo de cómo agregar métodos de un módulo a nivel de clase.
module Humor def tickle “hee, hee!” end end class Grouchy include Humor extend Humor end Grouchy.tickle a = Grouchy.new a.tickle
->
“hee, hee!”
->
“hee, hee!”
263
Definiciones de Clase y Módulo Después de haber agotado las combinaciones de clases y objetos, podemos (afortunadamente) volver a la programación para mirar las tuercas y tornillos de las definiciones de clases y módulos. En lenguajes como C++ y Java, las definiciones de clase se procesan en tiempo de compilación: el compilador crea las tablas de símbolos, trabaja sobre laq asignación de almacenamiento, construye tablas de distribución y hace todas esas cosas oscuras sobre las que preferimos no pensar demasiado.
Ruby es diferente. En Ruby, las definiciones de clase y de módulo son código ejecutable. Aunque se analizan en tiempo de compilación, las clases y los módulos son creados en tiempo de ejecución, según se encuentra la definición. (Lo mismo es cierto para las definiciones de método). Esto le permite estructurar sus programas mucho más dinámicamente que la mayoría de los lenguajes convencionales. Se pueden tomar decisiones una vez, cuando se define la clase, en lugar de cada vez que se utilizan los objetos de la clase. La clase en el siguiente ejemplo, decide a medida que se define la versión a crear de una rutina de descifrado. module Tracing # ... end class MediaPlayer include Tracing if $DEBUG if ::EXPORT_VERSION def decrypt(stream) raise “Decryption not available”
264
end else def decrypt(stream) # ... end end end Si las definiciones de clase son código ejecutable, esto implica que se ejecutan en el contexto de un objeto: self debe hacer referencia a algo. Vamos a ver lo que es. class Test puts “Class of self = #{self.class}” puts “Name of self = #{self.name}” end produce: Class of self = Class Name of self = Test Esto significa que una definición de clase se ejecuta con esa clase como el objeto actual. Volviendo a la sección sobre metaclases unas páginas atrás, podemos ver que esto significa que los métodos en la metaclase y sus superclases estarán disponibles durante la ejecución de la definición del método. Podemos comprobar esto. class Test def Test.say_hello puts “Hello from #{name}” end say_hello end produce: Hello from Test En este ejemplo se define un método de clase, Test.say_hello, y luego es llamado en el cuerpo de la definición de clase. Dentro de say_hello, llamamos a name, un método de instancia de la clase Module . Debido a que Module es un ancestro de Class, sus métodos de instancia se pueden llamar sin un receptor explícito dentro de una definición de clase.
Variables de Instancia de Clase Si se ejecuta una definición de clase en el contexto de algún objeto, esto implica que una clase puede tener variables de instancia. class Test @cls_var = 123 def Test.inc @cls_var += 1 end end Test.inc Test.inc
-> ->
124 125
Si las clases tienen sus propias variables de instancia, ¿podemos utilizar attr_reader y amigos para acceder a ellos? Podemos, pero tenemos que ejecutar estos métodos en el lugar correcto. Para las variables de instancia normales, los accesores de atributo se definen a nivel de clase. Para las variables de
265
instancia de clase, tenemos que definir los métodos de acceso en la metaclase. class Test @cls_var = 123 class <
->
123
Esto nos lleva a un punto interesante. Muchas de las directivas que se utilizan para definir una clase o módulo, cosas como alias_method, attr y public, son simplemente métodos en la clase Module. Esto crea algunas posibilidades intrigantes --se puede ampliar la funcionalidad de las definiciones de clase y módulo escribiendo código Ruby. Echemos un vistazo a un par de ejemplos. Vamos a ver un ejemplo de adición de una línea de documentación básica a los módulos y las clases. Esto nos permite asociar una cadena a módulos y clases que escribimos y que es accesible cuando el programa está en funcionamiento. Vamos a elegir una sintaxis sencilla. class Example doc “This is a sample documentation string” # .. rest of class end Tenemos que hacer doc disponible para cualquier módulo o clase, por lo que necesitamos hacerlo un método de instancia de la clase Module. class Module @@docs = {} # invocado en las definiciones de clase def doc(str) @@docs[self.name] = self.name + “:\n” + str.gsub(/^\s+/, ‘’) end # invocado para obtener la documentación def Module::doc(aClass) # Si nos pasa una clase o módulo, se convierte en cadena # (‘<=’ Para las clases de comprobación de la misma clase o subtipo) aClass = aClass.name if aClass.class <= Module @@docs[aClass] || “No documentation for #{aClass}” end end class Example doc “This is a sample documentation string” # .. rest of class end module Another doc <<-edoc And this is a documentation string in a module edoc # rest of module end puts Module::doc(Example) puts Module::doc(“Another”) produce:
266
Example: This is a sample documentation string Another: And this is a documentation string in a module El segundo ejemplo es una mejora de rendimiento basada en el módulo date de Tadayoshi Funaba (que se verá más adelante). Digamos que tenemos una clase que representa una cierta cuantificación subyacente (en este caso, una fecha). La clase puede tener muchos atributos que presentan la misma fecha subyacente de diversas maneras: como un número de día Juliano, como una cadena, como un triple [año, mes, día] , etc. Cada valor representa la misma fecha y su derivación puede implicar un cálculo bastante complejo. Quisiéramos por tanto, tener el cálculo de cada atributo de una sola vez, la primera que es accedido. La forma manual sería añadir una prueba para cada accesor. class ExampleDate def initialize(day_number) @day_number = day_number end def as_day_number @day_number end def as_string unless @string # complex calculation @string = result end @string end def as_YMD unless @ymd # another calculation @ymd = [ y, m, d ] end @ymd end # ... end Esta es una técnica torpe --Vamos a ver si podemos llegar a algo más sexy. Lo que estamos buscando es una directiva que indique que el cuerpo de un método en particular debe ser invocado sólo una vez. El valor devuelto por la primera llamada debe ser almacenado en caché. A partir de entonces, la llamada a ese mismo método debe devolver el valor almacenado en caché sin volver a evaluar el cuerpo del método de nuevo. Esto es similar al modificador de Eiffel once para las rutinas. Nos gustaría ser capaces de escribir algo como: class ExampleDate def as_day_number @day_number end def as_string # complex calculation end def as_YMD # another calculation [ y, m, d ] end once :as_string, :as_YMD end
267
Podemos utilizar once como una directiva escribiéndola como un método de clase de ExampleDate, pero, ¿como tiene que verse internamente? El truco está en reescribir los métodos cuyos nombres se pasan. Para cada método, se crea un alias para el código original, y luego se crea un nuevo método con el mismo nombre. Aquí está el código de Tadayoshi Funaba, ligeramente reordenado. def once(*ids) # :nodoc: for id in ids module_eval <<-”end;” alias_method :__#{id.to_i}__, :#{id.to_s} private :__#{id.to_i}__ def #{id.to_s}(*args, &block) (@__#{id.to_i}__ ||= [__#{id.to_i}__(*args, &block)])[0] end end; end end Este código utiliza module_eval para ejecutar un bloque de código en el contexto del módulo llamador (o, en este caso, la clase llamadora). El método original se renombra con __nnn__, donde la parte nnn es la representación de número entero del símbolo ID del nombre del método. El código utiliza el mismo nombre para la variable de instancia de caché. Entonces se define un método con el nombre original. Si la variable de instancia de caché tiene un valor, se devuelve ese valor, de lo contrario se llama al método original y se devuelve su valor de retorno en caché.
Si se entiende este código se estará bien encaminado a cierto dominio de Ruby.
Sin embargo, podemos ir más lejos. Observando el módulo date se verá el método once escrito ligeramente diferente. class Date class << self def once(*ids) # ... end end # ... end Lo interesante aquí es la definición de la clase interna << self. Se define una clase basada en el objeto self, y self pasa a ser el objeto de la clase Date. ¿El resultado? Todos los métodos dentro de la definición de la clase interna son automáticamente métodos de clase de Date. La característica once es aplicable en general --debería funcionar para cualquier clase. Si se toma once y se convierte en un método privado de instancia de la clase Module, estará disponible para en cualquier clase Ruby. (Y por supuesto se puede hacer esto, ya que la clase Module es abierta y usted es libre de añadir métodos en ella.)
Los Nombres de Clase son Constantes Hemos dicho que cuando se llama a un método de clase, todo lo que estamos haciendo es enviar un mensaje al objeto Class en sí. Cuando usted dice algo como String.new(“Gumby”), está enviando el mensaje new al objeto que es la clase String. Pero, ¿cómo hace Ruby para saber como hacer esto? Después de todo, el receptor de un mensaje debe ser una referencia de objeto, lo que implica que debe haber una constante llamada String en alguna parte que contiene una referencia al objeto String2. Y de hecho, eso es exactamente lo que sucede. Todas las clases integradas, junto con las clases que usted defina, tienen una correspondiente constante global con el mismo nombre que la clase. Esto es directo y sutil. La sutileza viene del hecho de que las dos cosas son nombradas (por ejemplo) String en el sistema. Hay una constante que referencia la clase String (un objeto de la clase Class), y está el objeto (la clase) en sí mismo. 2 Será una constante, no una variable, ya que String comienza con una letra mayúscula. 268
El hecho de que los nombres de clase son constantes significa que se puede tratar a las clases como a cualquier otro objeto Ruby: pueden copiarse, pasarse a métodos y utilizarse en expresiones. def factory(klass, *args) klass.new(*args) end factory(String, “Hello”) factory(Dir, “.”)
-> ->
“Hello” #
flag = true (flag ? Array : Hash)[1, 2, 3, 4] flag = false (flag ? Array : Hash)[1, 2, 3, 4]
->
[1, 2, 3, 4]
->
{1=>2, 3=>4}
Esto tiene otra faceta: si una clase que no tiene nombre se asigna a una constante, Ruby le da a la clase el nombre de la constante. var = Class.new var.name -> “” Wibble = var var.name ->
“Wibble”
Entrono de Ejecución de Nivel Superior Muchas veces en este libro hemos afirmado que todo en Ruby es un objeto. Sin embargo, hemos utilizado una y otra vez lo que parece contradecir esto --el entorno de ejecución de alto nivel de Ruby. puts “Hola, Mundo” No hay un objeto a la vista. Bien podríamos haberlo escrito en alguna variante de Fortran o BASIC. Sin embargo, si se cava más profundo, se encontrararán objetos y clases que acechan incluso en el más simple código. Sabemos que el literal “Hola, Mundo” genera un String de Ruby, por lo que es un objeto. También sabemos que la llamada al método pelado puts al e es efectivamente la misma que self.puts. Pero, ¿qué es self? self.class
-->
Object
En el nivel superior, estamos ejecutando el código en el contexto de un objeto predeterminado. Cuando definimos los métodos, en realidad estamos creando métodos de instancia (privados) para la clase Object . Esto es bastante sutil, ya que como están en la clase Object, estos métodos están disponibles en todas partes. Y ya que estamos en el contexto de Object, podemos utilizar todos los métodos de Object(incluidos los mixtos de Kernel) en forma de función. Esto explica por qué podemos llamar a los métodos de Kernel, como puts en el nivel superior (y de hecho a lo largo de todo Ruby): estos métodos son parte de cada objeto.
Las variables de instancia de alto nivel también pertenecen a este objeto de nivel superior.
Herencia y Visibilidad
La última arruga en la herencia de clases es bastante oscura.
Dentro de una definición de clase, se puede cambiar la visibilidad de un método en una clase antecesora. Por ejemplo, usted puede hacer algo como class Base def aMethod
269
puts “Got here” end private :aMethod end class Derived1 < Base public :aMethod end class Derived2 < Base end En este ejemplo, se debe ser capaz de invocar aMethod en instancias de la clase Derived1, pero no a través de las instancias de Base o Derived2. Entonces, ¿cómo logra Ruby esta hazaña de tener un método con dos visibilidades diferentes? En pocas palabras: se engaña. Si una subclase cambia la visibilidad de un método en una clase padre, Ruby inserta efectivamente un método proxy oculto en la subclase que invoca el método original utilizando super. A continuación, establece la visibilidad de ese proxy para lo que se ha pedido. Esto significa que el código class Derived1 < Base public :aMethod end
es efectivamente lo mismo que
class Derived1 < Base def aMethod(*args) super end public :aMethod end La llamada a super puede acceder a métodos de los padres, independientemente de su visibilidad, por lo que la reescritura permite a la subclase reemplazar las reglas de visibilidad de su clase padre. Dá miedo, ¿eh?
Congelación de Objetos A veces usted ha trabajado duro para hacer de su objeto exactamente correcto y, que le aspen si usted permite que nadie lo vaya a cambiar. Tal vez tiene que pasar algún tipo de objeto opaco entre dos de sus clases a través de algunos objeto de terceros y desea asegurarse de que llega sin modificaciones. Tal vez desee utilizar un objeto como una clave hash y necesitee asegurarse de que nadie lo modifique mientras se está utilizando. Tal vez algo está haciendo corrupto uno de sus objetos y le gustaría que Ruby provoque una excepción tan pronto como se produzca el cambio. Ruby proporciona un mecanismo muy simple para ayudar en esto. Cualquier objeto puede ser congelado por la invocación de Object#freeze. Un objeto congelado no puede ser modificado: no puede cambiar sus variables de instancia (directa o indirectamente), no se le puede asociar métodos singleton, y, si se trata de una clase o módulo, no se les puede añadir, eliminar o modificar sus métodos. Una vez congelado, un objeto se mantiene congelado: no hay ningún Object#deshielo. Usted puede probar si un objeto está congelado mediante Object#frozen?. ¿Qué sucede cuando se copia un objeto congelado? Eso depende del método que se utilice. Si se llama al método de un objeto clone, el estado del objeto entero (incluyendo si está congelado) se copia en el nuevo objeto. Por otro lado, dup normalmente sólo copia el contenido del objeto --la nueva copia no heredará el estado congelado. str1 = “hello” str1.freeze ->
“hello”
270
str1.frozen? -> str2 = str1.clone str2.frozen? -> str3 = str1.dup str3.frozen? ->
true true false
Aunque al principio la congelación de objetos puede parecer una buena idea, es posible que prefiera esperar a hacerlo hasta que llegue una necesidad real. La congelación es una de esas ideas que se ven esenciales en el papel pero que no se utilizan mucho en la práctica.
Bloqueo de Seguridad de Ruby Walter WebCoder tiene una gran idea para un sitio de portal: la Página Web de la Aritmética. Rodeado de todo tipo de fríos enlaces matemáticos y anuncios de banner que lo hará rico, es un sencillo formulario web que contiene un campo de texto y un botón. Los usuarios escriben una expresión aritmética en el campo, hacen clic en el botón, y se muestra la respuesta. Todas las calculadoras del mundo se vuelven obsoletos de la noche a la mañana, Walter hace caja y se retira para dedicar la vida a su colección de números de matrículas de coches. La aplicación de la calculadora es fácil, piensa Walter. Accede a los contenidos del campo del formulario utilizando la biblioteca CGI de Ruby y utiliza el método eval para evaluar la cadena como una expresión. require ‘cgi’ cgi = CGI.new(“html4”) # Recuperar el valor del campo del formulario “expression” expr = cgi[“expression”].to_s begin result = eval(expr) rescue Exception => detail # manejar expresiones malas end # mostrar el resultado de vuelta al usuario... Aproximadamente siete segundos después de que Walter pone la aplicación en línea, una niña de doce años de edad, desde Waxahachie, con problemas glandulares y sin vida real, tipea system(“rm *”) en el formulario y, al igual que los archivos de su ordenador, los sueños de Walter se vienen abajo. Walter aprendió una importante lección: Toda la información externa es peligrosa. No deje cerca a las interfaces que pueden modificar su sistema. En este caso, el contenido del campo de formulario fué el de datos externos y la llamada a eval fue la violación de la seguridad. Afortunadamente, Ruby proporciona soporte para la reducción de este riesgo. Toda la información del mundo exterior puede ser marcada como contaminada. Cuando se ejecuta en un modo seguro, los métodos potencialmente peligrosos lanzarán un SecurityError si pasa un objeto contaminado.
Los Niveles de Seguridad La variable $SAFE determina el nivel Ruby de paranoia. La Tabla 25.1, dos páginas adelante, proporciona más detalles de las comprobaciones realizadas en cada nivel de seguridad. El valor por defecto de $SAFE es cero en la mayoría de las circunstancias. Sin embargo, si un script Ruby se ejecuta en modo setuid o setgid3, o si se ejecuta bajo mod_ruby, se establece su nivel de seguridad automáticamente en 1. El nivel de seguridad también se pueden ajustar mediante el uso de la opción -T de línea de comandos y mediante la asignación de $SAFE dentro del programa. No es posible reducir el valor de $SAFE por asignación.
3
Un script de Unix puede ser marcado para ejecutarse con el ID de usuario o grupo al que pertenece. Esto permite a otro usuario ejecutar con script los privilegios que no tiene y poder acceder a los recursos que de otro modo tendría prohibidos. Estos scripts se llaman setuid o setgid.
271
El valor en curso de $SAFE se hereda cuando se crean hilos nuevos. Sin embargo, dentro de cada hilo, se puede cambiar el valor de $SAFE sin afectar al valor en otros hilos. Esta característica se puede utilizar para implementar “cajas” seguras. Áreas en las que el código externo se puede ejecutar sin peligro para el resto de la aplicación o sistema. Para ello, se envuelve el código que se va a cargar desde un archivo, en su propio módulo anónimo. Esto protegerá el espacio de nombres del programa de cualquier alteración involuntaria. f=open(filename,”w”) f.print ... # escribir en un fichero el programa no fiable f.close Thread.start do $SAFE = 4 load(filename, true) end
$SAFE
Limitaciones
0 No se comprueba el suminstro externo de datos (contaminados). Modo por defecto. ≥1 No se permite el uso de datos contaminados para operaciones potencialmente peligrosas. ≥2 Se prohíbe la carga de archivos de programa desde ubicaciones globales para escritura. ≥3 Todos los objetos de nueva creación se consideran contaminados. ≥4 Ruby particiona eficazmente la ejecución del programa en dos. Objetos no contaminados no pueden ser modificados. Con un nivel $SAFE de 4, sólo se pueden cargar archivos envueltos. Véase la descripción de Kernel.load más adelante para más detalles. El nivel de seguridad en vigor cuando se crea un objeto Proc se almacena con ese objeto. Un Proc no se puede pasar a un método si está contaminado y el nivel de seguridad actual es mayor que el que estaba en vigor cuando se creó el bloque.
Objetos Contaminados Cualquier objeto Ruby obtenido de una fuente externa (por ejemplo, una cadena leída desde un archivo o una variable de entorno) es automáticamente marcado como contaminado. Si el programa utiliza un objeto contaminado para obtener un nuevo objeto, ese nuevo objeto también estará contaminado, como se muestra en el código de abajo. Cualquier objeto con datos externos de algún momento de su pasado va a estar contaminado. Este proceso de contaminado se lleva a cabo independientemente del nivel de seguridad en curso. Puede ver si un objeto está contaminado utilizando Object#tainted?. # internal data # =============
# external data # =============
x1 = “a string” x1.tainted?
->
false
y1 = ENV[“HOME”] y1.tainted?
->
true
x2 = x1[2, 4] x2.tainted?
->
false
y2 = y1[2, 4] y2.tainted?
->
true
x1 =~ /([a-z])/ $1.tainted?
-> ->
0 false
y1 =~ /([a-z])/ $1.tainted?
-> ->
2 true
Se puede forzar la contaminación de un objeto invocando al método taint. Si el nivel de seguridad es inferior a 3, se puede descontaminar un objeto mediante la invocación de untaint. Esto no es como para hacerlo a la ligera (también se pueden utilizar algunos trucos tortuosos para hacer esto sin usar untaint, pero vamos a dejar que su lado más oscuro los encuentre).
Está claro que Walter ha ejecutado el script CGI en el nivel seguro 1. Esto hizo que se lanzara una
272
excepción cuando el programa trató de pasar datos del formulario a eval. Una vez que sucede esto, Walter tiene una serie de opciones. Podría haber optado por aplicar un programa de análisis de expresiones adecuadas, evitando los riesgos inherentes al uso de eval. Por lo menos, ¡la pereza!, probablemente tendría que haber realizado alguna comprobación simple de seguridad en los datos del formulario para descontaminarlos y que pasen sólo los inocuos. require ‘cgi’; $SAFE = 1 cgi = CGI.new(“html4”) expr = cgi[“expression”].to_s if expr =~ %r{\A[-+*/\d\seE.()]*\z} expr.untaint result = eval(expr) # display result back to user... else # display error message... end Personalmente, creemos que Walter sigue corriendo riesgos indebidos. Probablemente preferiríamos ver un verdadero analizador, pero la implementación de uno aquí, no tendría nada que enseñarnos acerca de los objetos contaminados, así que vamos a pasar a otros temas.
Reflexión, Espacio de Objeto y Ruby Distribuído Una de las muchas ventajas de los lenguajes dinámicos como Ruby es la capacidad de introspección --el examinar los aspectos del programa desde el propio programa. Java, por ejemplo, llama a esta característica reflection, pero las capacidades de Ruby van más allá de Java. La palabra reflexión evoca la imagen de mirarse en el espejo --tal vez la investigación de la propagación implacable de la calva en la parte superior de la cabeza. Es una analogía muy apropiada: se utiliza la reflexión para examinar las partes de nuestros programas que normalmente no son visibles desde donde estamos. En este estado de ánimo profundamente introspectivo, mientras que estamos contemplando nuestro ombligo y quemando incienso (teniendo cuidado de no intercambiar las dos tareas), ¿qué podemos aprender de nuestro programa? Pues, podríamos descubrir:
• • • •
Qué objetos contiene, La jerarquía de clases, los atributos y métodos de los objetos y información sobre los métodos.
Armados con esta información, podemos mirar a los objetos particulares y decidir cuáles de sus métodos llamar en tiempo de ejecución --incluso si no existía la clase del objeto cuando se escribió por primera vez el código. También podemos empezar a hacer cosas inteligentes, tal vez modificar el programa según se está ejecutando. ¿Causa temor? No tiene por qué. De hecho, con estas capacidades de reflexión vamos a hacer algunas cosas muy útiles. Más adelante en este capítulo vamos a ver Ruby distribuido y el cálculo de referencias, dos tecnologías basadas en la reflexión que nos permite enviar objetos en todo el mundo y a través del tiempo.
Mirando Objetos ¿Alguna vez ha anhelado la posibilidad de recorrer todos los objetos que viven en su programa? ¡Podemos! Ruby le permite realizar este truco con ObjectSpace.each_object. Podemos usarlo para hacer todo tipo de truquitos.
273
Página 383 del original en inglés.
274
Por ejemplo, para iterar sobre todos los objetos de tipo Numeric, se podría escribir lo siguiente.
a = 102.7 b = 95.1 ObjectSpace.each_object(Numeric) {|x| p x } produce: 95.1 102.7 2.71828182845905 3.14159265358979 2.22044604925031e-16 1.79769313486232e+308 2.2250738585072e-308 Hey, ¿de dónde salen todos esos números extra? No los definimos en nuestro programa. Más adelante, veremos que la clase Float define las constantes para el float máximio y mínimo, así como Epsilon, la más pequeña diferencia distinguible entre dos floats. El módulo Math define constantes para e y π. Puesto que estamos examinando todos los objetos que viven en el sistema, estos a su vez aparecen también.
Vamos a probar el mismo ejemplo con números diferentes.
a = 102 b = 95 ObjectSpace.each_object(Numeric) {|x| p x } produce: 2.71828182845905 3.14159265358979 2.22044604925031e-16 1.79769313486232e+308 2.2250738585072e-308 Ninguno de los objetos Fixnum que hemos creado apareció. Esto es porque ObjectSpace no sabe acerca de los objetos con valores inmediatos: Fixnum, Symbol, true, false y nil.
Mirando Dentro de los Objetos Una vez que haya encontrado un objeto interesante, puede tener la tentación de averiguar lo que puede hacer. A diferencia de los lenguajes estáticos, donde el tipo de una variable determina su clase y, por lo tanto, los métodos que soporta, Ruby soporta objetos liberados. Usted realmente no puede decir exactamente lo que un objeto puede hacer hasta que mira debajo de su capota (o bajo su capó, para los objetos creados al este del Atlántico). Hablamos de esto en el capítulo de Duck Typing, anteriormente.
Por ejemplo, podemos obtener una lista de todos los métodos a los que un objeto va a responder.
r = 1..10 # Crear un objeto Range list = r.methods list.length -> 68 list[0..3] -> [“collect”, “to_a”, “instance_eval”, “all?”]
O bien, podemos comprobar si un objeto admite un método en particular.
r.respond_to?(“frozen?”) r.respond_to?(:has_key?) “me”.respond_to?(“==”)
-> -> ->
true false true
275
Podemos determinar la clase de nuestro objeto y su identificador de objeto único (ID) y poner a prueba su relación con otras clases. num = 1 num.id num.class num.kind_of? Fixnum num.kind_of? Numeric num.instance_of? Fixnum num.instance_of? Numeric
-> -> -> -> -> ->
3 Fixnum true true true false
Mirando Clases Saber acerca de los objetos es una parte de la reflexión, pero para obtener el cuadro completo, usted también necesita ser capaz de mirar a las clases --los métodos y constantes que contienen. En cuanto a la jerarquía de clases es fácil. Puede obtener la clase padre de cualquier clase en particular con Class#superclass. Para clases y módulos, Module#ancestors lista tanto superclases como módulos mixed-in. klass = Fixnum begin print klass klass = klass.superclass print “ < “ if klass end while klass puts p Fixnum.ancestors produce: Fixnum < Integer < Numeric < Object [Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel] Si se quiere construir una jerarquía de clases completa, basta con ejecutar este código para cada clase en el sistema. Podemos utilizar ObjectSpace para iterar sobre todos los objetos Class. ObjectSpace.each_object(Class) do |klass| # ... end
Mirando Dentro de las Clases Podemos buscar un poco más en los métodos y las constantes de un objeto en particular. En lugar de sólo comprobar si el objeto responde a un mensaje dado, podemos preguntarle a los métodos por el nivel de acceso e inquirir sólo por los métodos singleton. También podemos echar un vistazo a las constantes, locales y variables de instancia del objeto. class Demo @@var = 99 CONST = 1.23 private def private_method end protected def protected_method end public def public_method
276
@inst = 1 i = 1 j = 2 local_variables end def Demo.class_method end end Demo.private_instance_methods(false) Demo.protected_instance_methods(false) Demo.public_instance_methods(false) Demo.singleton_methods(false) Demo.class_variables Demo.constants Demo. superclass.constants
-> -> -> -> ->
[“private_method”] [“protected_method”] [“public_method”] [“class_method”] [“@@var”]
->
[“CONST”]
demo = Demo.new demo.instance_variables -> [] # Obtener ‘public_method’ para retornar sus variables locales # y establecer una variable de instancia demo.public_method -> [“i”, “j”] demo.instance_variables -> [“@inst”] Module.constants devuelve todas las constantes disponibles a través de un módulo, incluyendo las constantes de las superclases del módulo. No estamos interesados en estas justo en este momento, así que las restamos de nuestra lista. Usted puede preguntarse acerca de los parámetros false del código anterior. A partir de Ruby 1.8, estos métodos de reflexión, por defecto recorre las clases padres y las clases padres de éstas y así sucesivamente hasta recorrer toda la cadena de los antepasados. Pasando false se detiene este tipo de fisgoneo. Dada una lista de nombres de métodos, ahora puede verse tentados a tratar de llamalos. Afortunadamente, esto es fácil con Ruby.
Llamar a Métodos de Forma Dinámica Los programadores de C y Java a menudo se encuentran escribiendo una especie de mesa de despacho: funciones que se invocan sobre la base de un comando. Piense en el típico lenguaje C donde se tiene que traducir una cadena a un puntero a función. typedef struct { char *name; void (*fptr)(); } Tuple; Tuple list[]= { { “play”, fptr_play }, { “stop”, fptr_stop }, { “record”, fptr_record }, { 0, 0 }, }; ... void dispatch(char *cmd) { int i = 0; for (; list[i].name; i++) { if (strncmp(list[i].name,cmd,strlen(cmd)) == 0) { list[i].fptr(); return; }
277
} /* not found */ } En Ruby, usted puede hacer todo esto en una sola línea. Ponga todas las funciones de comando en una clase, cree una instancia de esa clase (llamamandola commands), y haga que el objeto ejecute un método llamado con el mismo nombre que la cadena de comando. commands.send(command_string) Ah, y por cierto, hace mucho más que la versión C --ya que es de forma dinámica. La versión Ruby encuentra nuevos métodos añadidos en tiempo de ejecución con la misma facilidad.
No se tienen que escribir las clases especiales de comando para send: funciona en cualquier objeto.
“John Coltrane”.send(:length) “Miles Davis”.send(“sub”, /iles/, ‘.’)
-> ->
13 “M. Davis”
Otra forma de invocar métodos de forma dinámica es utilizando objetos Method. Un objeto Method es como un objeto Proc: representa un trozo de código y un contexto en el que se ejecuta. En este caso, el código es el cuerpo del método y el contexto es el objeto que creó el método. Una vez que tenemos nuestro objeto Method, se puede ejecutar en algún momento más adelante mediante el envío del mensaje call. trane = “John Coltrane”.method(:length) miles = “Miles Davis”.method(“sub”) trane.call miles.call(/iles/, ‘.’)
-> ->
13 “M. Davis”
Puede pasar el objeto Method como lo haría con cualquier otro objeto y cuando se invoca Method#call, se ejecuta el método como si se hubiera invocado en el objeto original. Es como tener un puntero a función de estilo C, pero de forma totalmente orientada a objetos.
También puede utilizar los objetos Method con iteradores.
def double(a) 2*a end mObj = method(:double) [ 1, 3, 5, 7 ].collect(&mObj)
->
[2, 6, 10, 14]
Los objetos Method están destinados a un objeto en particular. Se puede crear métodos no consolidados (de la clase UnboundMethod) y, posteriormente, unirse a uno o más objetos. La unión (binding) crea un nuevo objeto Method. Al igual que con los alias, los métodos no ligados son referencias a la definición del método en el momento de su creación. unbound_length = String.instance_method(:length) class String def length 99 end end str = “cat” str.length -> 99 bound_length = unbound_length.bind(str) bound_length.call -> 3 Como las cosas buenas vienen de tres en tres, esta es otra manera de llamar a los métodos de forma dinámica. El método eval (y sus variantes, tales como class_eval, module_eval y instance_eval)
278
analizará y ejecutará una cadena arbitraria de código fuente legítimo Ruby. trane = %q{“John Coltrane”.length} miles = %q{“Miles Davis”.sub(/iles/, ‘.’)} eval trane eval miles
-> ->
13 “M. Davis”
Cuando se utiliza eval, puede ser útil de manera explícita el contexto en el que la expresión debe ser evaluada, en lugar de utilizar el contexto actual. Se puede obtener un contexto llamando a Kernel#binding en el punto deseado. def get_a_binding val = 123 binding end val = “cat” the_binding = get_a_binding eval(“val”, the_binding) -> eval(“val”) ->
123 “cat”
La primera eval evalúa val en el contexto de la unión, como era cuando el método get_a_binding se estaba ejecutando. En esta unión, la variable val tiene un valor de 123. El segundo eval evalúa val en el nivel superior de unión, donde se tiene el valor “cat”.
Consideraciones sobre el rendimiento Como hemos visto en esta sección, Ruby nos ofrece varias formas de invocar un método arbitrario de algún objeto: Object#send, Method#call y las diferentes versiones de eval. Es posible que se prefiera utilizar cualquiera de estas técnicas en función de las necesidades, pero hay que teneren cuenta que eval es significativamente más lento que los otros (o, para los lectores optimistas, send y call son significativamente más rápidos que eval). require ‘benchmark’ include Benchmark test = “Stormy Weather” m = test.method(:length) n = 100000 bm(12) {|x| x.report(“call”) { n.times { m.call } } x.report(“send”) { n.times { test.send(:length) } } x.report(“eval”) { n.times { eval “test.length” } } } produce: user system total real call 0.250000 0.000000 0.250000 (0.340967) send 0.210000 0.000000 0.210000 (0.254237) eval 1.410000 0.000000 1.410000 (1.656809)
Sistema de Ganchos Un gancho es una técnica que le permite atrapar algún evento Ruby, tal como la creación de objetos. La técnica de gancho más simple en Ruby es interceptar las llamadas a métodos en las clases del sistema. Tal vez se quiere registrar todos los comandos del sistema operativo que ejecuta el programa. Basta con cambiar el nombre del método Kernel.system y sustituirlo por uno a elegir que registre tanto los 279
comandos como las llamadas al método Kernel original. module Kernel alias_method :old_system, :system def system(*args) result = old_system(*args) puts “system(#{args.join(‘, ‘)}) returned #{result}” result end end system(“date”) system(“kangaroo”, “-hop 10”, “skippy”) produce: Thu Aug 26 22:37:22 CDT 2004 system(date) returned true system(kangaroo, -hop 10, skippy) returned false Un gancho potente es la captura de los objetos que se crean. Si se puede estar presente en cada objeto que nace, se puede hacer todo tipo de cosas interesantes: se pueden envolver, añadirles y eliminarles métodos, añadirles a contenedores que implementan persistencia, lo que sea. Vamos a mostrar un ejemplo sencillo: vamos a añadir una marca de tiempo a cada objeto que se crea. Primero, añadiremos un atributo timestamp a cada objeto del sistema. Podemos hacer esto hackeando la mismísima clase Object. class Object attr_accessor :timestamp end Después tenemos que enganchar la creación de objetos para agregarles esta marca de tiempo. Una manera de hacer esto es mediante nuestro truco de cambiar el nombre de método en Class#new, el método al que se llama para reservar espacio para un nuevo objeto. La técnica no es perfecta, algunos objetos integrados, tales como cadenas literales, se construyen sin llamar a new --pero va a funcionar bien para los objetos que escribamos. class Class alias_method :old_new, :new def new(*args) result = old_new(*args) result.timestamp = Time.now result end end Por último, podemos realizar una prueba. Vamos a crear un par de objetos con unos pocos milisegundos de diferencia y a comprobar sus marcas de tiempo. class Test end obj1 = Test.new sleep(0.002) obj2 = Test.new obj1.timestamp.to_f obj2.timestamp.to_f
-< ->
1093577843.1312 1093577843.14144
Todo esto cambiar el nombre de método está muy bien y realmente funciona, pero hay que tener en cuenta que puede causar problemas. Si una subclase hace lo mismo, y cambia el nombre de los métodos utilizando los mismos nombres, se terminará con un bucle infinito. Se puede evitar esto con alias de los
280
métodos a un nombre de símbolo único o mediante el uso de una nomenclatura coherente. Hay otras formas más refinadas de obtener el corazón de un programa en ejecución. Ruby ofrece varios métodos de retorno de llamada que permiten atrapar ciertos eventos de una manera controlada.
Retornos de Llamada en Tiempo de Ejecución (Runtime Callbacks)
Usted puede ser notificado cada vez que ocurre uno de los siguientes eventos:
Evento Método de Retorno de llamada Adición de un método de instancia Module#method_added Eliminación de un método de instancia Module#method_removed Método de instancia no definido Module#method_undefined Adición de un método singleton Kernel.singleton_method_added Eliminación de un método singleton Kernel.singleton_method_removed Método singleton no definido Kernel.singleton_method_undefineded Se crea una subclase Class#inherited Se crea un método mixin Module#extend_object Por defecto, estos métodos no hacen nada. Si se define un método de retorno en una clase, va a ser invocado de forma automática. La secuencia real de llamada se ilustra en las descripciones de librería para cada método de retorno de llamada. Hacer un seguimiento del método de creación y la utilización de clases y módulos permite crear una imagen exacta del estado dinámico del programa. Esto puede ser importante. Por ejemplo, puede haber se escrito código que envuelva todos los métodos de una clase, tal vez para añadir soporte transaccional o para implementar alguna forma de delegación. Esto sólo sería la mitad del trabajo: la naturaleza dinámica de Ruby hace que los usuarios de esta clase pudieran añadirle nuevos métodos en cualquier momento. Utilizando estas devoluciones de llamada, se puede escribir código que envuelva estos nuevos métodos a medida que se crean.
Seguimiento de la Ejecución del Programa
Mientras nos estamos divirtiendo con este tema sobre la reflexión de los objetos y las clases en nuestros programas, no nos olvidemos de las humildes declaraciones que hacen que nuestro código en realidad funcione eficazmente. Resulta que Ruby nos permite también mirar en detalle estas declaraciones. En primer lugar, se puede ver al intérprete como ejecuta el código. set_trace_func ejecuta un Proc con todo tipo de jugosa información de depuración siempre que se ejecuta una línea fuente nueva, llamadas a métodos, objetos creados, etc. Se encontrará una descripción completa más adelante pero aquí vamos a ver una muestra. class Test def test a = 1 b = 2 end end set_trace_func proc {|event, file, line, id, binding, classname| printf “%8s %s:%-2d %10s %8s\n”, event, file, line, id, classname } t = Test.new t.test produce: line prog.rb:11 false c-call prog.rb:11 new Class c-call prog.rb:11 initialize Object 281
c-return prog.rb:11 initialize Object c-return prog.rb:11 new Class line prog.rb:12 false call prog.rb:2 test Test line prog.rb:3 test Test line prog.rb:4 test Test return prog.rb:4 test Test El método trace_var le permite agregar un gancho a una variable global. Cada vez que se haga una asignación a nivel global, se invoca el objeto Proc.
¿Cómo Llegamos Hasta Aquí? Una buena pregunta, y uno se la hace con frecuencia. Lapsos mentales a un lado, en Ruby al menos se puede saber exactamente “cómo se llegó allí” con el método caller, que devuelve un array de objetos String que representa la pila de llamadas en curso. def cat_a puts caller.join(“\n”) end def cat_b cat_a end def cat_c cat_b end cat_c produce: prog.rb:5:in `cat_b’ prog.rb:8:in `cat_c’ prog.rb:10
Una vez que haya averiguado cómo llegó allí, el dónde ir ahora depende de usted.
Código Fuente Ruby ejecuta programas desde antiguos ficheros planos. Se pueden ver estos archivos para examinar el código fuente que compone el programa con una de una serie de técnicas. La variable especial __FILE__ contiene el nombre del archivo fuente actual. Esto lleva a un muy corto (si hacer trampa) Quine --un programa que produce su propio código fuente como salida única. print File.read(__FILE__) El método Kernel.caller devuelve la pila de llamadas --la lista de la pila existente en el momento en que se llamó al método. Cada entrada de esta lista comienza con un nombre de archivo, dos puntos y un número de línea en ese archivo. Se puede analizar esta información para buscar en el código fuente. En el siguiente ejemplo, tenemos un programa principal, main.rb, que llama a un método en un archivo separado, sub.rb. Este método invoca a su vez un bloque, donde se recorre la pila de llamadas y escribe las líneas fuente en cuestión. Nótese el uso de un hash del contenido del archivo, indexado por el nombre de archivo.
Aquí está el código que hace el volcado de la pila de llamadas, incluyendo información de la fuente.
def dump_call_stack file_contents = {} puts “File Line Source Line” puts “-------------------------------------------------------+------+--------------------” 282
caller.each do |position| next unless position =~ /\A(.*?):(\d+)/ file = $1 line = Integer($2) file_contents[file] ||= File.readlines(file) printf(“%-25s:%3d - %s”, file, line, file_contents[file][line-1].lstrip) end end
El (trivial) archivo sub.rb contiene un solo método.
def sub_method(v1, v2) main_method(v1*3, v2*6) end Y aquí está el programa principal, que invoca el volcado de pila después de haber sido llamado por el submétodo. require ‘sub’ require ‘stack_dumper’ def main_method(arg1, arg2) dump_call_stack end sub_method(123, “cat”) produce: File Line Source Line ----------------------------------+------+-------------------code/caller/main.rb : 5 - dump_call_stack ./code/caller/sub.rb : 2 - main_method(v1*3, v2*6) code/caller/main.rb : 8 - sub_method(123, “cat”) La constante SCRIPT_LINES__ está estrechamente relacionada con esta técnica. Si un programa inicializa una constante llamada SCRIPT_LINES__ con un hash, este hash recibe el código fuente de todos los archivos cargados posteriormente en el intérprete añ utilizar require o load. Vea más adelante Kernel.require para un ejemplo.
Ruby Formateado (marshaling) y Distribuído Java ofrece la posibilidad de serializar objetos, lo que permite guardarlos en algún lugar y reconstruirlos cuando sea necesario. Se puede utilizar esta función, por ejemplo, para salvar un árbol de objetos que representan una parte del estado de la aplicación --un documento, un dibujo en CAD, una pieza de música, etc. Ruby llama a este tipo de serialización marshaling (hay que pensar en una estación de clasificación de ferrocarril, donde los automóviles se montan secuencialmente en un tren completo, que luego es enviado a alguna parte). Salvar un objeto y todos o algunos de sus componentes se realiza mediante el método Marshal.dump. Por lo general, se vuelca un árbol de objetos completo a partir de un objeto dado. Más tarde, se puede reconstituir el objeto utilizando Marshal.load. He aquí un breve ejemplo. Contamos con una clase Chord que contiene una colección de notas musicales. Nos gustaría salvar fuera un acorde particularmente fabuloso para poderlo enviar por correo electrónico a un par de cientos de nuestros amigos más cercanos. Así pueden cargarlo en su copia de Ruby y disfrutarlo también. Vamos a empezar con las clases Note y Chord. Note = Struct.new(:value) class Note def to_s value.to_s
283
end end class def end def end end
Chord initialize(arr) @arr = arr play @arr.join(‘-’)
Ahora vamos a crear nuestra obra maestra y a utilizar Marshal.dump para salvarla a una versión serializada en el disco. c = Chord.new( [ Note.new(“G”), Note.new(“Bb”), Note.new(“Db”), Note.new(“E”) ] ) File.open(“posterity”, “w+”) do |f| Marshal.dump(c, f) end
Finalmente, para que nuestros nietos lo lean y se transporten por la grandiosidad de nuestra creación.
File.open(“posterity”) do |f| chord = Marshal.load(f) end chord.play -> “G-Bb-Db-E”
Estrategia de Serialización Personalizada No todos los objetos pueden ser objeto de volcado: bindings, objetos de procedimiento, instancias de la clase IO y objetos singleton no se pueden salvar fuera del entorno de ejecución Ruby (se produce un TypeError si se intenta). Incluso si su objeto no contiene uno de estos objetos problemáticos, es posible que desee tomar el control de la serialización de objetos por sí mismo. Marshal ofrece los ganchos que se necesitan. En los objetos que requieran serialización personalizada, simplemente hay aplicar dos métodos de instancia: marshal_dump, que escribe el objeto a una cadena y marshal_load, que lee una cadena que se habrá creado previamente y la utiliza para inicializar un objeto recién asignado. (En versiones anteriores de Ruby tendrá que utilizar los métodos llamados _dump y _Load, pero las nuevas versiones desempeñan mejor con el nuevo esquema de asignación de Ruby 1.8). El método de instancia Marshal_dump debe devolver un objeto que representa el estado de volcado. Cuando el objeto es reconstituido posteriormente utilizando Marshal.load que llama a este objeto, lo utilizará para establecer el estado de su receptor --que se llevará a cabo en el contexto de una asignación pero no inicializa el objeto de la clase que se está cargando. Por ejemplo, aquí hay una clase de ejemplo que define su propia serialización. Por las razones que sean, en Special no se quiere salvar uno de sus miembros de datos interno, @volatile. El autor ha decidido serializar las otras dos variables de instancia en una matriz. class def end def end def
Special initialize(valuable, volatile, precious) @valuable = valuable @volatile = volatile @precious = precious marshal_dump [ @valuable, @precious ] marshal_load(variables)
284
@valuable = variables[0] @precious = variables[1] @volatile = “unknown” end def to_s “#@valuable #@volatile #@precious” end end obj = Special.new(“Hello”, “there”, “World”) puts “Before: obj = #{obj}” data = Marshal.dump(obj) obj = Marshal.load(data) puts “After: obj = #{obj}” produce: Before: obj = Hello there World After: obj = Hello unknown World
Para más detalles, consulte la sección de referencia Marshal más adelante.
YAML para el Formateado El módulo Marshal está integrado en el intérprete y utiliza un formato binario para almacenar los objetos externos. Aunque rápido, este formato binario tiene una gran desventaja: si el intérprete cambia de manera significativa, el formato marshal binario también puede cambiar, y los antiguos archivos volcados ya no se podrán cargar. Una alternativa es utilizar un formato externo menos exigente, preferentemente uno de texto en lugar de archivos binarios. Una opción suministrada como librería estándar de Ruby 1.8 es YAML (http://www. yaml.org. YAML es sinónimo de YAML no es el lenguaje de marcado --YAML Ain’t Markup Language--, pero esto no parece importante. Podemos adaptar nuestro ejemplo marshal anterior para utilizar YAML. En lugar de aplicar métodos específicos de carga y volcado para controlar el proceso marshal, simplemente definimos el método to_yaml_properties, que devuelve una lista de variables de instancia para ser salvadas. require ‘yaml’ class Special def initialize(valuable, volatile, precious) @valuable = valuable @volatile = volatile @precious = precious end def to_yaml_properties %w{ @precious @valuable } end def to_s “#@valuable #@volatile #@precious” end end obj = Special.new(“Hello”, “there”, “World”) puts “Before: obj = #{obj}” data = YAML.dump(obj) obj = YAML.load(data) puts “After: obj = #{obj}” produce: Before: obj = Hello there World
285
After: obj = Hello
World
Podemos echar un vistazo a lo que crea YAML como forma serializada del objeto --es bastante simple.
obj = Special.new(“Hello”, “there”, “World”) puts YAML.dump(obj) produce: --- !ruby/object:Special precious: World valuable: Hello
Ruby Distribuído Ya que puede serializar un objeto o un conjunto de objetos en una forma adecuada para almacenarlos fuera de proceso, podemos utilizar esta capacidad para la transmisión de objetos de un proceso a otro. Unimos esto con la poderosa capacidad de las redes y, voilà: Se tiene un sistema de objetos distribuidos. Para ahorrarle la molestia de tener que escribir el código, le sugerimos que utilice la librería de Ruby distribuido (Distributed Ruby library -drb-) de Masatoshi Seki, que ya está disponible como librería estándar de Ruby. Utilizando drb, un proceso Ruby puede actuar como un servidor, como un cliente o como ambos. Un servidor drb actúa como fuente de objetos, mientras que un cliente es un usuario de esos objetos. Para el cliente, los objetos aparecen como locales, pero en realidad el código está siendo ejecutado de forma remota. Un servidor inicia un servicio mediante la asociación de un objeto con un puerto determinado. Entonces se crean hilos internamente para manejar las peticiones entrantes en ese puerto, a fin de recordar la unión al hilo drb antes de salir del programa. require ‘drb’ class TestServer def add(*args) args.inject {|n,v| n + v} end end server = TestServer.new DRb.start_service(‘druby://localhost:9000’, server) DRb.thread.join # No salir todavía! Un cliente drb sencillo, simplemente crea un objeto drb local y la asocia con el objeto en el servidor remoto. El objeto local es un proxy. require ‘drb’ DRb.start_service() obj = DRbObject.new(nil, ‘druby://localhost:9000’) # Ahora utilizamos obj puts “Sum is: #{obj.add(1, 2, 3)}” El cliente se conecta al servidor y llama al método add, que utiliza la magia de inyectar a la suma sus argumentos. Se devuelve el resultado, que imprime el cliente. Sum is: 6 El argumento inicial nil a DRbObject indica que se desea adjuntar un nuevo objeto distribuido. También se puede utilizar un objeto existente. Ho Hum, dice usted. Esto suena como RMI de Java, o CORBA, o lo que sea. Sí, se trata de un mecanismo de objetos distribuidos funcional--pero está escrito en tan sólo unos pocos cientos de líneas de
286
código Ruby. Nada de C, nada lujoso, simple y viejo código plano Ruby. Por supuesto, no tiene servicio de nombres u operador de servicios, ni nada parecido a lo que vemos en CORBA, pero es sencillo y razonablemente rápido. En un sistema Powerbook de 1GHz, este código de ejemplo ejecuta alrededor de unas 500 llamadas de mensajes remotos por segundo. Y, si le gusta el aspecto de JavaSpaces de Sun, la base de la arquitectura JINI, le interesará saber que el drb se distribuye con un breve módulo que realiza el mismo tipo de cosas. JavaSpaces se basa en una tecnología llamada Linda. Para probar que su autor japonés tiene sentido del humor, la versión de Ruby de Linda se conoce como Rinda. Si le gusta la mensajería remota gruesa, tonta e interoperable, también podría buscar en las bibliotecas de SOAP distribuidas con Ruby (SOAP hace mucho tiempo abandonó la parte simple de sus siglas. La implementación Ruby de SOAP es una maravillosa pieza de trabajo).
¿Tiempo de Compilación? ¿Tiempo de Ejecución? ¡En Cualquier Momento! Lo importante que hay recordar acerca de Ruby es que no hay una gran diferencia entre “tiempo de compilación” y “tiempo de ejecución.” Es todo lo mismo. Puede agregar código a un proceso en ejecución. Puede volver a definir los métodos sobre la marcha, cambiar su ámbito de public a private, etc. Usted puede incluso alterar los tipos básicos, como Class y Object. Una vez que uno se acostumbra a esta flexibilidad, es difícil volver a un lenguaje estático como C++ o incluso a un lenguaje medio estático como Java.
Pero, ¿por qué querría uno hacer eso?
Referencia de Librería Ruby Clases y Módulos Integrados Este capítulo documenta las clases y módulos integrados en el estándar del lenguaje Ruby. Están disponibles para todos los programas de Ruby automáticamente sin necesidad de require. Esta sección no contiene las diversas variables y constantes predefinidas, las cuales se enumeraron anteriormente.
Más adelante se muestran las invocaciones de ejemplo para cada método.
new String.new( some_string ) → new_string Esta descripción muestra un método de clase que se invoca como String.new. El parámetro en cursiva indica que se pasa una sola cadena, y la flecha indica que se devuelve otra cadena desde el método. Como este valor de retorno tiene un nombre diferente que el del parámetro, representa un objeto diferente. En la ilustración de los métodos de instancia, se muestra un ejemplo de llamada con un nombre de objeto ficticio en cursiva, como el receptor.
each
str.each( sep=$/ ) {| record | block } → str
El parámetro de String#each ha demostrado tener un valor por defecto. Si se llama a each sin parámetros, se utilizará el valor de $/ . Este método es un iterador, por lo que la llamada es seguida por un bloque. String#each devuelve su receptor, por lo que el nombre del mismo (str en este caso) aparece de nuevo después de la flecha. Algunos métodos tienen parámetros opcionales. Se muestran estos parámetros entre paréntesis angulares, < xxx >. (Además, se usa la notación < xxx > * para indicar cero o más ocurrencias de xxx, y se usa < xxx > + para indicar una o más ocurrencias de xxx).
287
index
self.index( str < , offset > ) → pos o nil
Por último, para métodos que se pueden llamar de varias formas diferentes, se lista cada forma en una línea separada. Clase
Array < Object Las matrices son colecciones ordenadas de cualquier objeto indexadas con enteros. L ndexación de un array empieza en 0, como en C o Java. Un índice negativo se asume que es relativo al final de la matriz, es decir, un índice de -1 indica el último elemento de la matriz, -2 es el siguiente al último elemento de la matriz y así sucesivamente. Se mezcla en
Enumerable:
all?, any?, collect, detect, each_with_index, entries, find, find_all, grep, include?, inject, map, max, member? , min, partition, reject, select, sort, sort_ by, to_a, zip Métodos de Clase
[ ]
Array[ < obj >* ]→ una_matriz
Devuelve una matriz nueva llenada con los objetos dados. Equivalente a la forma del operador Array. [](...) Array.[]( 1, ‘a’, /^A/ ) Array[ 1, ‘a’, /^A/ ] [ 1, ‘a’, /^A/ ]
new
-> -> ->
[1, “a”, /^A/] [1, “a”, /^A/] [1, “a”, /^A/]
Array.new→ Array.new ( size=0, obj=nil )→ Array.new( array )→ Array.new( size ) {| i | block } →
una_matriz una_matriz una_matriz una_matriz
Devuelve una matriz nueva. En la primera forma, la nueva matriz está vacía. En el segunda, se crea con copias tamaño size de obj (es decir, las referencias de tamaño al mismo obj). En la tercera forma se crea una copia de la matriz pasada como parámetro (la matriz se genera llamando a to_ary en el parámetro). En la última forma, se crea una matriz del tamaño dado. Cada elemento de este vector se calcula pasando el índice del elemento al bloque dado y almacenando el valor de retorno. Array.new Array.new(2) Array.new(5, “A”)
-> -> ->
[] [nil, nil] [“A”, “A”, “A”, “A”, “A”]
# sólo se creauna instancia del objeto por defecto a = Array.new(2, Hash.new) a[0][‘cat’] = ‘feline’ a -> [{“cat”=>”feline”}, {“cat”=>”feline”}] a[1][‘cat’] = ‘Felix’ a -> [{“cat”=>”Felix”}, {“cat”=>”Felix”}] a = Array.new(2) { Hash.new } # varias instancias a[0][‘cat’] = ‘feline’
288
a
->
[{“cat”=>”feline”}, {}]
squares = Array.new(5) {|i| i*i} squares -> [0, 1, 4, 9, 16] copy = Array.new(squares) # inicializado por copia squares[5] = 25 squares -> [0, 1, 4, 9, 16, 25] copy -> [0, 1, 4, 9, 16] Métodos de Instancia
&
arr & otra_matriz → una_matriz
Intersección de conjuntos --Devuelve una nueva matriz que contiene elementos comunes a las dos matrices, sin duplicados. Las reglas para la comparación de los elementos son los mismas que para las claves hash. Si se necesita un comportamiento similar a los conjuntos, ver la librería de la clase Set. [ 1, 1, 3, 5 ] & [ 1, 2, 3 ]
->
[1, 3]
* arr * str → una_cadena
arr * int → una_matriz
Repetición --Con un argumento responde a to_str, equivalente a arr.join (str). De otra manera, devuelve una nueva matriz construida mediante la concatenación de int copias a arr. [ 1, 2, 3 ] * 3 [ 1, 2, 3 ] * “--”
-> ->
[1, 2, 3, 1, 2, 3, 1, 2, 3] “1--2--3”
+ arr + otra_matriz → una_matriz Concatenación --Devuelve una nueva matriz construida mediante la concatenación de las dos matrices para producir una tercera matriz. [ 1, 2, 3 ] + [ 4, 5 ]
->
[1, 2, 3, 4, 5]
– arr - otra_matriz → una_matriz Diferencia --Devuelve una nueva matriz que es una copia de la matriz original, eliminando todos los elementos que también aparecen en otra_matriz. Si se necesita un comportamiento similar a los conjuntos, ver la librería de la clase Set. [ 1, 1, 2, 2, 3, 3, 4, 5 ] - [1, 2, 4 ]
->
[3, 3, 5]
<< arr << obj → arr Adición --Empuja el objeto dado al final de la matriz. Esta expresión devuelve la propia matriz, por lo varias adiciones pueden ser encadenadas juntos. Véase también Array#push. [ 1, 2 ] << “c” << “d” << [ 3, 4 ]
<=>
->
[1, 2, “c”, “d”, [3, 4]]
arr <=> otra_matriz → −1, 0, +1
Comparación --Devuelve un entero -1, 0 o +1 si la matriz es menor, igual o mayor que otra_matriz. Cada objeto en cada matriz es comparado (utilizando <=>). Si algún valor no es igual, entonces esta desigualdad es el valor de retorno. Si todos los valores encontrados son iguales, entonces el retorno se basa en una comparación de las longitudes de matriz. De este modo, dos matrices son “iguales”, de acuerdo a Array#<=> si y sólo si tienen la misma longitud y el valor de cada elemento es igual al valor del correspondiente elemento de la otra matriz. 289
[ “a”, “a”, “c” ] <=> [ “a”, “b”, “c” ] [ 1, 2, 3, 4, 5, 6 ] <=> [ 1, 2 ]
-> ->
==
-1 1
arr == obj → true o false
Igualdad --Dos matrices son iguales si contienen el mismo número de elementos y si cada elemento es igual a (de acuerdo a Object#==) el correspondiente elemento de la otra matriz. Si obj no es una matriz, trata de convertirlo utilizando to_ary y retorna obj==arr. [ “a”, “c” ] == [ “a”, “c”, 7 ] [ “a”, “c”, 7 ] == [ “a”, “c”, 7 ] [ “a”, “c”, 7 ] == [ “a”, “d”, “f” ]
[ ]
-> -> ->
false true false
arr[int] → obj o nil arr[start, length] → una_matriz o nil arr[range] → una_matriz o nil
Referencia a Elemento --Devuelve el elemento situado int en el índice int, devuelve una submatriz comenzando en el índice start y continuando hasta length elementos, o devuelve una submatriz especificado por range. Los índices negativos cuentan hacia atrás desde el final de la matriz (-1 es el último elemento). Devuelve nil si el índice del primer elemento seleccionado es mayor que el tamaño de la matriz. Si el índice de inicio es igual al tamaño de la matriz y se dá un parámetro length o range, se devuelve una matriz vacía. Equivalente a Array#slice. a = [ “a”, “b”, “c”, a[2] + a[0] + a[1] a[6] a[1, 2] a[1..3] a[4..7] a[6..10] a[-3, 3]
“d”, -> -> -> -> -> -> ->
“e” ] “cab” nil [“b”, “c”] [“b”, “c”, “d”] [“e”] nil [“c”, “d”, “e”]
# casos especiales a[5] -> nil a[5, 1] -> [] a[5..10] -> []
[ ]=
arr[int] = obj → obj arr[start, length] = obj → obj arr[range] = obj → obj
Asignación de elementos --Establece el elemento en el índice int, sustituye a una submatriz comenzando en el índice start y continuando hasta length elementos, o reemplaza a una submatriz especificada por range. Si int es mayor que la capacidad actual de la matriz, la matriz crece de forma automática. Un int negativo cuenta hacia atrás desde el final de la matriz. Inserta los elementos, si la longitud es igual a cero. Si obj es nil, borra elementos de arr. Si obj es una matriz, la forma con índice único insertará esta matriz en arr y las formas con una longitud o con un rango reemplazará los elementos dados en arr con el contenido de la matriz. Se lanza un IndexError si los índices negativos van más allá del principio de la matriz. Véase también Array#push y Array#unshift. a = Array.new a[4] = “4”; a a[0] = [ 1, 2, 3 ]; a a[0, 3] = [ ‘a’, ‘b’, ‘c’ ]; a a[1..2] = [ 1, 2 ]; a a[0, 2] = “?”; a a[0..2] = “A”; a
-> -> -> -> -> -> ->
290
[] [nil, nil, nil, nil, “4”] [[1, 2, 3], nil, nil, nil, “4”] [“a”, “b”, “c”, nil, “4”] [“a”, 1, 2, nil, “4”] [“?”, 2, nil, “4”] [“A”, “4”]
a[-1] = “Z”; a[1..-1] = nil;
a a
-> ->
|
[“A”, “Z”] [“A”]
arr | otra_matriz → una_matriz
Unión --Devuelve una matriz nueva de la unión de una matriz con otra_matriz, eliminando duplicados. Las reglas para la comparación de los elementos son las mismas que para las claves hash. Si se necesita un comportamiento similar a los conjuntos, ver la librería de la clase Set. [ “a”, “b”, “c” ] | [ “c”, “d”, “a” ]
assoc
->
[“a”, “b”, “c”, “d”]
arr.assoc( obj ) → una_matriz o nil
Busca en una matriz cuyos elementos son también matrices, comparando obj con el primer elemento de cada matriz contenida utiliznado obj.==. Devuelve la primera matriz contenida que coincida (es decir, la primera matriz associada) o nil si no hay coincidencia. Véase también Array#rassoc. s1 = [ “colors”, “red”, “blue”, “green” ] s2 = [ “letters”, “a”, “b”, “c” ] s3 = “foo” a = [ s1, s2, s3 ] a.assoc(“letters”) -> [“letters”, “a”, “b”, “c”] a.assoc(“foo”) -> nil
at
arr.at( int ) → obj o nil
Devuelve el elemento en el índice int. Un índice negativo cuenta desde el final de arr. Devuelve nil si el índice está fuera de rango. Véase también Array#[]. (Array#at es ligeramente más rápido que el Array#[], ya que no acepta rangos, etc.) a = [ “a”, “b”, “c”, “d”, “e” ] a.at(0) -> “a” a.at(-1) -> “e”
clear
arr.clear → arr
Elimina todos los elementos de arr.
a = [ “a”, “b”, “c”, “d”, “e” ] a.clear -> []
collect!
arr.collect! {| obj | block } → arr
Invoca block una vez para cada elemento de arr, sustituyendo el elemento con el valor devuelto por el bloque. Véase también Enumerable#collect. a = [ “a”, “b”, “c”, “d” ] a.collect! {|x| x + “!” } -> a ->
[“a!”, “b!”, “c!”, “d!”] [“a!”, “b!”, “c!”, “d!”]
compact
arr.compact → una_matriz
Devuelve una copia de arr con todos los elementos nil eliminados.
[ “a”, nil, “b”, nil, “c”, nil ].compact
compact!
->
[“a”, “b”, “c”]
arr.compact! → arr o nil
Elimina los elementos nil de arr. Retorna nil si no se hicieron cambios.
291
[ “a”, nil, “b”, nil, “c” ].compact! [ “a”, “b”, “c” ].compact!
-> ->
concat
[“a”, “b”, “c”] nil
arr.concat( otra_matriz ) → arr
Añade los elementos de otra_matriz en arr.
[ “a”, “b” ].concat( [“c”, “d”] )
delete
->
[“a”, “b”, “c”, “d”]
arr.delete( obj ) → obj o nil arr.delete( obj ) { block } → obj o nil
Elimina elementos de arr que son iguales a obj. Si no se encuentra el elemento, devuelve nil. Si se dá el bloque de código opcional, devuelve el resultado del bloque si no se encuentra el elemento. a = [ “a”, “b”, “b”, “b”, “c” ] a.delete(“b”) -> “b” a -> [“a”, “c”] a.delete(“z”) -> nil a.delete(“z”) { “not found” } -> “not found”
delete_at
arr.delete_at( index ) → obj o nil
Elimina el elemento en el índice especificado, devolviendo ese elemento, o nil si el índice está fuera de rango. Véase también Array#slice!. a = %w( ant bat cat dog ) a.delete_at(2) -> “cat” a -> [“ant”, “bat”, “dog”] a.delete_at(99) -> nil
delete_if
arr.delete_if {| item | block } → arr
Elimina todos los elementos de arr para los cuáles block evalúa true.
a = [ “a”, “b”, “c” ] a.delete_if {|x| x >= “b” }
->
each
[“a”]
arr.each {| item | block } → arr
Llamadas a block una vez por cada elemento de arr, pasando ese elemento como un parámetro.
a = [ “a”, “b”, “c” ] a.each {|x| print x, “ -- “} produce: a -- b -- c
each_index
arr.each_index {| index | block } → arr
Lo mismo que Array#each uno, pero pasa el índice del elemento en lugar del propio elemento.
a = [ “a”, “b”, “c” ] a.each_index {|x| print x, “ -- ”} produce:
292
1 -- 2 -- 3
empty?
arr.empty? → true o false
Devuelve true si la matriz arr no contiene elementos. [].empty? [ 1, 2, 3 ].empty?
-> ->
true false
eql?
arr.eql?( otro ) → true o false
Devuelve true si arr y otro son el mismo objeto o si otro es un objeto de la clase Array con la misma longitud y contenido que arr. Los elementos de cada matriz se comparan utilizando Object#eql?.Véase también Array#<=>. [ “a”, “b”, “c” ].eql?([“a”, “b”, “c”]) [ “a”, “b”, “c” ].eql?([“a”, “b”]) [ “a”, “b”, “c” ].eql?([“b”, “c”, “d”])
-> -> ->
true false false
fetch arr.fetch( index ) → obj arr.fetch( index, default ) → obj arr.fetch( index ) {| i | block } → obj Intenta devolver el elemento en la posición index. Si el índice está fuera de la matriz, la primera forma produce una excepción IndexError, la segunda forma devuelve default, y la tercera forma devuelve el valor de la invocación del bloque, al que se le pasa el índice. Los valores negativos del índice cuentan desde el final de la matriz. a = [ 11, 22, 33, 44 ] a.fetch(1) a.fetch(-1) a.fetch(-1, ‘cat’) a.fetch(4, ‘cat’) a.fetch(4) {|i| i*i }
fill
-> -> -> -> ->
22 44 44 “cat” 16
arr.fill( obj arr.fill( obj, start < , length > arr.fill( obj, range arr.fill {| i | block arr.fill( start < , length > ) {| i | block arr.fill( range ) {| i | block
) ) ) } } }
→ → → → → →
arr arr arr arr arr arr
Las tres primeras formas establecen los elementos seleccionados de arr (que puede ser toda la matriz) para obj. Un start nil es equivalente a cero. Una longitud nil es equivalente a arr.length. Las tres últimas formas llenan la matriz con el valor del bloque, al que se le pasa el índice absoluto de cada elemento por cubrir. a = [ “a”, “b”, “c”, “d” ] a.fill(“x”) -> [“x”, “x”, “x”, “x”] a.fill(“z”, 2, 2) -> [“x”, “x”, “z”, “z”] a.fill(“y”, 0..1) -> [“y”, “y”, “z”, “z”] a.fill {|i| i*i} -> [0, 1, 4, 9] a.fill(-3) {|i| i+100} -> [0, 101, 102, 103]
first
arr.first → obj o nil arr.first( count ) → una_matriz
Devuelve el primer elemento o los primeros elementos del recuento de arr. Si la matriz está vacía, la
293
primera forma retorna nil y la segunda devuelve una matriz vacía. a = [ “q”, “r”, “s”, “t” ] a.first -> “q” a.first(1) -> [“q”] a.first(3) -> [“q”, “r”, “s”]
flatten
arr.flatten → una_matriz
Devuelve una nueva matriz que es un aplanamiento de una dimensión de esta matriz (recursivamente). Es decir, para cada elemento que es una matriz, extrae sus elementos en la nueva matriz. s = [ 1, 2, 3 ] t = [ 4, 5, 6, [7, 8] ] a = [ s, t, 9, 10 ] a.flatten
-> -> -> ->
[1, 2, 3] [4, 5, 6, [7, 8]] [[1, 2, 3], [4, 5, 6, [7, 8]], 9, 10] [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
flatten!
arr.flatten! → arr o nil
Igual que el Array#flatten, pero modifica el receptor en su lugar. Retorna nil si no se realizaron modificaciones (es decir, arr no contiene submatrices). a = [ 1, 2, a.flatten! a.flatten! a
[3, -> -> ->
[4, 5] ] ] [1, 2, 3, 4, 5] nil [1, 2, 3, 4, 5]
include?
arr.include?( obj ) → true o false
Devuelve true si el objeto dado está presente en arr (es decir, si cualquier objeto == obj), false en caso contrario. a = [ “a”, “b”, “c” ] a.include?(“b”) -> true a.include?(“z”) -> false
indexes arr.indexes( i1, i2, ... iN ) → una_matriz
Obsoleto, utilizar Array#values_at.
indices arr.indices( i1, i2, ... iN ) → una_matriz
Obsoleto, utilizar Array#values_at
arr.insert( index, < obj >+ ) → arr
insert
Si el índice no es negativo, se inserta el valor dado antes del elemento con el índice dado. Si el índice es -1, añade los valores a arr. De lo contrario inserta los valores después del elemento con el índice dado. a = %w{ a b c d } a.insert(2, 99) a.insert(-2, 1, 2, 3) a.insert(-1, “e”)
-> -> ->
[“a”, “b”, 99, “c”, “d”] [“a”, “b”, 99, “c”, 1, 2, 3, “d”] [“a”, “b”, 99, “c”, 1, 2, 3, “d”, “e”]
join
arr.join( separator=$, ) → str
Devuelve una cadena creada mediante la concatenación de cada elemento de la matriz separando cada uno por el separador.
294
[ “a”, “b”, “c” ].join [ “a”, “b”, “c” ].join(“-”)
-> ->
“abc” “a-b-c”
last
arr.last → obj o nil arr.last( count ) → una_matriz
Devuelve el último elemento o elementos últimos del recuento de arr. Si la matriz está vacía, la primera forma devuelve nil, la segunda una matriz vacía. [ “w”, “x”, “y”, “z” ].last [ “w”, “x”, “y”, “z” ].last(1) [ “w”, “x”, “y”, “z” ].last(3)
-> -> ->
“z” [“z”] [“x”, “y”, “z”]
length
arr.length → int
Devuelve el número de elementos de arr. Véase también Array#nitems.
[ 1, nil, 3, nil, 5 ].length
->
5
map!
arr.map! {| obj | block } → arr
Sinónimo de Array#collect!.
nitems
arr.nitems → int
Devuelve el número de elementos no nil de arr. Vease también Array#length.
[ 1, nil, 3, nil, 5 ].nitems
pack
->
3
arr.pack ( template ) → binary_string
Empaqueta los contenidos de arr en una secuencia binaria de acuerdo a las directivas en template (ver la Tabla 9 en la página siguiente). Las directivas A, a y Z puede ser seguido por un recuento, que da el ancho del campo resultante. El resto de directivas también pueden tomar un conteo, indicando el número de elementos a convertir de la matriz. Si el contador es un asterisco (*), se convertirán todos los elementos restantes de la matriz. Cualquiera de las directivas “sSiIlL” pueden ser seguida por un guión bajo (_) para utilizar el tamaño nativo de la plataforma subyacente para el tipo especificado. De otra manera, utilizará una plataforma independiente del tamaño. Los espacios son ignorados en la cadena de plantilla. Los comentarios comenzando con # al siguiente salto de línea o al final de la cadena también se ignoran. Véase también String#unpack. a = [ “a”, “b”, “c” ] n = [ 65, 66, 67 ] a.pack(“A3A3A3”) -> a.pack(“a3a3a3”) -> n.pack(“ccc”) ->
“a̺̺b̺̺c ” “a\000\000b\000\000c\000\000” “ABC”
pop
arr.pop → obj o nil
Elimina el último elemento de arr y lo devuelve, o retorna nil si la matriz está vacía.
a = [ “a”, “m”, “z” ] a.pop -> “z” a -> [“a”, “m”]
295
1 Los octetos de un entero BER-comprimido representan un entero sin signo en base de 128, con el dígito más significativo en primer lugar y con tan pocos dígitos como sea posible. Se establece el octavo bit (el bit alto) en cada byte, excepto el último (Self-Describing Binary Data Representation, MacLeod)
arr.push( < obj >* ) → arr
push
Añade el argumento o argumentos dados a arr.
a = [ “a”, “b”, “c” ] a.push(“d”, “e”, “f”)
rassoc
->
[“a”, “b”, “c”, “d”, “e”, “f”]
arr.rassoc( key ) → una_matriz o nil
Busca en la matriz elementos que son también matrices. Compara key con el segundo elemento de cada matriz contenida con ==. Devuelve la primera matriz contenida que coincide. Véase también Array#assoc.
296
a = [ [ 1, “one”], [2, “two”], [3, “three”], [“ii”, “two”] ] a.rassoc(“two”) -> [2, “two”] a.rassoc(“four”) -> nil
reject!
arr.reject! { block } item → arr o nil
Equivalente a Array#delete_if, pero retorna nil si no se hicieron cambios. Ver también Enumerable#reject.
replace
arr.replace( otra_matriz ) → arr
Reemplaza el contenido de arr con los contenidos de otra_matriz, truncando o ampliando si es necesario.
a = [ “a”, “b”, “c”, “d”, “e” ] a.replace([ “x”, “y”, “z” ]) -> a ->
[“x”, “y”, “z”] [“x”, “y”, “z”]
reverse
arr.reverse → una_matriz
Devuelve una nueva matriz con los elementos de arr en orden inverso.
[ “a”, “b”, “c” ].reverse [ 1 ].reverse
-> ->
[“c”, “b”, “a”] [1]
reverse!
arr.reverse! → arr
Invierte arr. a = [ “a”, “b”, a.reverse! a [ 1 ].reverse!
“c” ] -> [“c”, “b”, “a”] -> [“c”, “b”, “a”] -> [1]
reverse_each
arr.reverse_each {| item | block } → arr
Lo mismo que Array#each, pero atraviesa arr en orden inverso.
a = [ “a”, “b”, “c” ] a.reverse_each {|x| print x, “ “ } produce: c b a
rindex
arr.rindex( obj ) → int o nil
Devuelve el índice del último objeto en arr, tal que objeto == obj. Devuelve nil si no hay coincidencia.
a = [ “a”, “b”, “b”, “b”, “c” ] a.rindex(“b”) -> 3 a.rindex(“z”) -> nil
shift
arr.shift → obj o nil
Devuelve el primer elemento de arr y lo elimina (desplazando una posición atrás todos los demás elementos). Devuelve nil si la matriz está vacía. args = [ “-m”, “-q”, “filename” ] args.shift -> “m” args -> [“q”, “filename”]
297
size
arr.size → int
Sinónimo de Array#length.
slice
arr.slice( int ) → obj arr.slice( start, length ) → una_matriz arr.slice( range ) → una_matriz
Sinónimo de Array#[ ].
a = [ “a”, “b”, “c”, “d”, “e” ] a.slice(2) + a.slice(0) + a.slice(1) a.slice(6) a.slice(1, 2) a.slice(1..3) a.slice(4..7) a.slice(6..10) a.slice(-3, 3) # casos especiales a.slice(5) a.slice(5, 1) a.slice(5..10)
slice!
-> -> -> -> -> -> ->
“cab” nil [“b”, “c”] [“b”, “c”, “d”] [“e”] nil [“c”, “d”, “e”]
-> -> ->
nil [] []
arr.slice!( int ) → obj o nil arr.slice!( start, length ) → una_matriz o nil arr.slice!( range ) → una_matriz o nil
Elimina el elemento o elementos dados con un índice (de manera opcional con una longitud) o con un rango. Devuelve el objeto eliminado, submatriz o nil si el índice está fuera de rango. Equivalente a def slice!(*args) result = self[*args] self[*args] = nil result end a = [ “a”, “b”, “c” ] a.slice!(1) -> “b” a -> [“a”, “c”] a.slice!(-1) -> “c” a -> [“a”] a.slice!(100) -> nil a -> [“a”]
sort arr.sort → una_matriz arr.sort {| a,b | block } → una_matriz Devuelve una matriz nueva creada por la ordenación de arr. Las comparaciones de la ordenación se llevarán a cabo utilizando el operador <=> operador o utilizando un bloque de código opcional. El bloque implementa una comparación entre a y b, devolviendo -1, 0 o +1. Véase también Enumerable#sort_by. a = [ “d”, “a”, “e”, “c”, “b” ] a.sort -> [“a”, “b”, “c”, “d”, “e”] a.sort {|x,y| y <=> x } -> [“e”, “d”, “c”, “b”, “a”]
sort!
arr.sort!→ arr arr.sort! {| a,b | block } → arr
298
Ordena arr, que queda congelada mientras una ordenación está en curso. a = [ “d”, “a”, “e”, “c”, “b” ] a.sort! -> [“a”, “b”, “c”, “d”, “e”] a -> [“a”, “b”, “c”, “d”, “e”]
to_a
arr.to_a → arr array_subclass.to_a → array
Si arr es una matriz, devuelve arr. Si arr es una subclase de Array, invoca to_ary, y usa el resultado para crear un nuevo objeto matriz.
to_ary
arr.to_ary → arr
Devuelve arr.
to_s
arr.to_s → str
Devuelve arr.join. [ “a”, “e”, “i”, “o” ].to_s
->
“aeio”
transpose
arr.transpose → una_matriz
Asume que arr es una matriz de matrices y transpone las filas y columnas.
a = [[1,2], [3,4], [5,6]] a.transpose -> [[1, 3, 5], [2, 4, 6]]
uniq
arr.uniq → una_matriz
Devuelve una nueva matriz mediante la eliminación de los valores duplicados en arr, donde los duplicados se detectan por comparación utilizando eql?. a = [ “a”, “a”, “b”, “b”, “c” ] a.uniq -> [“a”, “b”, “c”]
uniq!
arr.uniq! → una_matriz o nil
Igual que el Array#uniq, pero modifica el receptor en su lugar. Devuelve nil si no se realizan cambios (es decir, si no se encuentran duplicados). a = [ “a”, “a”, “b”, “b”, “c” ] a.uniq! -> [“a”, “b”, “c”] b = [ “a”, “b”, “c” ] b.uniq! -> nil
arr.unshift( < obj >+ ) → arr
unshift
Antepone el objeto u objetos a arr.
a = [ “b”, “c”, “d” ] a.unshift(“a”) -> a.unshift(1, 2) ->
[“a”, “b”, “c”, “d”] [1, 2, “a”, “b”, “c”, “d”]
299
arr.values_at( < selector >* ) → una_matriz
values_at
Devuelve una matriz que contiene los elementos de arr correspondientes al selector o selectores dados. Los selectores pueden ser índices enteros o rangos. a = %w{ a b c d e f } a.values_at(1, 3, 5) a.values_at(1, 3, 5, 7) a.values_at(-1, -3, -5, -7) a.values_at(1..3, 2...5)
-> -> -> ->
[“b”, [“b”, [“f”, [“b”,
“d”, “d”, “d”, “c”,
“f”] “f”, nil] “b”, nil] “d”, “c”, “d”, “e”]
Clase
Bignum < Integer Los objetos Bignum contienen enteros fuera del rango de Fixnum. Los objetos Bignum se crean automáticamente cuando de otro modo, los cálculos de enteros darían un desbordamiento de Fixnum. Cuando un cálculo en relación a objetos Bignum devuelve un resultado que se ajusta a un Fixnum, el resultado se convierte automáticamente. A los efectos de las operaciones bit a bit y para [], un Bignum se trata como si fuera una cadena de bits de longitud infinita con la representación de complemento a 2. Los valores Fixnum son inmediatos, mientras que los objetos Bignum no --las asignaciones y paso de parámetros trabajan con referencias a objetos, no con los objetos mismos. (...) Clase
Binding < Objeto Los Objetos de la clase Binding encapsulan el contexto de ejecución en algún lugar en particular en el código y conservan este contexto para su futuro uso. Las variables, los métodos, el valor de self y posiblemente un bloque iterador se pueden acceder en este contexto ya que están todos retenidos. Los objetos Binding pueden ser creados usando Kernel#binding y están a disposición de la devolución de llamada de kernel#set_trace_func. Estos objetos de enlace se pueden pasar como el segundo argumento del método Kernel#eval, estableciendo un entorno para la evaluación. class Demo def initialize(n) @secret = n end def get_binding return binding() end end k1 b1 k2 b2
= = = =
Demo.new(99) k1.get_binding Demo.new(-3) k2.get_binding
eval(“@secret”, b1) eval(“@secret”, b2) eval(“@secret”)
-> -> ->
99 3 nil
Los Objetos binding no tienen métodos específicos de clase.
(...)
300
[En el original en inglés, continúa con las demás clases: Bignum, Binding, Class, Compa-
rable, etc, por orden alfabético. Consultar el mismo, ya que como se vé, viene de forma esquemática y con ejemplos por lo que es de fácil comprensión. Para terminar se verán algunos otros temas interesantes (aunque no en su totalidad) y nos remitimos a la versión original en inglés o a la tercera edición en inglés, en la que ya se expone la versión 1.9 de Ruby. Comentar que está previsto que para el 2012 salga la versión 2.0.] Módulo
Comparable Basado en: <=> El mixin Comparable es utilizado por las clases cuyos objetos pueden ser ordenados. La clase debe definir el operador <=>, que compara el receptor con otro objeto, devolviendo -1, 0 o +1 dependiendo de si el receptor es menor, igual o mayor que el otro objeto. Comparable utiliza <=> para implementar los operadores de comparación convencionales (<, <=, ==, >= y >) y también utiliza el método between?. class CompareOnSize include Comparable attr :str def <=>(other) str.length <=> other.str.length end def initialize(str) @str = str end end s1 = CompareOnSize.new(“Z”) s2 = CompareOnSize.new([1,2]) s3 = CompareOnSize.new(“XXX”) s1 < s2 s2.between?(s1, s3) s3.between?(s1, s2) [ s3, s2, s1 ].sort
-> -> -> ->
true true false [“Z”, [1, 2], “XXX”]
Método de Instancia
between?
obj.between?( min, max ) → true o false
Devuelve false si obj <=> min es menor que cero o si obj <=> max es mayor que cero, true si lo contrario. 3.between?(1, 5) 6.between?(1, 5) ‘cat’.between?(‘ant’, ‘dog’) ‘gnu’.between?(‘ant’, ‘dog’)
-> -> -> ->
true false true false
(...) Módulo
Enumerable
Basado en: each, <=>
El mixin Enumerable ofrece clases de colección con varios métodos de recorrido y búsqueda y con la capacidad de ordenar. La clase debe proporcionar un método each, que da los miembros sucesivos de una colección. Si se utiliza Enumerable#max, #min, #sort, o #sort_by, los objetos de la colección
301
también deben implementar el operador <=>, ya que estos métodos están basados en una determinada ordenación entre los miembros de la colección. Métodos de instancia
all?
enum.all? < {| obj | block } > → true o false
Pasa cada elemento de la colección al bloque dado. El método devuelve true si el bloque nunca retorna false o nil. Si no se da el bloque, Ruby añade un bloque implícito {|obj| obj} (donde all? devolverá true sólo si ninguno de los miembros de la colección es false o nil). %w{ ant bear cat}.all? {|word| word.length >= 3} %w{ ant bear cat}.all? {|word| word.length >= 4} [ nil, true, 99 ].all?
any?
-> -> ->
true false false
enum.any? < {| obj | block } > → true o false
Pasa cada elemento de la colección para el bloque dado. El método devuelve true si el bloque siempre retorna un valor que no sea false o nil. Si no se da el bloque, Ruby añade un bloque implícito {|obj| obj} (donde any? devolverá true si al menos uno de los miembros de la colección no es false o nil). %w{ ant bear cat}.any? {|word| word.length >= 3} %w{ ant bear cat}.any? {|word| word.length >= 4} [ nil, true, 99 ].any?
collect
-> -> ->
true true true
enum.collect {| obj | block } → array
Devuelve una nueva matriz que contiene los resultados de la ejecución del bloque una vez por cada elemento en enum. (1..4).collect {|i| i*i } (1..4).collect { “cat” }
detect
-> ->
[1, 4, 9, 16] [“cat”, “cat”, “cat”, “cat”]
enum.detect( ifnone = nil ) {| obj | block } → obj o nil
Pasa cada entrada de enum al bloque. Retorna la primera para la que el bloque es no falso. Devuelve nil si no coincide con objeto, a menos que se de el ifnone proc, en cuyo caso se llama y se retorna su resultado. (1..10).detect {|i| i % 5 == 0 and i % 7 == 0 } (1..100).detect {|i| i % 5 == 0 and i % 7 == 0 } sorry = lambda { “not found” } (1..10).detect(sorry) {|i| i > 50}
each_with_index
-> ->
nil 35
->
“not found”
enum.each_with_index {| obj, i | block } → enum
Llama al bloque con dos argumentos, el elemento y su índice, para cada elemento de enum.
hash = Hash.new %w(cat dog wombat).each_with_index do |item, index| hash[item] = index end hash -> {“cat”=>0, “wombat”=>2, “dog”=>1}
entries enum.entries → array
Sinónimo de Enumerable#to_a.
find
enum.find( ifnone = nil ) {| obj | block } → obj o nil
Sinónimo de Enumerable#detect.
302
find_all
enum.find_all {| obj | block } → array
Devuelve una matriz que contiene todos los elementos de enum para los que el bloque es no falso (ver también Enumerable#reject). (1..10).find_all {|i| i % 3 == 0 }
grep
->
[3, 6, 9]
enum.grep( pattern ) → array enum.grep( pattern ) {| obj | block } → array
Devuelve una matriz de todos los elementos de enum para los que patrón === elemento. Si se suministra el bloque opcional, se le pasa cada elemento coincidente y el resultado del bloque se almacena en la matriz de salida. (1..100).grep 38..44 -> [38, 39, 40, 41, 42, 43, 44] c = IO.constants c.grep(/SEEK/) -> [“SEEK_CUR”, “SEEK_SET”, “SEEK_END”] res = c.grep(/SEEK/) {|v| IO.const_get(v) } res -> [1, 0, 2]
include?
enum.include?( obj ) → true o false
Devuelve true si algún miembro de enum es igual a obj. Se prueba la igualdad usando ==. IO.constants.include? “SEEK_SET” IO.constants.include? “SEEK_NO_FURTHER”
inject
-> ->
true false
enum.inject(initial) {|memo, obj | block } → obj enum.inject {|memo, obj | block } → obj
Combina los elementos de enum mediante la aplicación del bloque a un valor acumulador (memo) y a cada elemento por separado. En cada paso, memo se establece en el valor devuelto por el bloque. La primera forma permite proporcionar un valor inicial para memo. La segunda forma utiliza el primer elemento de la colección como el valor inicial (y se salta ese elemento, mientras itera). # Suma algunos números (5..10).inject {|sum, n| sum + n } # Multiplica algunos números (5..10).inject(1) {|product, n| product * n }
->
45
->
151200
# buscar la palabra más larga longest = %w{ cat sheep bear }.inject do |memo, word| memo.length > word.length ? memo : word end longest -> “sheep” # buscar la longitud de la palabra más larga longest = %w{ cat sheep bear }.inject(0) do |memo, word| memo >= word.length ? memo : word.length end longest -> 5
map
enum.map {| obj | block } → array
Sinónimo de Enumerable#collect.
max
enum.max → obj enum.max {| a,b | block } → obj 303
Devuelve el objeto en enum con el valor máximo. La primera supone que todos los objetos implementan <=>, y la segunda utiliza el bloque para devolver a <=> b. a = %w(albatross dog horse) a.max a.max {|a,b| a.length <=> b.length }
member?
-> ->
“horse” “albatross”
enum.member?( obj ) → true o false
Sinónimo de Enumerable#include?.
min enum.min → obj enum.min {| a,b | block } → obj Devuelve el objeto de enum con el valor mínimo. La primera forma supone que todos los objetos implementan Comparable, y la segunda utiliza el bloque para devolver a <=> b. a = %w(albatross dog horse) a.min a.min {|a,b| a.length <=> b.length }
partition
-> ->
“albatross” “dog”
enum.partition {| obj | block }→[ true_array, false_array ]
Devuelve dos matrices, la primera contiene los elementos de enum para los que el bloque se evalúa como verdadero, la segunda contiene el resto. (1..6).partition {|i| (i&1).zero?}
->
[[2, 4, 6], [1, 3, 5]]
reject enum.reject {| obj | block } → array Devuelve una matriz que contiene los elementos de enum para los que el bloque es falso (véase también Enumerable#find_all). (1..10).reject {|i| i % 3 == 0 }
->
[1, 2, 4, 5, 7, 8, 10]
select enum.select {| obj | block } → array
Sinónimo de Enumerable#find_all.
sort
enum.sort → array enum.sort {| a, b | block } → array
Devuelve una matriz que contiene los elementos de enum ordenados, ya sea de acuerdo a su propio método <=> o mediante el uso de los resultados del bloque suministrado. El bloque debe devolver -1, 0 o +1 en función de la comparación entre a y b. A partir de Ruby 1.8, el método Enumerable#sort_by implementa una Transformada Schwartzian integrada, útil cuando el cálculo o la comparación de clave son costosos. %w(rhea kea flea).sort (1..10).sort {|a,b| b <=> a}
sort_by
-> ->
[“flea”, “kea”, “rhea”] [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
enum.sort_by {| obj | block } → array
Ordena enum, utilizando claves generadas por asignación a los valores de enum, mediante el bloque dado y utilizando el resultado del bloque para la comparación de elementos. sorted = %w{ apple pear fig }.sort_by {|word| word.length}
304
sorted
->
[“fig”, “pear”, “apple”]
Internamente, sort_by genera una matriz de tuplas que contiene la colección original de elementos y el valor asignado. Esto hace de sort_by bastante costoso cuando los conjuntos de claves son simples. require ‘benchmark’ include Benchmark a = (1..100000).map {rand(100000)} bm(10) do |b| b.report(“Sort”) { a.sort } b.report(“Sort by”) { a.sort_by {|a| a} } end produce: user system total real Sort 0.070000 0.010000 0.080000 ( 0.085860) Sort by 1.580000 0.010000 1.590000 ( 1.811626) Sin embargo, hay que tener en cuenta el caso de cuando la comparación de claves es una operación no trivial. El código siguiente ordena algunos archivos por tiempo de modificación utilizando el método básico sort. files = Dir[“*”] sorted = files.sort {|a,b| File.new(a).mtime <=> File.new(b).mtime} sorted
->
[“mon”, “tues”, “wed”, “thurs”]
Esta ordenación es ineficiente: genera dos nuevos objetos File en cada comparación. Una técnica algo mejor es utilizar el método Kernel#test para generar los tiempos de modificación directamente. files = Dir[“*”] sorted = files.sort do |a,b| test(?M, a) <=> test(?M, b) end sorted
->
[“mon”, “tues”, “wed”, “thurs”]
Esto todavía genera muchos objetos Time innecesarios. Una técnica más eficiente es almacenar en caché las claves de ordenación (tiempos de modificación en este caso) antes de la ordenación. Los usuarios de Perl a menudo llaman a este método una transformación Schwartzian, llamado así por Randal Schwartz. Construimos una matriz temporal, donde cada elemento es una matriz que contiene la clave de ordenación, junto con el nombre de archivo. Ordenamos esta matriz y luego se extrae el nombre del archivo a partir del resultado. sorted = Dir[“*”].collect {|f| [test(?M, f), f] }.sort.collect {|f| f[1] } sorted
->
[“mon”, “tues”, “wed”, “thurs”]
Esto es exactamente lo que sort_by hace internamente.
sorted = Dir[“*”].sort_by {|f| test(?M, f)} sorted
->
[“mon”, “tues”, “wed”, “thurs”]
sort_by también puede ser útil para ordenaciones multinivel. Un truco, que se basa en el hecho de que las matrices se comparan elemento por elemento, es hacer que el bloque de devuelva una matriz con
305
cada una de las claves de comparación. Por ejemplo, para ordenar una lista de palabras primero por su longitud y luego por orden alfabético, se podría escribir: words = %w{ puma cat bass ant aardvark gnu fish } sorted = words.sort_by {|w| [w.length, w] } sorted -> [“ant”, “cat”, “gnu”, “bass”, “fish”, “puma”, “aardvark”]
to_a
enum.to_a → array
Devuelve una matriz que contiene los elementos de enum.
(1..7).to_a { ‘a’=>1, ‘b’=>2, ‘c’=>3 }.to_a
-> ->
[1, 2, 3, 4, 5, 6, 7] [[“a”, 1], [“b”, 2], [“c”, 3]]
enum.zip( < arg >+ ) → array
zip
enum.zip( < arg >+ ) {| arr | block } → nil
Convierte los argumentos a matrices y luego fusiona los elementos de enum con los correspondientes elementos de cada argumento. El resultado es una matriz que contiene el mismo número de elementos que enum. Cada elemento es una matriz de n elementos, donde n es uno o más del recuento de los argumentos. Si el tamaño de los argumentos es menor que el número de elementos en enum, se suministran valores nil. Si se da un bloque, se invoca para cada matriz de salida, de otra manera se devuelve una matriz de matrices. a = [ 4, 5, 6 ] b = [ 7, 8, 9 ] (1..3).zip(a, b) “cat\ndog”.zip([1]) (1..3).zip
-> -> ->
[[1, 4, 7], [2, 5, 8], [3, 6, 9]] [[“cat\n”, 1], [“dog”, nil]] [[1], [2], [3]]
Módulo
Errno Los objetos excepción de Ruby son subclases de Exception. Sin embargo, los sistemas operativos suelen reportar errores usando simples enteros. El módulo Errno se crea de forma dinámica para asignar estos errores del sistema operativo a las clases Ruby, con cada número de error generando su propia subclase de SystemCallError. Como la subclase se crea en el módulo Errno, su nombre comenzará con Errno::. Exception StandardError SystemCallError Errno::xxx Los nombres de las clases Errno:: dependerán del entorno en el que se ejecute Ruby. En una plataforma típica de Unix o Windows, se encuentra que tiene clases Errno como Errno::EACCES, Errno::EAGAIN, Errno::EINTR, etc. El número entero de error del sistema operativo correspondiente a un particular error está disponible en la constante de clase Errno::error::Errno. Errno::EACCES::Errno Errno::EAGAIN::Errno Errno::EINTR::Errno
-> -> ->
13 35 4
La lista completa de errores del sistema operativo en su plataforma está disponible como las constantes
306
de Errno. Cualquier definición de usuario de excepción en este módulo (incluyendo las subclases de las excepciones existentes) también deben definir una constante Errno. Errno.constants -> E2BIG, EACCES, EADDRINUSE, EADDRNOTAVAIL, EAFNOSUPPORT, EAGAIN, EALREADY, ... A partir de Ruby 1.8, las excepciones son comparadas en las cláusulas rescue usando Module#===. El método === se anula para SystemCallError de la clase a fin de comparar en función del valor Errno. Así, si dos clases Errno distintas tienen el mismo valor Errno subyacente, serán tratadas como la misma excepción por una cláusula rescue. Clase
Exception < Object Los descendientes de la clase Exception se utilizan para la comunicación entre los métodos raise y las declaraciones rescue en los bloques begin/end. Los objetos Exception contienen información acerca de la excepción --el tipo (nombre de la clase de la excepción), una cadena descriptiva opcional e información de rastreo opcional. La librería estándar define las excepciones que se muestran en la Figura 21 en la página siguiente. Véase también la descripción de Errno en la página anterior. Métodos de Clase
exception
Exception.exception( < message > ) → exc
Crea y devuelve un nuevo objeto excepción, opcionalmente se puede establecer un mensaje en mesagge.
new
Exception.new( < message > ) → exc
Crea y devuelve un nuevo objeto excepción, opcionalmente se puede establecer un mensaje en mesagge.
Métodos de Instancia
backtrace
exc.backtrace → array
Devuelve cualquier traza asociada a la excepción. La traza es una matriz de cadenas, cada una conteniendo ya sea filename:linea: en ‘metodo’ o filename:linea. def a raise “boom” end def b a() end begin b() rescue => detail print detail.backtrace.join(“\n”) end produce: prog.rb:2:in `a’ prog.rb:6:in `b’ prog.rb:10
exception
exc.exception( < message > ) → exc o exception 307
Sin argumento, devuelve el receptor. De lo contrario, crea un nuevo objeto excepción de la misma clase que el receptor pero con un mensaje diferente.
message
exc.message → msg
Devuelve el mensaje asociado a esa excepción.
set_backtrace
exc.set_backtrace( array ) → array
Establece la información de backtrace asociada con exc. El argumento debe ser un array de objetos String con el formato descrito en Exception#backtrace.
status
exc.status → status
(SystemExit solamente) Devuelve el estado de salida asociado a esa excepción SystemExit. Normalmente, esta situación se configura con Kernel#exit. begin exit(99) rescue SystemExit => e puts “Exit status is: #{e.status}” end produce:
308
Exit status is: 99
success?
exc.success? → true o false
(SystemExit solamente) Devuelve true si el estado de salida es nil o cero. begin exit(99) rescue SystemExit => e print “This program “ if e.success? print “did” else print “did not” end puts “ succeed” end produce: This program did not succeed
to_s
exc.to_s → msg
Devuleve el mensaje asociado con esa excepción (o el nombre de la excepción si no tiene mensaje establecido). begin raise “The message” rescue Exception => e puts e.to_s end produce: The message
to_str
exc.to_str → msg
Devuleve el mensaje asociado con esa excepción (o el nombre de la excepción si no tiene mensaje establecido). Implementar to_str da a las excepciones funcionamiento de cadena. Clase
File < IO Un File es una abstracción de cualquier objeto fichero accesible en el programa y está estrechamente relacionada con la clase IO. File incluye los métodos del módulo FileTest como métodos de clase, lo que permite escribir (por ejemplo) File.exist?(“foo”). Los bits de permiso son un conjunto de bits, específico de la plataforma, que indican los permisos de un archivo. En sistemas basados en Unix, los permisos son vistos como un conjunto de tres octetos, para el propietario, para el grupo y para el resto del mundo. Para cada una de estas entidades, los permisos se pueden configurar para leer, escribir o ejecutar el archivo.
309
Los bits de permiso en 0644 (en octal), podrían interpretarse como lectura / escritura para el propietario y de sólo lectura para el grupo y otros. Los bits superiores también pueden ser utilizados para indicar el tipo de archivo (normal, directorio, pipe, socket, etc) y varias otras características especiales. Si los permisos son para un directorio, el significado del bit de ejecución cambia y cuando se establece, se puede buscar en el directorio. Cada archivo tiene tres tiempos asociados. El atime es el momento del último acceso. El ctime es el momento en que el estado del archivo (no necesariamente el contenido del archivo) se modificó por última vez. Por último, el mtime es el momento en que los datos del archivo se modificaron por última vez. En Ruby, todos estos tiempos se devuelven como objetos Time. En los sistemas operativos no POSIX, sólo hay la posibilidad de hacer un archivo de sólo lectura, o de lectura / escritura. En este caso, los bits de permisos restantes serán sintetizados para parecerse a los valores típicos. Por ejemplo, en Windows los bits de permisos por defecto son 0644, lo que significa que son de lectura / escritura para el propietario y de sólo lectura para todos los demás. El único cambio que se puede hacer es pasar el archivo a sólo lectura, que se presenta como 0444.
Véase también Pathname.
Módulo
FileTest FileTest implementa las operaciones de prueba de archivo similares a las que se utilizan en File::Stat. Los métodos de FileTest están duplicados en la clase File. Aquí sólo vamos a listar los nombres de los métodos. FileTest parece un módulo un poco rudimentario.
Los métodos de FileTest son:
blockdev?, chardev?, directory?, executable?, executable_real?, exist?, exists?, file?, grpowned?, owned?, pipe?, readable?, readable_real?, setgid?, setuid?, size, size?, socket?, sticky?, symlink?, world_readable?, world_writable?, writable?, writable_real? y zero? Clase
IO < Objeto Subclases: File La clase IO es la base para todas las entradas y salidas en Ruby. Un flujo de E/S puede ser dúplex (es decir, bidireccional) y por lo tanto puede usar más de un flujo del sistema operativo nativo.
La única subclase estándar de IO es File. Las dos clases están estrechamente relacionadas.
La clase IO utiliza la abstracción Unix de descriptores de fichero (fd), enteros pequeños que representan los archivos abiertos. Convencionalmente, la entrada estándar tiene un fd de 0, la salida estándar un fd de 1 y el error estándar un fd de 2. Ruby convierte si es posible los nombres de las rutas entre las diferentes convenciones de los sistemas operativos. Por ejemplo, en un sistema Windows el nombre de archivo /gumby/ruby/test.rb se abrirá como \gumby\ruby\test.rb. Cuando se especifica un nombre de archivo al estilo Windows en una cadena Ruby entre comillas dobles, hay que acordarse de escapar las barras invertidas.
“c:\\gumby\\ruby\\test.rb”
Se puede utilizar File::SEPARATOR para obtener el carácter separador de la plataforma específica.
Los puertos de E/S pueden ser abierto en cualquiera de varios modos diferentes, que se muestran en esta sección como una cadena de modo. Esta cadena de modo debe ser uno de los valores listados en la tabla siguiente:
310
Modo
Significado
r Sólo lectura. Se inicia al comienzo del archivo (modo predeterminado). r+ Lectura / escritura. Se inicia al comienzo del archivo. w Sólo escritura. Trunca un archivo existente con longitud cero o crea un nuevo archivo para escritura. a Sólo escritura. Se inicia al final del archivo si este existe, o de otro modo crea un nuevo archivo para escritura. a+ Lectura / escritura. Se inicia al final del archivo si este existe, o de otro modo crea un nuevo archivo para lectura / escritura. b (sólo DOS / Windows) Modo fichero binario (puede aparecer con cualquiera de las letras clave listadas arriba. Módulo
Marshal La biblioteca de serialización (marshaling library) convierte colecciones de objetos Ruby en un flujo de bytes, lo que les permite ser almacenados fuera del script que está activo. Estos datos posteriormente pueden ser leídos y reconstituir los objetos originales. Véase también la biblioteca YAML. Los datos serializados tienen un número de versión mayor y menor almacenados junto con la información del objeto. En uso normal, el serializado puede cargar datos únicamente escritos con el mismo número de versión mayor y un número de versión menor igual o inferior. Si la bandera Ruby “verbose” está activa (normalmente con -d, -v, -w o --verbose), los números mayores y menores deben coincidir exactamente. La versión de serialización es independiente de los números de versión de Ruby. Puede extraer la versión por la lectura de los dos primeros bytes de los datos serializados. str = Marshal.dump(“thing”) RUBY_VERSION -> “1.8.2” str[0] -> 4 str[1] -> 8 Algunos objetos no pueden ser objeto de volcado: si los objetos a ser objeto de volcado incluyen enlaces, objetos de procedimiento o método, instancias de la clase IO, objetos singleton, o si se trata de volcar clases o módulos anónimos, se lanzará un TypeError. Si su clase tiene necesidades especiales de serialización (por ejemplo, si se desea serializar en un formato específico), o si contiene objetos que de otro modo no son serializables, puede implementar su propia estrategia de serialización. Antes de Ruby 1.8, se definen los métodos _dump y _load. Ruby 1.8 incluye una interfaz más flexible para la serialización personalizada con los métodos de instancia marshal_dump y marshal_load: Si un objeto que se va a serializar responde a marshal_ dump, se llama a este método en lugar de a _dump. marshal_dump puede devolver un objeto de cualquier clase (no sólo String). Una clase que implementa marshal_dump también deben implementar marshal_load, que es llamado como un método de instancia de un objeto recién asignado y se le pasa el objeto original creado por marshal_dump. El siguiente código utiliza este nuevo marco para almacenar un objeto Time en la versión serializada de un objeto. Cuando está cargado, este objeto se pasa a marshal_load, que convierte este time a una forma imprimible, almacenando el resultado en una variable de instancia. class TimedDump attr_reader :when_dumped def marshal_dump Time.now end def marshal_load(when_dumped) @when_dumped = when_dumped.strftime(“%I:%M%p”) end end
311
t = TimedDump.new t.when_dumped
->
nil
str = Marshal.dump(t) newt = Marshal.load(str) newt.when_dumped -> “10:38PM”
Constantes de Módulo MAJOR_VERSION MINOR_VERSION Métodos de Módulo
dump
dump( obj < , io > , limit=–1 ) → io
Serializa obj y todos los objetos descendientes. Si se especifica io, los datos serializados se escribirán en él, de lo contrario los datos se devuelven como un String. Si se especifica limit, el recorrido de subobjetosse limitará a esa profundidad. Si el límite es negativo, no hay comprobación de profundidad. class def end def end end
Klass initialize(str) @str = str say_hello @str
o = Klass.new(“hello\n”) data = Marshal.dump(o) obj = Marshal.load(data) obj.say_hello -> “hello\n”
load
load( from < , proc > ) → obj
Devuelve el resultado de la conversión de los datos serializados en from en un objeto Ruby (posiblemente asociados con los objetos subordinados). from puede ser una instancia de IO o un objeto que responde a to_str. Si se especifica proc, es pasado a cada objeto a medida que se deserializa.
restore
restore( from < , proc > ) → obj
Sinónimo de Marshal.load.
Módulo
ObjectSpace El módulo ObjectSpace contiene una serie de rutinas que interactúan con la utilidad de recolección de basura y que permiten recorrer con un iteradortodos los objetos que habitan. ObjectSpace también proporciona soporte para los finalizadores de objetos. Estos son procs que se llaman cuando un objeto específico está a punto de ser destruido por el recolector de basura. include ObjectSpace
312
a, b, c = puts “a’s puts “b’s puts “c’s
“A”, “B”, “C” id is #{a.object_id}” id is #{b.object_id}” id is #{c.object_id}”
define_finalizer(a, lambda {|id| puts “Finalizer one on #{id}” }) define_finalizer(b, lambda {|id| puts “Finalizer two on #{id}” }) define_finalizer(c, lambda {|id| puts “Finalizer three on #{id}” }) produce: a’s id is b’s id is c’s id is Finalizer Finalizer Finalizer
936150 936140 936130 three on 936130 two on 936140 one on 936150
Métodos de Módulo
_id2ref
ObjectSpace._id2ref( object_id ) → obj
Convierte un identificador de objeto a una referencia al objeto. No puede ser llamado en un ID de objeto pasado como parámetro a un finalizador. s = “I am a string” oid = s.object_id r = ObjectSpace._id2ref(oid) r r.equal?(s)
define_finalizer
-> -> -> -> ->
“I am a string” 936550 “I am a string” “I am a string” true
ObjectSpace.define_finalizer( obj, a_proc=proc() )
Añade a_proc como un finalizador, llamado cuando obj está a punto de ser destruido.
each_object
ObjectSpace.each_object( < class_or_mod > ) {| obj | block }→ fixnum
Llama al bloque una vez por cada viviente y no inmediato objeto en ese proceso Ruby. Si se especifica class_or_mod, se llama al bloque sólo para las clases o módulos que coincidan (o sean una subclase) con class_or_mod. Devuelve el número de objetos encontrados. Objetos inmediatos (Fixnums, Symbols, true, false y nil) nunca se devuelven. En el siguiente ejemplo, each_object devuelve tanto los números que se han definido como varias constantes definidas en el módulo Math. a = 102.7 b = 95 # Fixnum: No se retorna c = 12345678987654321 count = ObjectSpace.each_object(Numeric) {|x| p x } puts “Total count: #{count}” produce: 12345678987654321 102.7 2.71828182845905 3.14159265358979 2.22044604925031e16 1.79769313486232e+308 2.2250738585072e308 Total count: 7
313
garbage_collect
ObjectSpace.garbage_collect → nil
Inicia la recolección de basura.
undefine_finalizer
ObjectSpace.undefine_finalizer( obj )
Elimina todos los finalizadores para obj.
Clase
Proc < Objeto Los objetos Proc son bloques de código que se han ligado a un conjunto de variables locales. Una vez ligado, el código puede ser llamado en diferentes contextos y acceder aún a esas variables. def gen_times(factor) return Proc.new {|n| n*factor } end times3 = gen_times(3) times5 = gen_times(5) times3.call(12) times5.call(5) times3.call(times5.call(4))
-> -> ->
36 25 60
Métodos de Clase
new
Proc.new { block } → un_proc Proc.new → un_proc
Crea un objeto Proc nuevo, vinculado al contexto actual. Proc.new puede ser llamado sin un bloque, sólo dentro de un método con un bloque adjunto, en cuyo caso el bloque se convierte en el objeto Proc. def proc_from Proc.new end proc = proc_from { “hello” } proc.call -> “hello”
Métodos de Instancia
[ ]
prc[ < params >* ] → obj
Sinónimo de Proc.call.
==
prc== other → true o false
devuelve true si prc es lo mismo que other.
arity
prc.arity → integer
Devuelve el número de argumentos requeridos por el bloque. Si el bloque se declara para no tomar argumentos, devuelve 0. Si el bloque es conocido por tener exactamente n argumentos, devuelve n. Si el bloque tiene argumentos opcionales, devuelve -(n +1), donde n es el número de argumentos obligatorios. Un proc sin declaraciones de argumento también devuelve -1, ya que puede aceptar (e ignorar) un número arbitrario de parámetros.
314
Proc.new Proc.new Proc.new Proc.new Proc.new Proc.new Proc.new
{}.arity {||}.arity {|a|}.arity {|a,b|}.arity {|a,b,c|}.arity {|*a|}.arity {|a,*b|}.arity
-> -> -> -> -> -> ->
1 0 1 2 3 1 2
En Ruby 1.9, arity se define como el número de parámetros que no se puede ignorar. En 1.8, Proc.new{}.arity devuelve -1, y en 1,9 devuelve 0.
binding
prc.binding → binding
Devuelve el enlace asociado con prc. Hay que tener en cuenta que del Kernel#eval acepta o un Proc o un objeto Binding como segundo parámetro. def fred(param) lambda {} end b = fred(99) eval(“param”, b.binding) eval(“param”, b)
-> ->
99 99
call
prc.call( < params >* ) → obj
Invoca el bloque, estableciendo los parámetros del bloque a los valores en params utilizando alguna semántica como de llamada a método. Devuelve el valor de la última expresión evaluada en el bloque. a_proc = Proc.new {|a, *b| b.collect {|i| i*a }} a_proc.call(9, 1, 2, 3) -> [9, 18, 27] a_proc[9, 1, 2, 3] -> [9, 18, 27] Si el bloque es llamado explícitamente acepta un solo parámetro. call emite un mensaje de advertencia, a menos que se le dé exactamente un parámetro. De otra manera, acepta encantado lo que se le dé, haciendo caso omiso de los parámetros excedentes que se le pasan y establece los parámetros sin establecer del bloque a nil. a_proc = Proc.new {|a| a} a_proc.call(1,2,3) produce: prog.rb:1: warning: multiple values for a block parameter (3 for 1) from prog.rb:2 Si se quiere un bloque para recibir un número arbitrario de argumentos, hay que definirlo para aceptar *args. a_proc = Proc.new {|*a| a} a_proc.call(1,2,3) -> [1, 2, 3] Los bloques creados utilizando Kernel.lambda comprueban que se les llama exactamente con el número correcto de parámetros. p_proc = Proc.new {|a,b| puts “Sum is: #{a + b}” } p_proc.call(1,2,3) p_proc = lambda {|a,b| puts “Sum is: #{a + b}” }
315
p_proc.call(1,2,3) produce: Sum is: 3 prog.rb:3: wrong number of arguments (3 for 2) (ArgumentError) from prog.rb:3:in `call’ from prog.rb:4
to_proc
prc.to_proc → prc
Parte del protocolo para la conversión de objetos a objetos Proc. Las instancias de la clase Proc simplemente lo que devuelven es a si mismas.
to_s
prc.to_s → string
Devuelve una descripción de proc, incluyendo información sobre donde se definió.
def create_proc Proc.new end my_proc = create_proc { “hello” } my_proc.to_s -> “#” Clase
Regexp < Objeto Un Regexp tiene una expresión regular, que se utiliza para comparar un patrón con cadenas. Expresiones regulares son creados usando literales /.../ y %r... y con el constructor Regexp.new. Esta sección documenta expresiones regulares Ruby 1.8. Versiones posteriores de Ruby utilizan un motor para expresiones regulares diferente.
Constantes de Clase EXTENDED IGNORECASE MULTILINE
Ignora espacios y saltos de línea en expresiones regulares. Las comparaciones son case insensitive, es decir, ignora si son mayúsculas o minúsculas. Nueva línea es tratada como cualquier otro carácter.
Métodos de Clase
compile
Regexp.compile( pattern < , options < , lang > > )→ rxp
Sinónimo de Regexp.new.
escape
Regexp.escape( string ) → escaped_string
Escapa cualquier carácter que tenga un significado especial en una expresión regular. Para cualquier cadena, Regexp.escape(str)=~str será verdadero. Regexp.escape(‘\\[]*?{}.’)
->
\\\[\]\*\?\{\}\.
last_match Regexp.last_match → match Regexp.last_match( int ) → string La primera forma devuelve el objeto MatchData generado por la comparación exitosa de patrón anterior. Esto es equivalente a la lectura de la variable global $~. La segunda forma devuelve el enésimo campo de ese objeto MatchData.
316
/c(.)t/ =~ ‘cat’ Regexp.last_match Regexp.last_match(0) Regexp.last_match(1) Regexp.last_match(2)
-> -> -> -> ->
0 # “cat” “a” nil
new Regexp.new( string < , options < , lang > > )→ rxp Regexp.new( regexp ) → new_regexp Construye una nueva expresión regular desde pattern, que puede ser un String o un Regexp. En este último caso las opciones de expresión regular se propagan, y las nuevas opciones no pueden ser especificadas (cambia a partir de Ruby 1.8). Si options es un Fixnum, deben ser uno o más de Regexp::EXTENDED, Regexp::IGNORECASE y Regexp::POSIXLINE, or-ed conjuntamente. De otra manera, si options no es nil, la expresión regular será case insensitive. El parámetro lang habilita el soporte multibyte para la expresión regular: n, N, o nil = none, e, E = EUC, s, S = SJIS, u, U = UTF-8. r1 r2 r3 r4
= = = =
Regexp.new(‘^[a-z]+:\\s+\w+’) Regexp.new(‘cat’, true) Regexp.new(‘dog’, Regexp::EXTENDED) Regexp.new(r2)
quote
-> -> -> ->
/^[a-z]+:\s+\w+/ /cat/i /dog/x /cat/i
Regexp.quote( string )→ escaped_string
Sinónimo de Regexp.escape.
Métodos de Instancia
==
rxp == other_regexp → true o false
Igualdad --Dos expresiones regulares son iguales si sus patrones son idénticos, tienen el mismo código de conjunto de carácteres, y sus valores casefold? son los mismos. /abc/ == /abc/x /abc/ == /abc/i /abc/u == /abc/n
-> -> ->
false false false
===
rxp === string → true o false
Igualdad de caso --Sinónimo de REGEXP#=~ utilizado en las sentencias case.
a = “HELLO” case a when /^[a-z]*$/; when /^[A-Z]*$/; else end
print “Lower case\n” print “Upper case\n” print “Mixed case\n”
produce: Upper case
=~
rxp =~ string → int o nil
Comparación --Compara rxp con string, devolviendo la posición de la coincidencia respecto al inicio de la cadena o nil si la comparación falla. Establece $~ al correspondiente MatchData o nil. /SIT/ =~ “insensitive” /SIT/i =~ “insensitive”
~
-> ->
nil 5
317
~ rxp → int o nil
Comparación --Compara rxp con el contenido de $_. Equivalente a rxp =~ $ _.
$_ = “input data” ~ /at/ -> 7
casefold?
rxp.casefold? → true o false
Devuelve el valor de la bandera case-insensitive.
inspect
rxp.inspect → string
Devuelve una versión legible de rxp.
/cat/mi.inspect /cat/mi.to_s
-> ->
“/cat/mi” “(?mi-x:cat)”
kcode
rxp.kcode → string
Devuelve el código de caracteres de la expresión regular.
/cat/.kcode /cat/s.kcode
-> ->
nil “sjis”
match
rxp.match(string) → match o nil
Devuelve un objeto MatchData describiendo la comparación, es decir, retornando, o nil si no hay coincidencia. Esto equivale a recuperar el valor de la variable especial $~ después de una comparación normal.
/(.)(.)(.)/.match(“abc”)[2]
->
“b”
options
rxp.options → int
Devuelve el conjunto de bits correspondiente a las opciones utilizadas cuando se crea esa Regexp (ver Regexp.new para más detalles). Hay que tener en cuenta que se pueden configurar bits adicionales en las opciones retornadas: estos son utilizados internamente por el código de la expresión regular. Estos bits extras se ignoran si se pasan las opciones a Regexp.new. # Vamos a ver cuáles son los valores ... Regexp::IGNORECASE -> 1 Regexp::EXTENDED -> 2 Regexp::MULTILINE -> 4 /cat/.options /cat/ix.options Regexp.new(‘cat’, true).options Regexp.new(‘cat’, 0, ‘s’).options
-> -> -> ->
0 3 1 48
r = /cat/ix Regexp.new(r.source, r.options)
->
/cat/ix
source
rxp.source → string
Devuelve la cadena original del patrón.
/ab+c/ix.source
to_s
->
“ab+c”
318
rxp.to_s → string
Devuelve una cadena que contiene la expresión regular y sus opciones (utilizando la notación (?xx: yyy)). Esta cadena puede ser alimentada de nuevo a Regexp.new a una expresión regular con la misma semántica que la original. (Sin embargo, Regexp#== No se puede devolver verdadero cuando se comparan las dos, ya que la fuente de la propia expresión regular puede ser diferente, como muestra el ejemplo). Regexp#inspect produce una versión generalmente más legible de rxp. r1 = /ab+c/ix s1 = r1.to_s r2 = Regexp.new(s1) r1 == r2 r1.source r2.source
-> -> -> -> -> ->
/ab+c/ix “(?ix-m:ab+c)” /(?ix-m:ab+c)/ false “ab+c” “(?ix-m:ab+c)”
Módulo
Signal Muchos sistemas operativos permiten enviar señales a los procesos en ejecución. Algunas señales tienen un efecto definido sobre el proceso, y otras pueden ser atrapadas a nivel de código y actuar en consecuencia. Por ejemplo, un proceso puede atrapar la señal USR1 y utilizarla para cambiar la depuración, y puede utilizar TERM para iniciar un apagado controlado. pid = fork do Signal.trap(“USR1”) do $debug = !$debug puts “Debug now: #$debug” end Signal.trap(“TERM”) do puts “Terminating...” shutdown() end # . . . do some work . . . end Process.detach(pid) # Controlling program: Process.kill(“USR1”, pid) # ... Process.kill(“USR1”, pid) # ... Process.kill(“TERM”, pid) produce: Debug now: true Debug now: false Terminating... La lista de los nombres de las señales disponibles y su interpretación depende del sistema. La semántica de envío de señales también puede variar entre sistemas. El envío de una señal en particular no siempre es fiable.
Métodos de Módulo
list
Signal.list → hash
Devuelve una lista de los nombres de las señales con los correspondientes números de señal subyacentes asignados. Signal.list -> {“BUS”=>10, “SEGV”=>11, “KILL”=>9, “EMT”=>7, “SYS”=>12, “TERM”=>15, “IOT”=>6, “HUP”=>1, “STOP”=>17, “TRAP”=>5, “INT”=>2, “INFO”=>29, “PROF”=>27, “XCPU”=>24, “CLD”=>20,
319
“PIPE”=>13, “TTIN”=>21, “USR1”=>30, “XFSZ”=>25,
“FPE”=>8, “VTALRM”=>26, “IO”=>23, “WINCH”=>28, “TSTP”=>18, “ABRT”=>6, “TTOU”=>22, “QUIT”=>3, “CHLD”=>20, “CONT”=>19, “ILL”=>4, “USR2”=>31, “URG”=>16, “ALRM”=>14, “EXIT”=>0}
trap Signal.trap( signal, proc ) → obj Signal.trap( signal ) { block } → obj Especifica el tratamiento de las señales. El primer parámetro es un nombre de señal (una cadena como SIGALRM, SIGUSR1, etc) o un número de señal. Los caracteres SIG se pueden omitir en el nombre de la señal. El comando o bloque especifica el código que se vá a ejecutar cuando se lanza la señal. Si el comando es la cadena IGNORE o SIG_IGN, la señal será ignorada. Si el comando es DEFAULT o SIG_DFL, será invocado el controlador por defecto del sistema operativo. Si el comando es EXIT, el script será terminado por la señal. De otra manera, se ejecutará la orden o el bloque dado.
La señal especial EXIT o la señal número cero se invoca justo antes de la finalización del programa.
trap devuelve el controlador anterior de la señal dada.
Signal.trap(0, lambda { puts “Terminating: #{$$}” }) Signal.trap(“CLD”) { puts “Child died” } fork && Process.wait produce: Terminating: 27913 Child died Terminating: 27912 Clase
Symbol < Objeto Los objetos Symbol representan nombres en el intérprete Ruby. Éstos se generan utilizando la síntaxis literal :name y mediante los diferentes métodos to_sym. El objeto símbolo mismo creará una cadena para el nombre dado por la duración de la ejecución de un programa, independientemente del contexto o el significado de ese nombre. Por lo tanto, si Fred es una constante en un contexto, un método en otro, y una clase en un tercero, el Symbol :Fred será el mismo objeto en los tres contextos. module One class Fred end $f1 = :Fred end module Two Fred = 1 $f2 = :Fred end def Fred() end $f3 = :Fred $f1.id -> $f2.id -> $f3.id ->
2526478 2526478 2526478
Métodos de Clase
all_symbols
Symbol.all_symbols → array 320
Devuelve una matriz de todos los símbolos actuales en la tabla de símbolos Ruby.
Symbol.all_symbols.size -> 913 Symbol.all_symbols[1,20] -> [:floor, :ARGV, :Binding, :symlink, :chown, :EOFError, :$;, :String, :LOCK_SH, :”setuid?”, :$<, :default_proc, :compact, :extend, :Tms, :getwd, :$=, :ThreadGroup, :”success?”, :wait2]
Métodos de Instancia
id2name
sym.id2name → string
Devuelve la representación de cadena de sym. Antes de Ruby 1.8, los símbolos representaban típicamente nombres, ahora pueden ser cadenas arbitrarias. :fred.id2name :”99 red balloons!”.id2name
-> ->
“fred” “99 red balloons!”
inspect
sym.inspect → string
Devuelve la representación de sym como un símbolo literal.
:fred.inspect :”99 red balloons!”.inspect
-> ->
:fred :”99 red balloons!”
to_i
sym.to_i → fixnum
Devuelve un entero que es único para cada símbolo dentro de una ejecución particular de un programa.
:fred.to_i “fred”.to_sym.to_i
-> ->
9857 9857
to_int
sym.to_int → fixnum
Sinónimo de Symbol#to_i. Permite que los símbolos el tener un comportamiento similar al entero.
to_s
sym.to_s → string
Sinónimo de Symbol#id2name.
to_sym
sym.to_sym → sym
¡Los símbolos se comportan como símbolos!
Clase
UnboundMethod < Object Ruby soporta dos tipos de métodos objetivados. La clase Method se utiliza para representar los métodos que están asociados con un objeto en particular: estos objetos método están obligados a ese objeto. Se pueden crear objetos método enlazados a un objeto utilizando Object#method. Rubí también soporta métodos no consolidados, que son objetos método que no están asociados con un objeto particular. Estos pueden ser creados ya sea llamando a unbind en un objeto método vinculado o ya sea llamando a Module#instance_method. Sólo se puede llamar a métodos sin consolidar después de haber sido asociados a un objeto. Ese objeto debe ser una clase kind_of? original del método.
321
class def end def end end
Square area @side * @side initialize(side) @side = side
area_unbound = Square.instance_method(:area) s = Square.new(12) area = area_unbound.bind(s) area.call -> 144 Los métodos no consolidados son una referencia al método en el momento en que es objetivizado: los cambios posteriores a la clase base no afectará al método no consolidado. class Test def test :original end end um = Test.instance_method(:test) class Test def test :modified end end t = Test.new t.test um.bind(t).call
-> ->
:modified :original
Métodos de Instancia
arity
umeth.arity → fixnum
Ver Method#arity (que devuelve una indicación del número de argumentos aceptados por un método).
bind
umeth.bind( obj ) → method
Enlaza umeth a obj. Si Klass era la clase de la cual se obtuvo originalmente umeth, obj.kind_of?(Klass) debe ser verdadero. class A def test puts “In test, class = #{self.class}” end end class B < A end class C < B end um = B.instance_method(:test) bm = um.bind(C.new) bm.call bm = um.bind(B.new)
322
bm.call bm = um.bind(A.new) bm.call produce: In test, class = C In test, class = B prog.rb:16:in `bind’: bind argument must be an instance of B (TypeError) from prog.rb:16
Librería Estándar El intérprete Ruby viene con un gran número de clases, módulos y métodos integrados --que están disponibles como parte del programa en ejecución. Cuando se necesita una utilidad que no es parte del repertorio integrado, a menudo se encuentra en una biblioteca que puede dar lugar a un require en el programa. Un gran número de bibliotecas de Ruby están disponibles en Internet. Sitios tales como Ruby Application Archive (http://raa.rubylang.org) y RubyForge (http://rubyforge.org) tienen grandes índices y una gran cantidad de código. Sin embargo, Ruby también viene de serie con un gran número de bibliotecas. Algunas de éstas se han escrito en Ruby puro y estarán disponible en todas las plataformas de Ruby. Otras son extensiones de Ruby, y algunas de ellas estarán presentes sólo si su sistema es compatible con los recursos que necesitan. Todos se pueden incluir en su programa Ruby utilizando require. Y, a diferencia de las bibliotecas que se encuentran en Internet, se puede casi garantizar que todos los usuarios de Ruby tienen estas bibliotecas ya instaladas en sus máquinas. Usted no encontrará aquí descripciones detalladas de las librerías: esto es para que consulte la documentación propia de la biblioteca. Esto que se sugiere de “consulte la documentación propia de la biblioteca” está muy bien, pero, ¿dónde podemos encontrarla? La respuesta es “depende”. Algunas bibliotecas ya han sido documentadas mediante RDoc. Eso significa que se puede usar el comando ri para obtener su documentación. Por ejemplo, a partir de la línea de comandos, se debe ser capaz de ver la siguiente documentación del método decode64 en la biblioteca estándar Base64.
% ri Base64.decode64 --------------------------------------------------------------Base64#decode64 decode64(str) ---------------------------------------------------------------------------- Returns the Base64-decoded version of str. require ‘base64’ str = ‘VGhpcyBpcyBsaW5lIG9uZQpUaGlzIG’ + ‘lzIGxpbmUgdHdvClRoaXMgaXMgbGlu’ + ‘ZSB0aHJlZQpBbmQgc28gb24uLi4K’ puts Base64.decode64(str) Generates: This is line one This is line two This is line three And so on... Si no hay documentación RDoc disponible, el siguiente lugar para buscar es en la propia biblioteca. Si usted tiene una distribución de código fuente de Ruby, esto se encuentra en los subdirectorios ext/ y lib/.
323
En cambio, si tiene una instalación sólo binaria, todavía se puede encontrar la fuente Ruby pura de los módulos de biblioteca (normalmente en el directorio lib/ruby/1.8/ de la instalación de Ruby). A menudo, los directorios de los fuentes de las librerías contienen la documentación que el autor aún no ha convertido al formato RDoc. Si usted todavía no puede encontrar la documentación, es el turno de Google. Muchas de las librerías estándar de Ruby también se alojan en proyectos externos. Hay autores que desarrollan de forma independiente y luego periódicamente, integran el código en la distribución estándar de Ruby. Por ejemplo, si se desea información detallada sobre la API de la biblioteca YAML, hay que buscar en Google “yaml ruby” que puede llevar a http://yaml4r.sourceforge.net/doc/. Después de admirar la obra de arte why the lucky stiff ’s, un clic y se tiene acceso a sus más de 40 páginas de manual de referencia. El próximo puerto de escala es la lista de correo ruby-talk. Haga una pregunta (educada) allí, y lo más probable es que obtenga una respuesta erudita en cuestión de horas. Consulte la sección “Mailing Lists”, en http://www.rubylang.org/ para más detalles sobre como unirse a una lista de correo. Y si usted todavía no puede encontrar la documentación, siempre se puede seguir el consejo de Obi Wan y hacer lo que se hace cuando se documenta Ruby --utilizar la fuente. Se sorprenderá de lo fácil que es leer los fuentes reales de las bibliotecas Ruby y trabajar en los detalles de su utilización.
Como ejemplo vamos a ver aquí las librerías CGI y CGI::Session.
Librería
CGI
La clase CGI proporciona soporte para programas utilizados como scripts CGI (Common Gateway Interface) en un servidor Web. Los objetos CGI se inicializan con los datos del entorno y desde una solicitud HTTP, y proporcionan accesores convenientes a los datos de formulario y a cookies. También se puede administrar sesiones usando una variedad de mecanismos de almacenamiento. La clase CGI proporciona también utilidades básicas para la generación de HTML y métodos de clase de escape y desesescape de solicitudes y HTML. Nota: La implementación 1.8 de CGI introduce un cambio en la manera de acceder a los datos de formulario. Consulte la documentación ri de CGI#[] y CGI#params para más detalles.
Ver también: CGI:: Session.
• Escapa y desescapa caracteres especiales en direcciones URL y HTML. Si la variable $KCODE está establecida en “u” (por UTF8), la biblioteca se convertirá desde HTML a Unicode a UTF-8 interno. require ‘cgi’ CGI.escape(‘c:\My Files’) CGI.unescape(‘c%3a%5cMy+Files’) CGI::escapeHTML(‘”a”
-> -> ->
c%3A%5CMy+Files c:\My Files "a"
$KCODE = “u” # Usar UTF8 CGI.unescapeHTML(‘"a"<=>b’) CGI.unescapeHTML(‘AA’) CGI.unescapeHTML(‘πr²’)
-> -> ->
“a”<=>b AA πr2
• Accede a la información de la solicitud entrante.
require ‘cgi’ c = CGI.new c.auth_type c.user_agent
-> ->
“basic” “Mozscape Explorari V5.6”
• Accede a los campos del formulario de una solicitud entrante. Suponiendo que el siguiente script se instala como test.cgi y el usuario enlaza a él mediante http://mydomain.com/test. cgi?fred=10&barney=cat.
324
require ‘cgi’ c = CGI.new c[‘fred’] -> c.keys -> c.params ->
“10” [“barney”, “fred”] {“barney”=>[“cat”], “fred”=>[“10”]}
• Si un formulario contiene varios campos con el mismo nombre, los correspondientes valores se retornarán al script como una matriz. El accesor [ ] retorna sólo el primero de estos valores --indexa el resultado del método params para obtener todos ellos. En el siguiente ejemplo asuminos que el formulario tiene tres campos llamados “name”. require ‘cgi’ c = CGI.new c[‘name’] c.params[‘name’] c.keys c.params
-> -> -> ->
“fred” [“fred”, “wilma”, “barney”] [“name”] {“name”=>[“fred”, “wilma”, “barney”]}
• Envia una respuesta al navegador. (No hay mucha gente que utilice esta forma de generación de HTML. Considerar una de las bibliotecas de plantillas). require ‘cgi’ cgi = CGI.new(“html4Tr”) cgi.header(“type” => “text/html”, “expires” => Time.now + 30) cgi.out do cgi.html do cgi.head{ cgi.title{“Hello World!”} } + cgi.body do cgi.pre do CGI::escapeHTML( “params: “ + cgi.params.inspect + “\n” + “cookies: “ + cgi.cookies.inspect + “\n”) end end end end
• Almacenar una cookie en el navegador del cliente.
require ‘cgi’ cgi = CGI.new(“html4”) cookie = CGI::Cookie.new(‘name’ => ‘mycookie’, ‘value’ => ‘chocolate chip’, ‘expires’ => Time.now + 3600) cgi.out(‘cookie’ => cookie) do cgi.head + cgi.body { “Cookie stored” } end
• Recuperar una cookie almacenada previamente.
require ‘cgi’ cgi = CGI.new(“html4”) cookie = cgi.cookies[‘mycookie’] cgi.out(‘cookie’ => cookie) do cgi.head + cgi.body { “Flavor: “ + cookie[0] } end
325
Librería
CGI::Session CGI::Session mantiene un estado persistente de los usuarios Web en un entorno CGI. Las sesiones pueden ser residentes en memoria o se pueden almacenar en disco. Ver ri CGI::Session para más información. require ‘cgi’ require ‘cgi/session’ cgi = CGI.new(“html3”) sess = CGI::Session.new(cgi, “session_key” => “rubyweb”, “prefix” => “web-session.” ) if sess[‘lastaccess’] msg = “Los últimos que estuvieron aquí #{sess[‘lastaccess’]}.” else msg = “Parece que no han estado aquí por un tiempo” end count = (sess[“accesscount”] || 0).to_i count += 1 msg << “Numero de visitas: #{count}” sess[“accesscount”] = count sess[“lastaccess”] = Time.now.to_s sess.close cgi.out { cgi.html { cgi.body { msg } } } ---------------------------------------------
Por último y como colofón a esta traducción libre, un extracto muy interesante de la 3ª edi-
ción, en la que se documenta la versión 1.9 de este extraordinario lenguaje de programación.
Metaprogramación El telar de Jacquard, inventado hace más de 200 años, fue el primer dispositivo controlado mediante tarjetas perforadas --filas de agujeros en cada tarjeta utilizadas para controlar el patrón a tejer en la tela. Pero imaginemos que si en vez de producir tela, el telar pudiera perforar más tarjetas, y éstas podrían alimentar de nuevo el mecanismo. La máquina podría ser utilizada para crear nuevos programas que podrían ejecutarse. Y eso sería la metaprogramación --la escritura de código que escribe código. La programación es todo lo que trata acerca de la construcción de capas de abstracción. Para resolver éste problema está la construcción de puentes entre el implacable y mecánico mundo de silicio y el más ambiguo y fluido mundo que habitamos. Algunos lenguajes de programación --como C-- son cercanos a la máquina. La distancia desde el código C hasta el dominio de aplicación puede ser grande. Otros lenguajes --Ruby, tal vez-- proporcionan abstracciones de alto nivel y por lo tanto permiten empezar a escribir código más cercano al dominio de destino. Por esta razón, la mayoría de la gente considera que un lenguaje de alto nivel puede ser un mejor lugar de partida para el desarrollo de aplicaciones (aunque van a seguir discutiendo sobre que lenguaje elegir). Pero cuando se metaprograma, ya no se está limitado a la serie de abstracciones integradas en su lenguaje de programación. De otro modo, se pueden crear nuevas abstracciones que se integran en el lenguaje de acogida. En efecto, se está creando un nuevo y específico dominio de lenguaje de programación 326
--diseñado para que se puedan expresar los conceptos que se necesitan para resolver problemas particulares. Ruby hace fácil la metaprogramación. Como resultado, los programadores de Ruby más avanzados utilizan técnicas de metaprogramación para simplificar su código. En este capítulo se muestra cómo lo hacen. No pretende ser un estudio exhaustivo de las técnicas de metaprogramación. En su lugar, vamos a ver la los principios subyacentes de Ruby para hacer posible la metaprogramación. Desde ahí, podrá inventar su propio lenguaje de metaprogramación.
Objetos y clases Las clases y los objetos son, obviamente, el centro de Ruby. Pero a primera vista puede ser un poco confuso. Parece que hay un montón de conceptos: clases, objetos, objetos de clase, métodos de instancia, métodos de clase, clases singleton y clases virtuales. En realidad, sin embargo, Ruby tiene solo una única clase base y estructura de objeto. Un objeto Ruby tiene tres componentes: un conjunto de banderas, algunas variables de instancia y una clase asociada. Una clase Ruby es un objeto de la clase Class. Contiene todas las cosas que tiene un objeto y un conjunto de definiciones de métodos y una referencia a una superclase (que a su vez es otra clase). Y, fundamentalmente, eso es todo. A partir de aquí, se puede trabajar en los detalles de metaprogramación para uno mismo. Pero como siempre, el diablo se esconde en los detalles, así que vamos a profundizar un poco más.
self y Llamada a Método Ruby tiene el concepto de objeto actual (o en curso). Este objeto actual hace referencia a la variable integrada self de sólo lectura. self tiene dos funciones importantes en un programa Ruby en ejecución. En primer lugar, self controla cómo Ruby encuentra las variables de instancia. Ya hemos dicho que todos los objetos llevan alrededor un conjunto de variables de instancia. Cuando usted accede a una variable de instancia, Ruby busca en el objeto referenciado por self. En segundo lugar, self juega un papel vital en la llamada a método. En Ruby, cada llamada a método se realiza en algún objeto. Este objeto se llama el receptor de la llamada. Cuando se hace una llamada a un método como items.size, el objeto referenciado por la variable items es el receptor y size es el método a invocar. Si se hace una llamada a método, como puts “hola”, no hay receptor explícito. En este caso, Ruby utiliza el objeto actual, self, como el receptor. Va a la clase self y busca el método (en este caso, puts). Si no puede encontrar el método en la clase, busca en la superclase de la clase y luego en la superclase de esa clase, deteniéndose cuando se queda sin superclases (que sucederá después de buscar en BasicObject)4. Cuando se hace una llamada a método con un receptor explícito (por ejemplo, invocar items.size), el proceso es sorprendentemente similar. El único cambio - pero de vital importancia - es el hecho de que self se cambia por la duración de la llamada. Antes de iniciar el proceso de búsqueda de métodos, Ruby establece self para el receptor (el objeto referenciado por items en este caso). Entonces, después de que la llamada retorna, Ruby restablece el valor que self tenía antes de la llamada.
Vamos a ver cómo funciona esto en la práctica. He aquí un programa simple:
class def
4
Test one @var = 99 two
Si no puede encontrar el método después de haber agotado la jerarquía de clase del objeto, Ruby busca un método llamado method_missing en el receptor original, empezando desde la clase de self y luego buscando en la cadena de superclase. 327
end def two puts @var end end t = Test.new t.one produce: 99 La llamada a Test.new en la primera de las dos últimas líneas, crea un nuevo objeto de la clase Test, asignando ese objeto a la variable t. Luego, en la línea siguiente, llamamos al método t.one. Para ejecutar esta llamada, Ruby establece self a t y luego busca en la clase de t el método one. Ruby encuentra el método definido en la línea 2 y lo llama. Dentro del método, se establece la variable de instancia @var a 99. Esta variable de instancia se asociará con el objeto actual. ¿Cuál es ese objeto? Bueno, la llamada a t.one establece self en t, por lo que en el método one, self será esa instancia particular de la de la clase Test. En la siguiente línea, one llama a two. Como no hay receptor explícito, self no se cambia. Cuando Ruby busca el método two, lo ve en Test, la clase de t. El método two hace referencia a una variable de instancia @var. Una vez más, Ruby busca esta variable en el objeto actual y encuentra la misma variable que fué establecida por el método one. La llamada a puts al final de two funciona de la misma manera. Una vez más, como no hay receptor explícito, self no se ve afectada. Ruby busca el método puts en la clase del objeto actual, pero no lo encuentra. A continuación, busca en la superclase de Test, la clase Object y de nuevo, no encuentra puts. Sin embargo, Object se mezcla en el módulo Kernel y los módulos mixtos o mezclados actuan como si se trataran de superclases. El módulo Kernel hace definición de puts, por lo que el método es encontrado y ejecutado. Después de que one y two retornan, Ruby restablece self al valor que tenía antes de la llamada original a t.one. Esta explicación puede parecer pesada, pero la comprensión es vital para el dominio de la metaprogramación en Ruby.
Self y Definición de Clase Hemos visto que llamar a un método con un receptor explícito cambia self. Tal vez sorprendentemente, también cambia self por una definición de clase. Esto es una consecuencia del hecho de que las definiciones de clases son en realidad código ejecutable en Ruby --si podemos ejecutar código, necesitamos tener un objeto actual. Una prueba simple muestra lo que este objeto es: class puts puts puts end
Test “In the definition of class Test” “self = #{self}” “Class of self = #{self.class}”
produce: In the definition of class Test self = Test Class of self = Class
328
Dentro de una definición de clase, self se establece en el objeto de clase de la clase que se define. Esto significa que las variables de instancia establecidas en una definición de clase estarán disponible para los métodos de la clase (porque self será el mismo cuando las variables se definen y cuando los métodos se ejecutan): class Test @var = 99 def self.value_of_var @var end end puts Test.value_of_var produce: 99
Singletons Ruby permite definir los métodos que son específicos de un objeto en particular. Estos se denominan métodos singleton. Vamos a empezar con un simple objeto cadena: animal = “cat” puts animal.upcase produce: CAT Esto se traduce en la estructura de objeto que se muestra en la Figura. La variable animal apunta a un objeto que contiene (entre otras cosas) el valor de la cadena (“cat”) y un puntero a la clase del objeto String. Cuando se llama a animal.upcase, Ruby va al objeto referenciado por la variable animal y luego busca el método upcase en la clase del objeto referenciada desde el objeto animal. Nuestro animal es una cadena y por lo tanto tiene los métodos de la clase String disponibles.
Ahora vamos a hacerlo más interesante. Vamos a definir un método singleton en la cadena referenciada por animal: animal = “cat” def animal.speak puts “The #{self} says miaow” end
329
animal.speak puts animal.upcase produce: The cat says miaow CAT Ya vemos cómo la llamada a animal.speak funciona cuando nos fijamos en cómo se invocan los métodos. Ruby establece self al objeto cadena “cat” referenciado por animal y luego busca un método speak en la clase de ese objeto. Sorprendentemente, se encuentra. Es en principio sorprendente, porque la clase de “cat” es String, y String no tiene un método speak. Así que, ¿hace Ruby algún tipo de magia para los casos especiales de estos métodos que se definen en objetos individuales? Afortunadamente, la respuesta es “no.” El modelo objeto de Ruby es notablemente consistente. Cuando se define el método singleton para el objeto “cat”, Ruby crea una nueva clase anónima y define el método speak en esa clase. Esta clase anónima a veces es llamada una clase singleton y otras veces una eigenclass (clase-propia). Preferimos la primera, porque enlaza con la idea de los métodos singleton.
Ruby hace de esta clase singleton la clase del objeto “cat” y hace de String (que era la clase original de “cat”) la superclase de la clase singleton. Esto se muestra en la Figura siguiente: Ahora vamos a seguir la llamada a animal.speak. Ruby va al objeto referenciado por animal y luego busca en su clase el método speak. La clase del objeto animal es la clase singleton recién creada y contiene el método que necesitamos. ¿Qué pasa si en su lugar se llama a animal.upcase? El tratamiento se inicia de la misma manera: Ruby busca el método upcase en la clase singleton, pero no lo encuentra allí. A continuación, sigue las reglas normales de proceso y comienza a buscar en la cadena de superclases. La superclase de singleton es String, y Ruby encuentra el método upcase allí. Hay que tener en cuenta que aquí no hay un procesamiento de caso especial --el método Ruby de llamada siempre funciona de la misma manera.
Singletons y Clases Antes mencionamos que dentro de una definición de clase, self se establece al objeto de la clase que se está definiendo. Resulta que esta es la base para uno de los aspectos más elegantes del modelo de objetos de Ruby. Recordemos que podemos definir los métodos de clase en Ruby utilizando cualquiera de estas dos formas: def self.xxx o def ClassName.xxx.
330
class def end def end end
Dave self.class_method_one puts “Class method one” Dave.class_method_two puts “Class method two”
Dave.class_method_one Dave.class_method_two produce: Class method one Class method two Ahora sabemos por qué las dos formas son idénticas: dentro de la definición de clase, self se establece a Dave. Pero ahora que hemos visto los métodos singleton, también sabemos que, en realidad, no hay tal cosa como métodos de clase en Ruby. Ambas definiciones anteriores definen métodos singleton del objeto de la clase. Al igual que todos los otros métodos singleton, se les puede llamar a través del objeto (en este caso, la clase Dave). Antes de crear los dos métodos singleton en la clase Dave, el puntero de clase en el objeto de clase apunta a la clase Class (ésta es una frase confusa. Otra forma de decirlo es “Dave es una clase, por lo que la clase de Dave es la clase Class,” aunque esto es bastante confuso también). La situación se parece a la Figura siguiente: El diagrama de objetos para la clase Dave después de definir los métodos se muestra en la Figura de la página siguiente. ¿Se ve cómo se crea la clase Singleton, como se vió para el ejemplo animal? La clase se inserta como la clase de Dave, y la clase original de Dave se hace padre de esta nueva clase. Ahora podemos unir juntos los dos usos de self, el objeto actual. Hablamos de cómo las variables de instancia se buscan en self, y hablamos de cómo los métodos singleton definidos en self se convierten en métodos de clase. Vamos a utilizar estos hechos para acceder a variables de instancia de los objetos de clase: class Test @var = 99 def self.var @var end def self.var=(value) @var = value end end puts “Original value = #{Test.var}” Test.var = “cat” puts “New value = #{Test.var}” produce: Original value = 99 New value = cat Los recién llegados a Ruby comúnmente cometen el error de establecer las variables de instancia en línea en la definición de clase (como lo hicimos con @var en el código anterior) para luego tratar de acceder a estas variables desde los métodos de instancia. Como el código ilustra, esto no funcionará, ya que las variables de instancia definidas en el cuerpo de la clase, están asociadas con el objeto de clase y no con las instancias de la clase.
331
Otra Forma de Acceder a la Clase Singleton Hemos visto cómo se pueden crear métodos en la clase singleton de un objeto mediante añadiendo la referencia al objeto a la definición del método utilizando algo así como def animal.speak.
Se puede hacer lo mismo utilizando la notación de Ruby class << an_objec
animal = “dog” class << animal def speak puts “The #{self} says WOOF!” end
332
end animal.speak produce: The dog says WOOF! Dentro de este tipo de definición de clase, self se establece a la clase singleton para el objeto dado (animal, en este caso). Como las definiciones de clase devuelven el valor de la última instrucción ejecutada en el cuerpo de la clase, podemos usar este hecho para obtener el objeto de la clase singleton: animal = “dog” def animal.speak puts “The #{self} says WOOF!” end singleton = class << animal def lie puts “The #{self} lies down” end self # << retorna objeto de la clase singleton end animal.speak animal.lie puts “Singleton class object is #{singleton}” puts “It defines methods #{singleton.instance_methods - ‘cat’.methods}” produce: The dog says WOOF! The dog lies down Singleton class object is #> It defines methods [:speak, :lie] Hay que tener en cuenta la notación que Ruby utiliza para denotar una clase singleton: #>. Ruby le va a decir cuando algún problema impide el uso de clases singleton fuera del contexto de su objeto original. Por ejemplo, no se puede crear una nueva instancia de una clase singleton: singleton = class << “cat”; self; end singleton.new produce: prog.rb:2:in ‘new’: can’t create instance of singleton class (TypeError) from /tmp/prog.rb:2:in ‘’ Vamos a juntar lo que sabemos sobre las variables de instancia, self y las clases singleton. Anteriormente hemos dicho que con métodos accesores a nivel de clase vamos a obtener y establecer el valor de una variable de instancia definida en un objeto de clase. Sin embargo, Ruby ya tiene a attr_accessor, que define los métodos getter y setter (obtenedor y establecedor). Normalmente, sin embargo, estos se definen como métodos de instancia y por lo tanto tendrán acceso a los valores almacenados en las instancias de una clase. Para que funcionen con las variables de instancia a nivel de clase, tenemos que invocar a attr_accessor en la clase singleton: class Test @var = 99
333
class << self attr_accessor :var end end puts “Original value = #{Test.var}” Test.var = “cat” puts “New value = #{Test.var}” produce: Original value = 99 New value = cat
Herencia y Visibilidad Hay una arruga oscura cuando se trata la definición de método y la herencia de clase. Dentro de una definición de clase, puede cambiar la visibilidad de un método en una clase antecesora.
Por ejemplo, se puede hacer algo como esto:
class Base def a_method puts “Got here” end private :a_method end class Derived1 < Base public :a_method end class Derived2 < Base end En este ejemplo, usted sería capaz de invocar un_metodo en instancias de la clase Derived1, pero en cambio, no en instancias de Base o Derived2. Por tanto, ¿cómo logra Ruby esta hazaña de tener un método con dos visibilidades diferentes? En pocas palabras, se engaña. Si una subclase cambia la visibilidad de un método en uno de los antecesores, Ruby efectivamente inserta un método proxy oculto en la subclase que invoca al método original utilizando super. A continuación, establece la visibilidad de ese proxy para lo que se solicitó. Esto significa que el siguiente código: class Derived1 < Base public :a_method end
es efectivamente el mismo que este:
class Derived1 < Base def a_method(*) super end public :a_method end La llamada a super puede acceder a métodos de los padres independientemente de su visibilidad, por lo que la reescritura permite a la subclase reemplazar las reglas de visibilidad su antecesor. Atemoriza, ¿eh?
334
Módulos y Mixins Sabemos que cuando se incluye un módulo en una clase Ruby, los métodos de instancia de ese módulo están disponibles como métodos de instancia de la clase. module Logger def log(msg) STDERR.puts Time.now.strftime(“%H:%M:%S: ”) + “#{self} (#{msg})” end end class Song include Logger end class Album include Logger end s = Song.new s.log(“created”) produce: 11:53:26: # (created) La implementación Ruby de include es muy simple: el módulo que se incluye es efectivamente añadido como una superclase de la clase que se define. Es como si el módulo fuera el padre de la clase en la que se mezcla. Y eso sería el final de la descripción a excepción de una pequeña pega. Debido a que el módulo es inyectado en la cadena de superclases, se debe mantener un vínculo con la clase padre original. Si no, no habría manera de atravesar la cadena de superclases para buscar métodos. Sin embargo, usted puede mezclar el mismo módulo en diferentes clases, y estas clases podrían tener cadenas de superclase totalmente diferentes. Si hubiera sólo un objeto módulo mezclado en todas estas clases, no habría manera de hacer el seguimiento de las diferentes superclases para cada una. Para evitar esto, Ruby utiliza un truco ingenioso. Cuando se incluye un módulo en la clase Ejemplo, Ruby crea un nuevo objeto de clase, haciéndole la superclase de Ejemplo, y luego establece la superclase de la nueva clase a la superclase original de Ejemplo. A continuación, referencia al módulo desde este nuevo objeto de clase de tal manera que cuando se busca un método en esta clase, en realidad se busca en el módulo, como se muestra en la figura en la página siguiente. Un bonito efecto secundario de este arreglo es que si usted cambia un módulo después de su inclusión en una clase, los cambios se reflejan en la clase (y en los objetos de la clase). De esta manera, los módulos se comportan igual que las clases. module Mod def greeting “Hello” end end class Example include Mod end ex = Example.new puts “Before change, greeting is #{ex.greeting}” module Mod def greeting “Hi”
335
end end puts “After change, greeting is #{ex.greeting}” produce: Before change, greeting is Hello After change, greeting is Hi
Si un mismo módulo incluye otros módulos, una cadena de clases proxy se añadiran a cualquier clase que incluya a este módulo, un proxy para cada módulo que esté directa o indirectamente incluidos. Por último, Ruby incluirá un módulo sólo una vez en una cadena de herencia --incluir un módulo que ya está incluido en una de sus superclases no tiene ningún efecto.
extend El método include añade efectivamente un módulo como una superclase de self. Se utiliza dentro de una definición de clase para que los métodos de instancia en el módulo estén disponibles para las instancias de la clase. Sin embargo, a veces es conveniente añadir los métodos de instancia a un objeto particular. Esto se hace con Object#extend. He aquí un ejemplo: module Humor def tickle “#{self} says hee, hee!” end
336
end obj = “Grouchy” obj.extend Humor puts obj.tickle produce: Grouchy says hee, hee!
Vamos a parar un segundo para pensar cómo puede implementarse esto.
Cuando Ruby ejecuta obj.tickle en este ejemplo de código, se hace el truco habitual de buscar en la clase de obj un método llamado tickle (cosquillas). Para que extend funcione, tiene que añadir los métodos de instancia del módulo Humor en la cadena de superclases para la clase de obj. Por lo tanto, al igual que con las definiciones de métodos singleton, Ruby crea una clase singleton para obj y luego incluye el módulo Humor en esa clase. De hecho, sólo para demostrar que esto es todo lo que sucede, aquí está la implementación en C de extend en el actual intérprete de Ruby 1.9: void rb_extend_object(VALUE obj, VALUE module) { rb_include_module(rb_singleton_class(obj), module); } Hay un truco interesante con extend. Si lo usa en una definición de clase, los métodos de módulo se convierten en métodos de clase. Esto se debe a que la llamada a extend es equivalente a self.extend, por lo que los métodos se añaden a self, que en una definición de clase es la clase misma.
He aquí un ejemplo de cómo agregar métodos de módulo a nivel de clase:
module Humor def tickle “#{self} says hee, hee!” end end class Grouchy extend Humor end puts Grouchy.tickle produce: Grouchy says hee, hee! Más adelante, vamos a ver cómo utilizar extend para añadir métodos de estilo macro a una clase.
Metaprogramación de Macros a Nivel de Clase Si se ha utilizado Ruby para cualquier toma de tiempo, hay muchas posibilidades de haber utilizado attr_accessor, el método que define los métodos para leer y escribir las variables de instancia: class Song attr_accessor :duration end
Si se ha escrito una aplicación en Ruby on Rails, probablemente se ha utilizado has_many:
class Album < ActiveRecord::Base has_many :tracks end
337
Ambos son ejemplos de métodos a nivel de clase que generan código entre bastidores. Debido a la forma en que se expanden en algo más grande, la gente a veces les llama métodos macro. Vamos a crear un ejemplo trivial y luego construirlo en algo realista. Vamos a empezar por la implementación de un simple método que añade capacidades de registro a instancias de una clase. Anteriormente lo hicimos con un módulo --esta vez lo haremos utilizando un método a nivel de clase. Esta es la primera iteración: class Example def self.add_logging def log(msg) STDERR.puts Time.now.strftime(“%H:%M:%S: ”) + “#{self} (#{msg})” end end add_logging end ex = Example.new ex.log(“hello”) produce: 11:53:26: # (hello) Claramente esta es una pieza tonta de código. Tengan paciencia conmigo --voy a mejorar. Pero aún podemos aprender algunas cosas de él. En primer lugar, la notificación add_logging es un método de clase --se define en la clase singleton del objeto clase. Eso significa que podemos llamarlo más tarde en la definición de clase sin un receptor explícito, porque self se establece en el objeto clase dentro de una definición de clase. Luego hay que observar que este método add_logging contiene una definición de método anidada. Esta definición interna se vá a ejecutar sólo cuando se llama al método add_logging. El resultado es que log es definido como un método de instancia de la clase Example. Vamos a dar un paso más. Podemos definir el método add_logging en una clase y luego usarlo en una subclase. Esto funciona porque la jerarquía de la clase singleton es paralela a la jerarquía de clases regular. Como resultado, los métodos de clase en una clase padre están disponibles en la clase hija, tal como muestra el siguiente ejemplo. class Logger def self.add_logging def log(msg) STDERR.puts Time.now.strftime(“%H:%M:%S: ”) + “#{self} (#{msg})” end end end class Example < Logger add_logging end ex = Example.new ex.log(“hello”) produce: 11:53:26: # (hello) Piense de nuevo en los dos ejemplos al inicio de esta sección. Ambos funcionan de esta manera. attr_accessor es un método de clase definido en la clase Module y así está disponible en todas las definiciones de módulo y clase. has_many es un método de clase definido en la clase Base dentro
338
del módulo ActiveRecord de Rails y por lo tanto disponible para todas las clases de esa subclase ActiveRecord::Base. Este ejemplo, todavía no es muy convincente. Sería más fácil aún añadir directamente el método log como un método de instancia de nuestra clase Logger. Pero, ¿qué pasa si queremos construir una versión diferente del método log para cada clase que se utiliza? Por ejemplo, vamos a agregar la posibilidad de añadir una breve clase específica de identificación de cadena al inicio de cada mensaje de registro. Queremos ser capaces de decir algo como esto: class Song < Logger add_logging “Song” end class Album < Logger add_logging “CD” end Para ello, vamos a definir el método log sobre la marcha. Ya no podemos utilizar una sencilla definición estilo def...end. En su lugar, usaremos define_method, uno de los pilares de la metaprogramación. define_method toma el nombre de un método y un bloque, definiendo un método con el nombre dado y con el bloque como el cuerpo del método. Cualquier argumento en la definición del bloque se convierte en parámetro para el método que se está definiendo. class Logger def self.add_logging(id_string) define_method(:log) do |msg| now = Time.now.strftime(“%H:%M:%S”) STDERR.puts “#{now}-#{id_string}: #{self} (#{msg})” end end end class Song < Logger add_logging “Tune” end class Album < Logger add_logging “CD” end song = Song.new song.log(“rock on”) produce: 11:53:26-Tune: # (rock on)
Hay una sutileza importante en este código. El cuerpo del método log contiene la siguiente línea:
STDERR.puts “#{now}-#{id_string}: #{self} (#{msg})” El valor now es una variable local, y msg es el parámetro para el bloque. Pero id_string es el parámetro para el método add_logging que lo contiene. Es accesible en el interior del bloque debido a que las definiciones de bloque crean cierres, permitiendo que el contexto en el que se define el bloque sea trasladado y utilizado cuando se utiliza el bloque. En este caso, estamos dando un valor a partir de un método a nivel de clase y utilizándolo en un método de instancia que estamos definiendo. Este es un patrón común para crear este tipo de macros de nivel de clase. Así como se pasan parámetros desde un método de clase al cuerpo de un método que se define, también se puede utilizar el parámetro para determinar el nombre del método o métodos a crear. He aquí un
339
ejemplo que crea un nuevo tipo de attr_accessor que registra todas las asignaciones a una variable de instancia dada: class AttrLogger def self.attr_logger(name) attr_reader name define_method(“#{name}=”) do |val| puts “Assigning #{val.inspect} to #{name}” instance_variable_set(“@#{name}”, val) end end end class Example < AttrLogger attr_logger :value end ex = Example.new ex.value = 123 puts “Value is #{ex.value}” ex.value = “cat” puts “Value is now #{ex.value}” produce: Assigning 123 to value Value is 123 Assigning “cat” to value Value is now cat Una vez más, se utiliza el hecho de que el bloque que define el cuerpo del método es un cierre, accediendo al nombre del atributo en la cadena de mensaje de registro. Hay que notar que también se hace uso del hecho de que attr_reader es simplemente un método de clase --que se puede llamar dentro de nuestro método de clase para definir el método de lectura de nuestros atributos. Hay que tener en cuenta otro detalle común de metaprogramación --usamos instance_variable_set para establecer el valor de una variable de instancia (duh). Hay un correspondiente método _get que obtiene el valor de una variable de instancia por su nombre.
Macros y Módulos de Clase A veces, es perfectamente aceptable definir macros de clase en una clase y luego utilizar estos métodos macro en las subclases de esa clase. Otras veces sin embargo, no es apropiado utilizarlos en subclases, bien porque ya tenemos la subclase de alguna otra clase o bien porque nuestro estético diseño se rebela contra algo así como una canción de una subclase de un registrador. En estos casos, se puede utilizar un módulo que contenga la implementación de metaprogramación. Como hemos visto, utilizando extend dentro de una definición de clase se añaden métodos en un módulo como métodos de clase para la clase que se define: module AttrLogger def attr_logger(name) attr_reader name define_method(“#{name}=”) do |val| puts “Assigning #{val.inspect} to #{name}” instance_variable_set(“@#{name}”, val) end end end class Example
340
extend AttrLogger attr_logger :value end ex = Example.new ex.value = 123 puts “Value is #{ex.value}” ex.value = “cat” puts “Value is now #{ex.value}” produce: Assigning 123 to value Value is 123 Assigning “cat” to value Value is now cat Las cosas se ponen un poco más complicadas si se quieren añadir tanto los métodos de clase como los métodos de instancia en la clase que se define. Aquí hay una técnica, muy utilizada en la implementación del marco de trabajo de Rails. Se hace uso de un método gancho de Ruby, included, el cual es llamado automáticamente por Ruby cuando se incluye un módulo en una clase. Se le pasa el objeto clase de la clase que se define. module GeneralLogger # Método de instancia que se añade a cualquier clase que lo incluya def log(msg) puts Time.now.strftime(“%H:%M: ”) + msg end # módulo que contiene los métodos de clase que se añadirán module ClassMethods def attr_logger(name) attr_reader name define_method(“#{name}=”) do |val| log “Assigning #{val.inspect} to #{name}” instance_variable_set(“@#{name}”, val) end end end # extender la clase afitriona con los métodos de clase cuando se incluyen def self.included(host_class) host_class.extend(ClassMethods) end end class Example include GeneralLogger attr_logger :value end ex = Example.new ex.log(“New example created”) ex.value = 123 puts “Value is #{ex.value}” ex.value = “cat” puts “Value is #{ex.value}” produce:
341
11:53: New example created 11:53: Assigning 123 to value Value is 123 11:53: Assigning “cat” to value Value is cat Observar cómo la devolución de llamada incluida se utiliza para extender la clase anfitriona con los métodos definidos en el módulo ClassMethods interior. Ahora, como un ejercicio, tendrá que ejecutar el ejemplo anterior de su cabeza. Para cada línea de código, calcular el valor de self. Dominando esto se domina prácticamente este tipo de metaprogramación en Ruby.
Otras Dos Formas de Definición de Clases En el caso de que pensaramos que habíamos agotado las formas de definir las clases en Ruby, vamos a echar un vistazo a otras dos opciones.
Expresiones de Subclases La primera forma no es realmente nada nuevo --no es más que una generalización de la sintaxis de definición de clase regular. Sabemos que podemos escribir lo siguiente: class Parent ... end class Child < Parent ... end Lo que quizás no sabíamos es que la cosa a la derecha de < no tiene por qué ser sólo un nombre de clase, sino que puede ser cualquier expresión que devuelva un objeto de clase. En este ejemplo de código, tenemos la constante Parent. Una constante es una simple forma de expresión, y en este caso la constante Parent contiene el objeto clase de la primera clase que hemos definido. Ruby cuenta con una clase llamada Struct, que permite definir clases que contienen sólo atributos de datos. Por ejemplo, podríamos escribir lo siguiente: Person = Struct.new(:name, :address, :likes) dave = Person.new(‘Dave’, ‘TX’) dave.likes = “Programming Languages” puts dave produce: # El valor de retorno de Struct.new(...) es un objeto de clase. Al asignarlo a la constante Person, se puede utilizar Person a partir de entonces como si se tratara de cualquier otra clase. Pero queremos cambiar el método to_s de nuestra estructura. Podríamos hacerlo abriendo la clase y escribiendo el siguiente método: Person = Struct.new(:name, :address, :likes) class Person def to_s “#{self.name} lives in #{self.address} and likes #{self.likes}” end end 342
Sin embargo, podemos hacerlo más elegante (aunque a costa de un objeto de clase adicional) escribiendo lo siguiente: class Person < Struct.new(:name, :address, :likes) def to_s “#{self.name} lives in #{self.address} and likes #{self.likes}” end end dave = Person.new(‘Dave’, ‘Texas’) dave.likes = “Programming Languages” puts dave produce: Dave lives in Texas and likes Programming Languages
Creación de Clases Singleton
Vamos a ver algo de código Ruby:
class Example end ex = Example.new Cuando llamamos a Example.new, estamos invocando el método new en el objeto clase Example. Esto es sólo una llamada regular a método --Ruby busca el método new en la clase del objeto (la clase de Example es Class) y lo invoca. Resulta que también podemos invocar Class#new directamente: some_class = Class.new puts some_class.class produce: Class
Si se le pasa a Class.new un bloque, este se utiliza como el cuerpo de la clase:
some_class = Class.new do def self.class_method puts “In class method” end def instance_method puts “In instance method” end end some_class.class_method obj = some_class.new obj.instance_method produce: In class method In instance method Por defecto, estas clases serán descendientes directos de Object. Se les puede dar un padre diferente pasándoles la clase del padre como un parámetro:
343
some_class = Class.new(String) do def vowel_movement tr ‘aeiou’, ‘*’ end end obj = some_class.new(“now is the time”) puts obj.vowel_movement produce: n*w *s th* t*m* Como Toman las Clases sus Nombres Usted puede haber notado que las clases creadas por Class.new no tienen nombre. Sin embargo, no todo está perdido. Si se asigna el objeto clase para una clase sin nombre a una constante, Ruby automáticamente nombra la clase después de la constante: some_class = Class.new obj = some_class.new puts “Initial name is #{some_class.name}” SomeClass = some_class puts “Then the name is #{some_class.name}” puts “also works via the object: #{obj.class.name}” produce: Initial name is Then the name is SomeClass also works via the object: SomeClass Podemos utilizar estas clases construidas dinámicamente para extender Ruby de maneras interesantes. Por ejemplo, aquí tenemos una reimplementación simple de la clase Ruby Struct: def MyStruct(*keys) Class.new do attr_accessor *keys def initialize(hash) hash.each do |key, value| instance_variable_set(“@#{key}”, value) end end end end Person = MyStruct :name, :address, :likes dave = Person.new(name: “dave”, address: “TX”, likes: “Stilton”) chad = Person.new(name: “chad”, likes: “Jazz”) chad.address = “CO” puts “Dave’s name is #{dave.name}” puts “Chad lives in #{chad.address}” produce: Dave’s name is dave Chad lives in CO
344
instance_eval y class_eval Los métodos Object#instance_eval, Module#class_eval y Module#module_eval permiten establecer self en un objeto arbitrario, evaluar el código en un bloque y luego reajustar self: “cat”.instance_eval do puts “Upper case = #{upcase}” puts “Length is #{self.length}” end produce: Upper case = CAT Length is 3 Estas formas también toman una cadena (pero véase el recuadro resaltado con algunas notas sobre los peligros de la evaluación de cadenas): “cat”.instance_eval(‘puts “Upper=#{upcase}, length=#{self.length}”’) produce: Upper=CAT, length=3
eval es del año pasado Usted puede haber notado que hemos estado haciendo una buena cantidad de metaprogramación --acceder a variables de instancia, definición de métodos y la creación de clases-- y todavía no hemos utilizado eval. Esto es deliberado. En los viejos tiempos de Ruby, el lenguaje carecía de muchas de estas funcionalidades de metaprogramación y eval era la única manera de lograr estos efectos. Pero eval viene con un par de inconvenientes. En primer lugar, es lento --la llamada a eval efectivamente compila el código en la cadena antes de ejecutarlo. Pero, peor aún, eval puede ser peligroso. Si hay alguna posibilidad de datos externos --cosas que vienen de fuera de la aplicación-- pueden terminar dentro de los parámetros a eval. Entonces tendríamos un agujero de seguridad, debido a que los datos externos pueden terminar conteniendo código arbitrario que la aplicación ejecutará a ciegas. eval ahora se considera un método de último recurso. Tanto class_eval como instance_eval establecen self para la duración del bloque. Sin embargo, difieren en la manera de configurar el entorno para la definición del método. class_eval prepara las cosas como si estuviera en el cuerpo de una definición de clase, por lo que las definiciones de método definen métodos de instancia: class MyClass end MyClass.class_eval do def instance_method puts “In an instance method” end end obj = MyClass.new obj.instance_method produce:
345
In an instance method En cambio, la llamada a instance_eval en una clase, actúa como si estuviera funcionando dentro de la clase singleton de self. Por lo tanto, cualquier método que se defina se convertirá en método de clase. class MyClass end MyClass.instance_eval do def class_method puts “In a class method” end end MyClass.class_method produce: In a class method Puede ser útil recordar que, a la hora de definir los métodos, class_eval e instance_eval tienen exactamente los nombres equivocados: class_eval define los métodos de instancia e instance_eval define los métodos de clase. ¡Que sabemos...! Ruby 1.9 introduce variantes de estos métodos. Object#instance_exec, Module#class_exec, y Module#module_exec se comportan de forma idéntica a sus contrapartes _eval pero toman sólo un bloque (es decir, no toman una cadena). Cualquier argumento dado a los métodos se pasan como parámetros de los bloques. Esta es una característica importante. Anteriormente, era imposible pasar una variable de instancia dentro de un bloque dado a uno de los métodos _eval --como self cambia por la llamada, estas variables salen del ámbito. Con la forma _exec, ahora se puede pasar: @animal = “cat” “dog”.instance_exec(@animal) do |other| puts “#{other} and #{self}” end produce: cat and dog
instance_eval y Constantes Ruby 1.9 cambió la forma Ruby de buscar constantes cuando se ejecuta un bloque mediante instance_eval y class_eval. Ruby 1.9.2 luego revertió el cambio. En Ruby 1.8 y Ruby 1.9.2, las constantes se buscan en el ámbito léxico en el que se les hace referencia. En Ruby 1.9.0, se buscan en el ámbito en el que instance_eval es llamado. En este ejemplo (artificial) se muestra el comportamiento en el momento de la última vez en que se hizo este libro --que bien podría haber cambiado de nuevo en el momento en que usted lo ejecute... module One CONST = “Defined in One” def self.eval_block(&block) instance_eval(&block) end end module Two CONST = “Defined in Two” def self.call_eval_block One.eval_block do CONST end end
346
end Two.call_eval_block
# => “Defined in Two”
En Ruby 1.9.0, este mismo código se evaluará a “Defined in One”.
instance_eval y Lenguajes de Dominio Específico Resulta que instance_eval tiene un papel fundamental que desempeñar en un determinado tipo de dominio específico del lenguaje (domain-specific language o DSL). Por ejemplo, podríamos escribir un simple DSL para gráficos de tortuga5. Para dibujar un conjunto de tres cuadrados de 5x5, podríamos escribir lo siguiente6: 3.times do forward(8) pen_down 4.times do forward(4) left end pen_up end Claramente, pen_down, forward, left y pen_up se pueden implementar como métodos Ruby. Sin embargo, para llamarles sin un receptor de esta manera, o tenemos que estar dentro de una clase que los define (o es un hijo de esa clase) o tenemos que hacer los métodos globales. instance_eval al rescate. Podemos definir una clase Turtle que defina los distintos métodos que necesitamos como métodos de instancia. También vamos a definir un método walk (recorrido) que ejecutará nuestra tortuga DSL, y un método draw para dibujar la imagen resultante: class def def def def def def def end
Turtle left; ... end right; ... end forward(n); ... end pen_up; .. end pen_down; ... end walk(...); end draw; ... end
Si implementamos walk correctamente, podemos escribir lo siguiente:
turtle = Turtle.new turtle.walk do 3.times do forward(8) pen_down 4.times do forward(4) left end pen_up end end turtle.draw 5 En los sistemas de gráficos de tortuga, puede imaginar que tiene una tortuga a la que puede ordenar avanzar n casillas, girando a la izquierda y girando a la derecha. También puede hacer que la tortuga suba y baje una pluma retráctil. Si la pluma baja, una línea dibujará el seguimiento de los movimientos posteriores de la tortuga. Muy pocas de estas tortugas existen en la naturaleza, por lo que se tienden a simular en los ordenadores. 6 Sí, el forward(4) es correcto en este código. El punto inicial se dibuja siempre. 347
Así que, ¿cuál es la implementación correcta de walk? Bueno, es evidente que tiene que utilizar instance_eval, porque queremos que los comandos DSL en el bloque, llamen a los métodos en el objeto tortuga. También tenemos que organizarlo para pasar el bloque dado al método walk para ser evaluado por la llamada instance_eval. Nuestra implementación es la siguiente: def walk(&block) instance_eval(&block) end Observar como capturamos el bloque en una variable y luego expandimos de nuevo la variable en un bloque en la llamada a instance_eval.
Daremos un listado completo del programa tortuga al final.
¿Es este un buen uso de instance_eval? Depende de las circunstancias. La ventaja es que el código dentro del bloque parece simple --no se tiene que hacer explícito el receptor: 4.times do turtle.forward(4) turtle.left end Hay un inconveniente, sin embargo. Dentro del bloque, el ámbito no es el que se cree que es, por lo que este código no funcionaría: @size = 4 turtle.walk do 4.times do turtle.forward(@size) turtle.left end end Las variables de instancia se buscan en self, y self en el bloque no es lo mismo que self en el código, que establece la variable de instancia @size. Debido a esto, la mayoría de las personas se están alejando de este estilo de bloque instance_evaled.
Métodos Gancho En la sección anterior de Macros y Módulos de Clase, se define un método llamado included en nuestro módulo GeneralLogger. Cuando este módulo se incluye en una clase, Ruby invoca automáticamente a este método included, permitiendo a nuestro módulo agregar métodos de clase a la clase anfitriona. included es un ejemplo de un método gancho (a veces llamado devolución de llamada). Un método gancho es un método que escribimos, pero que Ruby llama desde dentro del intérprete cuando ocurre algún evento determinado. El intérprete busca estos métodos por su nombre --si se define un método en el contexto adecuado con un nombre apropiado, Ruby lo va a llamar cuando suceda el evento correspondiente. Los métodos que se pueden invocar desde dentro del intérprete se muestran en la Figura 36 en la página siguiente. No vamos a discutirlos aquí --en su lugar, vamos a mostrar algunos ejemplos de uso. En la sección de referencia de este libro se describen los métodos individuales, y el capítulo Duck Typing, se describen los métodos de coerción con más detalle.
El Gancho Heredado Si una clase define un método de clase llamado inherited, Ruby lo va a llamar siempre que la clase sea una subclase (es decir, siempre que una clase hereda de la original).
348
Este gancho se usa a menudo en situaciones en que una clase base debe llevar un seguimiento de sus hijos. Por ejemplo, una almacenaje en línea puede ofrecer una variedad de opciones de envío. Cada una podría estar representada por una clase separada, y cada una de estas clases puede ser una subclase de una clase de envío única (la clase Shipping por ejemplo). Esta clase padre puede hacer un seguimiento de todas las opciones de envío diferentes mediante un registro de cada clase de estas subclases. Cuando llega el momento de mostrar las opciones de envío para el usuario, la aplicación puede llamar a la clase base para pedirle una lista de las hijas: class Shipping # Clase Base @children = [] # esta variable se encuentra en la clase, no en instancias def self.inherited(child) @children << child end def self.shipping_options(weight, international) @children.select {|child| child.can_ship(weight, international)} end end class def end end
class def end end
MediaMail < Shipping self.can_ship(weight, international) !international FlatRatePriorityEnvelope < Shipping self.can_ship(weight, international) weight < 64 && !international
class InternationalFlatRateBox < Shipping def self.can_ship(weight, international) weight < 9*16 && international end end puts “Shipping 16oz domestic” puts Shipping.shipping_options(16, false) puts “\nShipping 90oz domestic” puts Shipping.shipping_options(90, false)
349
puts “\nShipping 16oz international” puts Shipping.shipping_options(16, true) produce: Shipping 16oz domestic MediaMail FlatRatePriorityEnvelope Shipping 90oz domestic MediaMail Shipping 16oz international InternationalFlatRateBox Los intérpretes de comandos suelen utilizar este modelo: la clase base mantiene un registro de los comandos disponibles, cada uno de los cuales se implementa en una subclase.
El Gancho method_missing Anteriormente, hemos visto cómo Ruby ejecuta una llamada a método mediante la búsqueda del método. Primero busca en la clase del objeto, después en su superclase, luego en la superclase de esa clase y así sucesivamente. Si la llamada al método tiene un receptor explícito, los métodos privados se omiten en esta búsqueda. Si el método no se encuentra en el momento en que nos quedemos sin superclases (debido a que BasicObject no tiene superclase), Ruby trata de invocar en el objeto original el método gancho method_missing. Una vez más, se sigue el mismo proceso --Ruby busca primero en la clase del objeto, entonces en su superclase, y así sucesivamente. Sin embargo, Ruby predefine su propia versión de method_missing en la clase BasicObject y por lo general, la búsqueda se detiene ahí. El método integrado method_missing básicamente plantea una excepción, ya sea un NoMethodError o un NameError dependiendo de las circunstancias. La clave aquí es que method_missing es simplemente un método Ruby. Podemos reemplazar en nuestras propias clases el manejo de las llamadas a métodos de otra manera indefinida en una forma específica de aplicación.
method_missing tiene una armadura simple, pero muchas personas se equivocan:
def method_missing(name, *args, &block) # ... El argumento name recibe el nombre del método que no se pudo encontrar y se pasa como un símbolo. El argumento args es una matriz con los argumentos que se pasaron en la llamada original. Y el argumento a veces olvidado block, recibe cualquier bloque que se pasó al método original. def method_missing(name, *args, &block) puts “Called #{name} with #{args.inspect} and #{block}” end wibble wobble 1, 2 wurble(3, 4) { stuff } produce: Called wibble with [] and Called wobble with [1, 2] and Called wurble with [3, 4] and # Antes de profundizar demasiado en los detalles, vamos a ofrecer un consejo acerca de la etiqueta. Hay dos principales maneras en que la gente usa method_missing. La primera intercepta cada uso de un método indefinido y lo maneja. La segunda es más sutil. Intercepta todas las llamadas pero sólo se ocupa
350
de algunas de ellas. En este último caso, es importante transmitir la llamada a una superclase, si se decide el no manejar en la implementación de method_missing: class MyClass < OtherClass def method_missing(name, *args, &block) if # manejar llamada else super # de lo contrario pasarlo end end end Si usted falla en pasar en las llamadas no manejadas, la aplicación ignorará las llamadas a métodos desconocidos en su clase.
Vamos a mostrar un par de usos de method_missing.
method_missing para Simular Accesores La clase OpenStruct se distribuye con Ruby. Esto permite escribir objetos con atributos que se crean dinámicamente por asignación. Por ejemplo, se podría escribir lo siguiente: require ‘ostruct’ obj = OpenStruct.new(name: “Dave”) obj.address = “Texas” obj.likes = “Programming” puts “#{obj.name} lives in #{obj.address} and likes #{obj.likes}” produce: Dave lives in Texas and likes Programming
Vamos a utilizar method_missing para escribir nuestra propia versión de OpenStruct:
class MyOpenStruct < BasicObject def initialize(initial_values = {}) @values = initial_values end def _singleton_class class << self self end end def method_missing(name, *args, &block) if name[-1] == “=” base_name = name[0..-2].intern _singleton_class.instance_exec(name) do |name| define_method(name) do |value| @values[base_name] = value end end @values[base_name] = args[0] else _singleton_class.instance_exec(name) do |name| define_method(name) do
351
@values[name] end end @values[name] end end end obj = MyOpenStruct.new(name: “Dave”) obj.address = “Texas” obj.likes = “Programming” puts “#{obj.name} lives in #{obj.address} and likes #{obj.likes}” produce: Dave lives in Texas and likes Programming Observe la forma de base de nuestra clase en BasicObject, una clase introducida en Ruby 1.9. BasicObject es la raíz de la jerarquía de objetos de Ruby y contiene sólo un número mínimo de métodos: p BasicObject.instance_methods produce: [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__] Esto está bien, porque significa que nuestra clase MyOpenStruct será capaz de tener atributos tales como display o clase. Si en su lugar hubiéramos basado MyOpenStruct en la clase Object, entoncesestos nombres, junto con cuarenta y nueve más, habrían sido predefinidos y por lo tanto no daría lugar a method_missing. Observe también otro patrón común dentro de method_missing. La primera vez que referencia o asignar a un atributo de nuestro objeto, podemos acceder o actualizar los @valores hash adecuadamente. Pero también define el método que la llamada está tratando de acceder. Esto significa que la próxima vez que se utilice este atributo, se utilizará el método y no se invocará method_missing. Esto puede o no puede valer la pena dependiendo de los patrones de acceso a su objeto. También hay que notar cómo hay que saltar a través de unos aros para definir el método. Queremos definir el método sólo para el objeto actual. Esto significa que tenemos que poner el método en la clase singleton del objeto. Podemos hacer esto utilizando instance_exec y define_method. Pero eso significa que tenemos que utilizar el truco class << self para obtener la clase singleton del objeto. A través de una sutil implementación interesante, define_method siempre define un método de instancia, independientemente de si se invoca a través de instance_exec o class_exec. Sin embargo, este código revela la parte más oscura de la utilización de method_missing y BasicObject. Considere lo siguiente: obj = MyOpenStruct.new(name: “Dave”) obj.address = “Texas” o1 = obj.dup o1.name = “Mike” o1.address = “Colorado” produce: prog.rb:37:in ‘’: undefined method ‘name=’ for nil:NilClass (NoMethodError)
352
El método dup no es definido por BasicObject, sino que aparece en la clase Object. Así que cuando se llama a dup, fue recogido por nuestro manejador method_missing, y justo retorna nil (porque no tenemos todavía un atributo llamado dup). Podríamos solucionar este problema para que al menos informe de error: def method_missing(name, *args, &block) if name[-1] == “=” # como antes... else super unless @values.has_key? name # como antes... end end Esta clase ahora informa de un error si llamamos en ella a dup (o a cualquier otro método). Sin embargo, todavía no podemos hacer dup o clone (o inspect, o convertir en una cadena, etc). Aunque BasicObjectparece algo natural para method_missing, puede ser más problemático de lo que parece.
method_missing como Filtro Como muestra el ejemplo anterior, method_missing tiene algunos inconvenientes si se utiliza para interceptar todas las llamadas. Probablemente es mejor usarlo para reconocer ciertos patrones de llamada, pasando aquellas que no reconoce a la clase padre para su manejo. Un ejemplo de ello es la funcionalidad de buscador dinámico del módulo ActiveRecord de Ruby on Rails. ActiveRecord es la biblioteca de objeto-relacional en Rails --que permite acceder a bases de datos relacionales como si fueran depósitos de objetos. Una característica particular le permite buscar las filas que cumplen los criterios de ciertos valores dados en determinadas columnas. Por ejemplo, si a una clase Active Record denominada Book se la asigna una tabla relacional llamada books y esta incluye columnas llamadas title y author, se podría escribir lo siguiente: pickaxe = Book.find_by_title(“Programming Ruby”) daves_books = Book.find_all_by_author(“Dave Thomas”) Active Record no predefine todos estos métodos de búsqueda posibles. En su lugar, utiliza nuestro viejo amigo method_missing. Dentro de este método, busca las llamadas a métodos no definidos que coinciden con el patrón /^find_(all_)?by_(.*)/7. Si el método que se invoca no coincide con este modelo o si los campos en el nombre del método no corresponden a las columnas en la tabla de la base de datos, Active Record llama a super para generar un verdadero informe method_missing.
Un Último Ejemplo Vamos a reunir todos los temas de metaprogramación que hemos discutido en un último ejemplo, escribiendo un módulo que nos permita rastrear o trazar la ejecución de los métodos de cualquier clase mezclada en el módulo. Podríamos escribir lo siguiente: require_relative ‘trace_calls’ class Example def one(arg) puts “One called with #{arg}” end end ex1 = Example.new ex1.one(“Hello”)
# no trazar para esta llamada
class Example include TraceCalls
7
También busca para /^find_or_(initialize|create)_by_(.*)/.
353
def two(arg1, arg2) arg1 + arg2 end end ex1.one(“Goodbye”) puts ex1.two(4, 5)
# pero veamos el traceo de estas dos
produce: One ==> One <== ==> <== 9
called with Hello calling one with [“Goodbye”] called with Goodbye one returned nil calling two with [4, 5] two returned 9
Podemos ver de inmediato que hay una sutileza aquí. Cuando mezclamos el módulo TraceCalls en una clase, tiene que añadir el traceo para cualquier método de instancia existente en esa clase. También tiene que hacer arreglos para añadir el traceo de los métodos que se añadan posteriormente.
Vamos a empezar con el listado completo del módulo TraceCalls:
module TraceCalls def self.included(klass) klass.instance_methods(false).each do |existing_method| wrap(klass, existing_method) end def klass.method_added(method) # note: definición anidada unless @trace_calls_internal @trace_calls_internal = true TraceCalls.wrap(self, method) @trace_calls_internal = false end end end def self.wrap(klass, method) klass.instance_eval do method_object = instance_method(method) define_method(method) do |*args, &block| puts “==> calling #{method} with #{args.inspect}” result = method_object.bind(self).call(*args, &block) puts “<== #{method} returned #{result.inspect}” result end end end end Al incluir este módulo en una clase, se invoca al método gancho included. En primer lugar, utiliza el método de reflexión instance_methods para encontrar todos los métodos de instancia existentes en la clase anfitriona (el parámetro false límita la lista a los métodos en la propia clase, y no en sus superclases). Para cada método existente, el módulo llama a un método de ayuda, wrap, para añadirle algo de código de traceo. Vamos a hablar de wrap en breve.
A continuación, el método included utiliza otro gancho, method_added. Este es llamado por Ruby
354
cuando se define un método en el receptor. Hay que tener en cuenta que se define este método en la clase que se pasa al método included. Esto significa que el método será llamado cuando los métodos son añadidos a esta clase anfitriona y no en el módulo. Esto es lo que nos permite incluir TraceCalls al principio de una clase y luego añadir los métodos de esa clase --todas las definiciones método serán manejadas por method_added. Ahora observe el código dentro del método method_added. Tenemos que lidiar con un potencial problema. Como se ve cuando nos fijamos en el método wrap, se añade el seguimiento de un método creando una nueva versión del método que llama al antiguo. Dentro de method_added, se llama a la función wrap para añadir este trazado. Pero dentro de wrap, vamos a definir un nuevo método para manejar este envoltorio y la definición va a invocar al method_added otra vez, y entonces se llama de nuevo a wrap y así sucesivamente, hasta que la pila se agote. Para evitar esto, se utiliza una variable de instancia y se hace el envoltorio sólo si no lo hemos echo ya. El método wrap toma un objeto de clase y el nombre de un método a envolver. Busca la definición original de este método (con instance_method) y lo guarda. A continuación, redefine este método. Este nuevo método produce salidas de trazado y luego llama al original, pasandole los parámetros y el bloque desde el envoltorio8. Fíjese en cómo se llama al método vinculando el método objeto a la instancia actual y luego invocando ese método vinculado. La clave para entender este código y la mayoría de código de metaprogramación, es seguir los principios básicos que hemos elaborado al comienzo de este capítulo --como self cambia como se llama a los métodos y se definen las clases y cómo se llama a los métodos mediante su búsqueda en la clase del receptor. Si se queda atascado, haga lo que hacemos nosotros y dibuje pequeñas cajas y flechas. Creemos que es útil seguir con la convención que se utiliza en este capítulo: los vínculos de clase van a la derecha y los vínculos de superclase arriba. Dado un objeto, una llamada a método es entonces una cuestión de encontrar el objeto receptor. Se va a la derecha una vez y luego siguiendo la cadena de superclase hacia arriba es por dónde tiene que ir.
Entorno de Ejecución de Nivel Superior Por último, hay un pequeño detalle que tenemos que cubrir para completar el entorno de la metaprogramación. Muchas veces en este libro hemos afirmado que todo en Ruby es un objeto. Sin embargo, hemos utilizado una y otra vez lo que parece contradecir esto --el entorno de ejecución de alto nivel de Ruby: puts “Hola, Mundo” No hay objeto a la vista. Nosotros, bien podríamos haber escrito alguna variante de Fortran o Basic. Sin embargo, cavando más profundo, nos econtraremos con objetos y clases que acechan en el más simple código. Sabemos que el literal “Hola, Mundo” genera un String de Ruby, por lo que es un objeto. También sabemos que la pelada llamada al método puts es efectivamente lo mismo que self.puts. Pero, ¿qué es self? self.class # => Object En el nivel superior, estamos ejecutando el código en el contexto de algún objeto predeterminado. Cuando definimos los métodos, en realidad estamos creando (privadamente) métodos de instancia para la clase Object. Esto es bastante sutil, ya que como están en la clase Object, estos métodos están disponibles en todas partes. Y ya que estamos en el contexto de Object, podemos utilizar todos los métodos de Object (incluidos los mixtos desde Kernel) en forma de función. Esto explica por qué podemos llamar a métodos Kernel tales como puts en el nivel superior (y de hecho a lo largo de todo Ruby), y es porque estos métodos son parte de cada objeto. Las variables de instancia de nivel superior también incumben a este objeto de nivel superior. La metaprogramación es una de los más agudas herramientas de Ruby. No hay que tener miedo de usarla para elevar el nivel en que se programa. Pero al mismo tiempo, hay que usarla sólo cuando sea necesario --demasiadas aplicaciones metaprogramadas pueden llegar a ser bastante oscuras rápidamente. 8 La capacidad de un bloque de tomar un parámetro de bloque fue introducido en Ruby 1.9. 355
El Progama de Gráficos Tortuga #--# Excerpted from “Programming Ruby 1.9”, # published by The Pragmatic Bookshelf. # Copyrights apply to this code. It may not be used to create training material, # courses, books, articles, and the like. Contact us if you are in doubt. # We make no guarantees that this code is fit for any purpose. # Visit http://www.pragmaticprogrammer.com/titles/ruby3 for more book information. #--class Turtle # directions: 0 = E, 1 = S, 2 = W, 3 = N # axis: 0 = x, 1 = y def initialize @board = Hash.new(“ “) @x = @y = 0 @direction = 0 pen_up end def pen_up @pen_down = false end def pen_down @pen_down = true mark_current_location end def forward(n=1) n.times { move } end def left @direction -= 1 @direction = 3 if @direction < 0 end def right @direction += 1 @direction = 0 if @direction > 3 end def walk(&block) instance_eval(&block) end def draw min_x, max_x = @board.keys.map{|x,y| x}.minmax min_y, max_y = @board.keys.map{|x,y| y}.minmax min_y.upto(max_y) do |y| min_x.upto(max_x) do |x| print @board[[x,y]] end puts end end private
356
def move increment = @direction > 1 ? -1 : 1 if @direction.even? @x += increment else @y += increment end mark_current_location end def mark_current_location @board[[@x,@y]] = “#” if @pen_down end end turtle = Turtle.new turtle.walk do 3.times do forward(8) pen_down 4.times do forward(4) left end pen_up end end turtle.draw produce: ##### # # # # # # #####
##### # # # # # # #####
##### # # # # # # #####
357