Aprendiendo Ruby on Rails

Conoce de afuera hacia adentro - Outside-in - el desarrollo de aplicaciones web con el marco de trabajo de Ruby on Rails 5. Aprender de la naturalidad de un lenguaje de programación como Ruby y de cómo Ruby on Rails 5 explota nuestra productividad.

Mario Alberto Chávez Cárdenas

Contenido

Bienvenido

Bienvenido a ésta aventura de conocer Ruby on Rails. Estas a punto adentrarte en el mundo de desarrollo Web a través de Ruby on Rails, él cual es conocido como un framework moderno que permite a los desarrolladores construir aplicaciones de manera muy rápida.

Ruby on Rails desde su llegada cambio la forma en como construimos aplicaciones Web, haciendo que nuestro trabajo como desarrolladores fuera más simple e inclusive divertido, características que hacen que éste framework sea tan atractivo para muchos desarrolladores.

Durante el curso de éste libro vamos a conocer que tiene Ruby on Rails que lo hace tan especial, pero antes de embarcarnos en ésta tarea, vamos a desviarnos un poco para iniciar por el principio, y el principio es el lenguaje de programación Ruby.

Capítulo 1 - Ruby, el lenguaje de los desarrolladores felices

Historia

Ruby es un lenguaje de programación de uso general, es decir no es lenguaje diseñado exclusivamente para el desarrollo Web, pero no podemos negar de que ha encontrado un nicho importante en ésta área.

La historia de Ruby se remonta hasta 1993, cuando fue creado por el japonés Yukihiro Matsumoto, mejor conocido como Matz. Ruby fue liberado al publico en 1995.

Desde entonces Ruby ha atraído a un gran número de desarrolladores debido a su diseño simple y sintaxis elegante.

Ruby es el resultado de la visión de Matz por crear un lenguaje de programación que mezcla ciertas características de sus lenguajes favoritos, según Matz, él tomo en cuenta:

Como podemos ver Matz tomó Lisp, Smalltalk y Perl para crear Ruby, inclusive Matz alguna vez comentó que Ruby realmente era Lisp en un inicio1.

Ruby cuenta con una sintaxis diferente a muchos lenguajes de programación, algunos desarrolladores se refieren a una sintaxis simple, pero Matz la define como:

"He intentado hacer Ruby natural, no simple"

Pero también ha comentado que:

"Ruby es sencillo en apariencia, pero es muy complejo internamente, tal como es el cuerpo humano"2.

El esfuerzo de Matz para lograr esto en Ruby ha sido exitoso, su sintaxis natural es simple de aprender, y es increíble como Ruby nos ayuda a ser productivos con muy pocas líneas de código. Éste nivel de productividad es lo que hace de los Rubystas ser desarrolladores felices.

¿Qué nos ofrece Ruby como lenguaje de programación?

Técnicamente Ruby es un lenguaje Orientado a Objetos, de hecho todo en Ruby es un objeto, bueno casi todo.

Con respecto a sus características de Orientación a Objetos, Ruby cumple con la definición de Alan Kay3, el creador del término Orientado a Objetos, es decir:

Además de lo anterior, Ruby, soporta la definición más común de programación orientado a objetos, donde podemos definir clases que a su vez pueden ser instanciadas a objetos en memoria.

La herencia de clase también es soportada, pero en el caso de Ruby, una clase solo puede heredar de una súper clase. Si bien esto puede sonar como una limitante contra otros lenguajes de programación, en Ruby es posible extender una clase mediante el uso de Módulos, es decir, una clase puede combinar más de un Modulo a un tiempo.

Otra característica que Ruby comparte con algunos de los lenguajes modernos de programación es el Garbage Collector o simplemente GC. El GC nos permite como desarrolladores el pedir un espacio de memoria para almacenar un dato, pero a diferencia de por ejemplo C, no tenemos que preocuparnos por liberar ese espacio de memoria explícitamente, en éste caso el GC se encarga de identificar los espacios que ya no son requeridos en cierto punto de ejecución de nuestro programa y marcar ese espacio para que pueda ser reutilizado para guardar nuevos datos.

El sistema de tipos de Ruby es dinámico, es decir no requiere de que explícitamente indiquemos el tipo de dato de una variable, Ruby al ejecutar nuestro código infiere el tipo de dato a partir del valor asignado. Esta cualidad de Ruby permite que nos enfoquemos en lo que un objeto puede hacer - hay que recordar que todo en Ruby es un objeto - y no en el tipo de dato, esto se conoce como Duck Typing.

Ruby pertenece a la categoría de los lenguajes dinámicos, es decir, Ruby no se compila como sucedería con un lenguaje por ejemplo de la familia de C. Nuestro código en Ruby es interpretado al momento de ejecutarse, esto sucede dentro de la maquina virtual de Ruby, YARV - Yet Another Virtual Machine -, conforme nuestro código va siendo cargado, YARV convierte el código a una representación intermedia donde le aplica micro-optimizaciones para después ser ejecutado.

La forma descrita de como Ruby se ejecuta a través de YARV es cierta cuando nos referimos al intérprete MRI, pero como veremos un poco mas adelante MRI no es el único interprete para Ruby.

El dinamismo de Ruby le otorga una de las herramientas más importantes del lenguaje: Metaprogramación. En términos simples Metaprogración nos permite escribir programas que generan programas. Metaprogración es una de las herramientas más importantes con las que cuenta un desarrollador de Ruby.

Las características anteriores son importantes para definir a Ruby como lenguaje, pero quizás la característica que es más visible y que hace de entrada que Ruby sea un lenguaje diferente es su sintaxis. Algunos definen que Ruby tiene algo llamado Syntactic Sugar, es un termino utilizado para definir que la sintaxis de Ruby no se interpone entre la idea y lo que se espera como resultado, es decir la sintaxis de Ruby nos ayuda a escribir código más conciso, expresivo y con menos líneas de código podemos escribir cosas complejas que en otros lenguajes se traduciría a algunas decenas de lineas de código.

Un efecto de la sintaxis de Ruby es la facilidad para escribir DSL o Lenguajes de dominio específico, es decir se puede escribir una nueva sintaxis encima de la sintaxis de Ruby que generalmente ahorra tiempo en ciertas tareas con Ruby, un ejemplo de este caso es Ruby On Rails, pero ya tocaremos ese tema más adelante en el libro.

Intérpretes de Ruby

En la sección anterior se hizo referencia a que Ruby puede ser ejecutado a través de más de un intérprete.

MRI

Él intérprete oficial de Ruby ha sido MRI o Matz Ruby Interpreter, éste es el intérprete original que ha acompañado a Ruby desde su lanzamiento.

MRI está escrito en C y debido a esto también se le conoce como CRuby.

A través del tiempo MRI ha ido cambiando con el objetivo de mejorar el rendimiento de los programas escritos en Ruby, técnicamente el lenguaje como tal ha sufrido una serie de cambios sintácticos, pero los cambios más grande que ha sufrido MRI han sido en términos de su funcionamiento interno, por ejemplo, a partir de la versión 1.9 incluir la máquina virtual YARV y la mejora del algoritmo del Garbage Collector.

MRI es un intérprete portable, es decir, puede ejecutarse en distintas plataformas y sistemas operativos. Podemos ejecutar programas de Ruby con MRI en:

La versión más reciente de MRI es la versión 2.1, la cual apenas fue liberada el 24 de febrero del 2014.

Rubinius

Otro de los intérpretes que nos permiten ejecutar Ruby es Rubinius4. Rubinius fue creado en el 2006 por Evan Phoenix.

Rubinius tiene como objetivo el proveer una implementación alterna para ejecutar Ruby en ambientes concurrentes y de la forma más rápida posible.

El alto grado de compatibilidad que Rubinius mantiene con MRI es el reflejo de los desarrolladores de Rubinius de simplificar la migración del MRI a Rubinius.

Una de las características de Rubinius que puede ser atractiva para los desarrolladores de Ruby, es que mantiene la tradición de lenguajes como LISP y SmallTalk de implementar, hasta donde sea posible, Ruby en código de Ruby.

Como parte del proyecto de Rubinius, en el 2006 también se creo RubySpecs5, es decir la especificación ejecutable del lenguaje de Ruby. Ésta especificación es la que ayuda a medir y calificar el nivel en que un intérprete implementa la sintaxis de Ruby y las clases de la librería estándar de Ruby.

La versión más reciente disponible de Rubinius es la versión 2.0, versión que es compatible con la versión 2.0 de MRI.

JRuby

JRuby6 es el punto donde Java y Ruby se interceptan. JRuby nos permite ejecutar programas de Ruby encima de la máquina virtual de Java, con todos los beneficios es ésta ultima nos provee.

La creación de Charles Nutter y Thomas Enebo, busca ofrecer una opción más para un Ruby más rápido y mejor soporte a hilos.

Adicional a ésto, JRuby permite que desde nuestros programas de Ruby podamos tener acceso a las librerías disponibles en el mundo Java, además de que es posible empotrar el intérprete de JRuby en programas de Java, proporcionándoles las capacidad de ejecutar scripts dinámicos.

En su versión más reciente, JRuby 1.7.5, ofrece soporte experimental compatible con la versión 2.0 de MRI.

Iniciando con Ruby

Ya que tenemos un poco más de contexto sobre Ruby, vamos a aprender de su sintaxis y de la comunidad al rededor del lenguaje.

Instalando Ruby

Los tres entornos más comunes para ejecutar Ruby son: Linux, OSX y Windows.

Dependiendo de la plataforma donde se desea ejecutar Ruby son las opciones que tenemos para instalarlo.

Windows

En Windows la instalación es más sencilla gracias a RailsInstaller7, sólo es necesario descargar el instalador, seguir el asistente de instalación y en cuestión de minutos Ruby va a estar listo para utilizarse.

Para los ejemplos y ejercicios de éste libro necesitamos por lo mínimo Ruby 1.9.3; en RailsInstaller existe un instalador para Ruby 2.0.0 pero aún hay algunos detalles con ciertas librerías en Windows para ésta versión de Ruby.

OSX

Para OSX es posible instalar Ruby con un instalador de RailsInstaller, aunque quizás la opción mas práctica es instalarlo desde código fuente.

Para poder instalar Ruby 2.1.2 desde código fuente es necesario realizar las siguientes actividades - el paso 4 va a estar definido a detalle un poco más abajo -:

  1. Instalar la última versión de XCode8 disponible en el sitio de Apple.
  2. Una vez instalado XCode, hay que abrirlo e ir a Preferencias y después a Descargas, ahí hay que instalar Command Line Tools.
  3. Instalar Homebrew9, el cual es un manejador de paquetes para OSX.
  4. Instalar Ruby.

En la terminal de OSX instalamos rbenv10 y ruby-build11 con la ayuda de Homebrew, ambos paquetes nos ayudan a instalar y manejar nuestra instalación de Ruby en OSX de una manera simple.

$ brew update
$ brew install rbenv
$ brew install ruby-build
$ rbenv install 2.1.2
$ rbenv rehash
$ rbenv global 2.1.2

El comando rbenv install 2.1.2 se encarga de descargar, compilar e instalar Ruby, por lo que puede tardar varios minutos en ejecutarse. El último comando configura el ambiente para usar Ruby 2.1.2 por omisión.

Para mayor información en como funcionan rbenv y ruby-build hay que visitar sus repositorios en Github.

Linux

Para los usuarios de Linux las instrucciones varían un poco entre las diferentes distribuciones y los sistemas de paquetes de cada una; obviamente va a ser muy difícil cubrirlas todas, por lo que las instrucciones siguientes va a estar basadas en la distribución Ubuntu Precise 12.4.

Los pasos para instalar Ruby de forma muy general se describen a continuación, el detalle de los mismos aparece un poco más abajo con los comandos a ejecutar en la terminal.

  1. Actualizar nuestro sistema operativo
  2. Instalar librerías requeridas para compilar Ruby
  3. Instalar rbenv y ruby-build
$ sudo apt-get update
$ sudo apt-get install zlib1g-dev openssl libopenssl-ruby1.9.1 libssl-dev libruby1.9.1 libreadline-dev git-core
$ cd ~
$ git clone git://github.com/sstephenson/rbenv.git .rbenv
$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(rbenv init -)"' >> ~/.bashrc
$ exec $SHELL
$ mkdir -p ~/.rbenv/plugins
$ cd ~/.rbenv/plugins
$ git clone git://github.com/sstephenson/ruby-build.git
$ cd ~
$ rbenv install 2.1.2
$ rbenv rehash
$ rbenv global 2.1.2

Ruby instalado y listo para usarse

En este punto sin importar el sistema operativo en que estés trabajando, Ruby ya debe de estar listo para utilizarse y para comprobar que así es, en la terminal ejecutamos el siguiente comando, con el cual vamos a poder ejecutar la consola interactiva de Ruby:

$ irb
irb(main):001:0>

Si al ejecutarla se nos muestra un error, hay que revisar los pasos de instalación y volver a probar, si estamos en la consola, hay que teclear exit y presionamos la tecla de enter para salir.

Estructura de Ruby

El lenguaje Ruby está estructurado en 3 grandes bloques, el Core12, las librerías estándar13 y las gemas14.

Core

El Core representa el fundamento del lenguaje en términos del interprete, máquina virtual, garbage collector y la sintaxis.

Librerías estándar

El conjunto de librerías que extienden el funcionamiento de Ruby más allá de su sintaxis es definido por las librerías estándar.

Gemas

Finalmente encontramos las Gemas o Gems que son librerías desarrolladas por la comunidad, RubyGems es el repositorio donde se publican las Gemas de Ruby. Actualmente RubyGems cuenta con alrededor de 64,500 gemas registradas.

Conociendo la sintaxis de Ruby

En este punto ya estamos listos para poder empezar a conocer ciertos aspectos de la sintaxis de Ruby.

En ésta sección nos vamos a enfocar en aspectos que nos permitan iniciar de forma rápida con Ruby, pero implica que vamos a dejar otros aspectos fuera; revisar todo lo que podemos hacer con Ruby puede llevar todo un libro completo, dado que este libro no ésta enfocado en el lenguaje exclusivamente se justifica ésta decisión.

Para ejecutar los ejemplos de código utilizares irb que como ya vimos es la consola de modo interactivo de Ruby y que nos permite introducir código de Ruby y hacer que éste se evalue al momento.

Iniciemos la consola interactiva desde la terminal de nuestro sistema operativo con:

$ irb

En cada evaluación el resultado de la misma es presentado en las siguientes lineas y aparece indicada con =>, símbolo que en Ruby se conoce como Hash Rocket.

irb(main):001:0> 1
=> 1

El prompt de la consola es irb(main):001:0>, cada vez que veamos una línea similar implica que la consola está esperando por nosotros para introducir alguna instrucción.

Si introducimos una sintaxis inválida o que lleve a una evaluación que produzca un error, obtendremos un mensaje como el mostrado a continuación, indicando el tipo de error, un mensaje y la línea de código que lo provocó.

irb(main):001:0> x
NameError: undefined local variable or method `x' for main:Object
   from (irb):4
   from /.rbenv/versions/2.1.2/bin/irb:12:in `<main>'

Variables

Una variable en Ruby es representada por un identificador, el cual le da el nombre a la variable. El identificador debe de ser una nombre válido, por ejemplo no iniciar con un número o tener el nombre de una palabra reservada de Ruby.

Como convención en Ruby un nombre de variable siempre se escribe en minúsculas y si el nombre de la variable esta conformado por más de una palabra, cada palabra va separada por un guión bajo _, a esta forma de nombrar se le conoce como snake_case.

De igual forma por convención todos los nombres de identificadores se escriben en inglés.

irb(main):001:0> name = 'Ruby'
=> "Ruby"
irb(main):002:0> quantity = 1
=> 1

Como vemos en el ejemplo, para asignar un valor a una variable simplemente usamos el signo de =, el cual precisamente implica asignación y se lee: Asigna el valor de la derecha a la variable de la izquierda.

Debido al sistema de tipos dinámicos de Ruby no es necesario especificar el tipo de dato de la variable, Ruby infiere el tipo de dato a partir del valor que se asigna a la variable.

Ésta característica de Ruby también permite redefinir una variable con un tipo de dato diferente.

irb(main):001:0> name = 'Ruby'
=> "Ruby"
irb(main):002:0> name = 1
=> 1

Es completamente válido en Ruby, aunque en términos prácticos esto puede llevar a problemas graves en un programa, por lo que los programadores de Ruby tratan de evitar redefinir variables a tipos de datos diferentes al original o a lo que el nombre de la variable implica.

Para imprimir el valor de una variable en la terminal simplemente escribirnos el nombre de la variable y presionamos enter.

irb(main):001:0> name = 'Ruby'
=> "Ruby"
irb(main):002:0> name
=> "Ruby"

Constantes

Una constante, al igual que una variable, nos sirve para poder asignar un valor a un identificador que nos permita acceso rápido a éste valor. Pero a diferencia de una variable, una constante no cambia su valor durante la vida de un programa.

La convención de nombre para una constante es similar a la convención para una variable, con la única diferencia de el nombre de la constante se escribe en mayúsculas.

irb(main):001:0> LANGUAGE = 'Ruby'
=> "Ruby"
irb(main):002:0> LANGUAGE
=> "Ruby"

Mencionamos que el valor de una constante no cambia durante la vida de un programa, pero en realidad el lenguaje Ruby si permite modificar el valor de una constante, pero al modificar el valor MRI despliega una advertencia del cambio.

irb(main):001:0> LANGUAGE = 'Ruby'
=> "Ruby"
irb(main):002:0> LANGUAGE = 1
(irb):3: warning: already initialized constant LANGUAGE
(irb):1: warning: previous definition of LANGUAGE was here
=> 1

Números

Ahora que ya sabemos como definir variables para guardar datos en Ruby, vamos conociendo que tipo de datos son los que podemos guardar.

En términos de valores numéricos en Ruby podemos trabajar con valores enteros y fraccionarios.

irb(main):001:0> 10
=> 10
irb(main):002:0> -30
=> -30
irb(main):003:0> 0
=> 0
irb(main):004:0> 774747474747474
=> 774747474747474

Los ejemplos anteriores pertenecen a la representación de números enteros positivos, negativos y el cero. En Ruby los valores enteros se representan con el tipo de datos FixNum.

Los números fraccionarios, que en Ruby se representan con el tipo de dato float.

irb(main):005:0> 3.1416
=> 3.1416
irb(main):006:0> -54.45
=> -54.45
irb(main):007:0> 0.0002
=> 0.0002
irb(main):008:0> 0.0
=> 0.0

Con los valores numéricos es posible realizar operaciones aritméticas, para tal efecto Ruby cuenta con una serie de operadores binarios:

irb(main):011:0> 2+5
=> 7
irb(main):012:0> 8-4
=> 4
irb(main):013:0> 3*6
=> 18
irb(main):014:0> 24/5
=> 4
irb(main):015:0> 24%5
=> 4

Para el caso de la división notamos que el consiente solo incluye la parte entera, y en la siguiente operación, módulo, vemos que el residuo de la misma division es diferente a cero, si nuestra intención en la division es mostrar el cociente con la parte decimal, entonces podemos replantear la division de la siguiente forma:

irb(main):016:0> 24/5.0
=> 4.8

Al forzar la division con uno de los valores como float, forzamos a que el resultado de la misma también sea float.

Como vimos en los ejemplos anteriores con Ruby es posible realizar operaciones aritméticas. Para realizar la evaluación de las operaciones, Ruby observa la procedencia de operadores.

irb(main):001:0> 1 + 2 * 3 / 4.0
=> 2.5

Si deseamos modificar el orden en como se evalúan las operaciones, entonces las agrupamos con paréntesis.

irb(main):001:0> (1 + 2) * (3 / 4.0)
=> 2.25

Cadenas de texto

Ruby cuenta con un tipo de datos para almacenar cadenas de texto, éste tipo de dato es String. Una cadena de texto en Ruby puede contener caracteres invisibles, generalmente estos caracteres se conocen como caracteres de control y sirven, por ejemplo, para indicar saltos de línea.

Para expresar una cadena de texto podemos utilizar la comillas simple ' o la doble comillas ". Como veremos hay algunas diferencias entre utilizar una u otra.

irb(main):001:0> text = 'Hola Ruby'
=> "Hola Ruby"

En el ejemplo anterior utilizamos la comillas simple para representar una cadena de texto. En el caso de éste tipo de comilla podemos escapar ciertos caracteres con la ayuda de \.

irb(main):001:0> text = 'Hola \'Ruby\''
=> "Hola 'Ruby'"
irb(main):002:0> text = 'Hola \\Ruby\\'
=> "Hola \\Ruby\\"
irb(main):003:0> puts text
Hola \Ruby\
=> nil

En el último ejemplo hacemos uso de la instrucción puts para indicarle a Ruby de que deseamos imprimir el contenido de la variable text, con puts se interpreta correctamente el escape de \\.

Con el uso de las comillas dobles podemos escapar una mayor cantidad de caracteres, incluyendo los caracteres de control.

irb(main):001:0> text = "Hola Ruby"
=> "Hola Ruby"
irb(main):002:0> text = "Hola \"Ruby\""
=> "Hola \"Ruby\""
irb(main):003:0> puts text
Hola "Ruby"
=> nil
irb(main):004:0> text = "Hola \\Ruby\\"
=> "Hola \\Ruby\\"
irb(main):005:0> puts text
Hola \Ruby\
=> nil
irb(main):006:0> text = "Hola Ruby\n\n"
=> "Hola Ruby\n\n"
irb(main):007:0> puts text
Hola Ruby

=> nil

En el último ejemplo vemos como al usar la instrucción puts para desplegar el contenido de la variable el carácter de control \n es interpretado correctamente como salto de línea.

Otra diferencia importante entre utilizar la comillas simple y la comillas doble en cadenas de texto es la interpolación. Veamos el ejemplo donde tenemos una cadena de texto, pero queremos formarla a partir de un valor declarado en otra variable.

irb(main):001:0> days = 2
=> 2
irb(main):002:0> text = "Han pasado " + days.to_s + " días"
=> "Han pasado 2 días"

Hay que notar que para poder utilizar el valor de la variable days al concatenar las cadenas de texto, tuvimos que hacer una conversion del valor numérico a cadena de texto, esto lo logramos con la llamada al método to_s.

Gracias a la interpolación en Ruby, podemos hacer que éste tipo de tareas sea más sencilla.

irb(main):001:0> days = 2
=> 2
irb(main):002:0> text = "Han pasado #{days} días"
=> "Han pasado 2 días"
irb(main):003:0> text = "Han pasado #{days * 2} días"
=> "Han pasado 4 días"

Como vemos en los ejemplos, con el uso de #{...} podemos interpolar un valor proveniente de otra variable en una cadena de texto. En el último ejemplo inclusive podemos ver que el valor a interpolar puede ser código normal de Ruby, el cual será interpretado e interpolado en la cadena.

En Ruby es posible que nosotros indiquemos el carácter delimitador para una cadena de texto, para éste caso contamos con las literales %q y %Q que tienen la funcionalidad de la comilla simple y la comilla doble respectivamente.

irb(main):001:0> texto = %q{Hola 'Ruby'}
=> "Hola 'Ruby'"
irb(main):002:0> texto = %q!Hola 'Ruby'!
=> "Hola 'Ruby'"
irb(main):003:0> texto = %Q!Hola 'Ruby'!
=> "Hola 'Ruby'"
irb(main):004:0> texto = %Q!Hola #{nombre}!
=> "Hola Ruby"
irb(main):005:0> texto = %Q!Hola #{nombre * 3}!
=> "Hola RubyRubyRuby"

Otra forma de declarar cadenas de texto con Ruby es mediante el uso de here document, ésta modalidad nos permite declarar una cadena de texto de múltiples líneas sin la necesidad de usar caracteres de escape.

irb(main):001:0> text = <<_DOC_
irb(main):002:0" Hola Ruby
irb(main):003:0" Esta es una declaración de
irb(main):004:0" cadena de texto multilínea
irb(main):005:0" _DOC_
=> "Hola Ruby\nEsta es una declaración de\ncadena de texto multilínea\n"
irb(main):006:0> puts text
Hola Ruby
Esta es una declaración de
cadena de texto multilínea
=> nil
irb(main):002:0" Hola Ruby
irb(main):003:0" Esta es una declaración de
irb(main):004:0" cadena de texto multilínea
irb(main):005:0" _DOC_
=> "Hola Ruby\nEsta es una declaración de\ncadena de texto multilínea\n"
irb(main):006:0> puts text
Hola Ruby
Esta es una declaración de
cadena de texto multilínea
=> nil

El delimitador _DOC_ puede ser cualquier identificador, por ejemplo _MSG_.

En Ruby casi todo es un objeto

Hasta éste momento hemos visto como trabajar con tipos de datos básicos como números y cadenas de texto, pero en Ruby tienen un trato diferente, ya que en Ruby se tiene la noción de que casi todo es un objeto.

Ésta aserción es relativamente fácil de comprobar.

irb(main):001:0> 4.class
=> Fixnum
irb(main):002:0> "Hola Ruby".class
=> String

Con el ejemplo anterior podemos ver que podemos enviar mensajes a éstos tipos de datos primitivos. La forma de enviar el mensaje al dato primitivo nos puede ser familiar, ya que la notación es la misma que en la mayoría de los lenguajes orientados a objetos, en donde usamos el . y acto seguido el nombre del mensaje.

En Ruby podemos consultar que mensajes podemos enviar a un objeto, veamos el caso de los mensajes disponibles para el valor 4.

irb(main):001:0> 4.methods
=> [:to_s, :-@, :+, :-, :*, :/, :div, :%, :modulo, :divmod, :fdiv, :**, :abs, :magnitude, :==, :===, :<=>, :>, :>=, :<, :<=, :~, :&, :|, :^, :[], :<<, :>>, :to_f, :size, :zero?, :odd?, :even?, :succ, :integer?, :upto, :downto, :times, :next, :pred, :chr, :ord, :to_i, :to_int, :floor, :ceil, :truncate, :round, :gcd, :lcm, :gcdlcm, :numerator, :denominator, :to_r, :rationalize, :singleton_method_added, :coerce, :i, :+@, :eql?, :quo, :remainder, :real?, :nonzero?, :step, :to_c, :real, :imaginary, :imag, :abs2, :arg, :angle, :phase, :rectangular, :rect, :polar, :conjugate, :conj, :between?, :nil?, :=~, :!~, :hash, :class, :singleton_class, :clone, :dup, :initialize_dup, :initialize_clone, :taint, :tainted?, :untaint, :untrust, :untrusted?, :trust, :freeze, :frozen?, :inspect, :methods, :singleton_methods, :protected_methods, :private_methods, :public_methods, :instance_variables, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :instance_of?, :kind_of?, :is_a?, :tap, :send, :public_send, :respond_to?, :respond_to_missing?, :extend, :display, :method, :public_method, :define_singleton_method, :object_id, :to_enum, :enum_for, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__, :__id__]

En el listado de los mensajes disponibles para el tipo de dato FixNum vemos que los mensajes aparecen con una notación especial que antepone el : al nombre del mensaje, ésta notación en Ruby se conoce como Símbolo, de momento es suficiente con saber como se llama ésta notación, más adelante veremos más detalle sobre los Símbolos.

Veamos algunos ejemplo de los mensajes que podemos enviar a un tipo de datos FixNum.

irb(main):001:0> 4.even?
=> true
irb(main):002:0> 4.next
=> 5
irb(main):003:0> 4.between? 3, 6
=> true

Símbolos

Los símbolos en Ruby son una característica un poco extraña, principalmente si solo se tiene experiencia con lenguajes basados en C. Los símbolos se utilizan de manera intensiva dentro de los programas en Ruby.

Los siguientes ejemplos corresponden a símbolos en Ruby:

irb(main):001:0> :hola_ruby
=> :hola_ruby
irb(main):002:0> :hola_ruby.class
=> Symbol
irb(main):001:0> :"Hola Ruby"
=> :"Hola Ruby"
irb(main):002:0> :"Hola Ruby".class
=> Symbol

En ambos casos la clase es Symbol sin importar la diferencia de escribirlos con " o sin ella, aunque la primera notación en forma snake_case es la más común.

Un símbolo es realmente una cadena de texto, pero inmutable, es decir no se puede modificar el contenido como se podría hacer con una cadena de texto convencional.

irb(main):001:0> 'Hola Ruby' << ' !!!!'
=> "Hola Ruby !!!!"
irb(main):002:0> :"Hola Ruby" << ' !!!!'
NoMethodError: undefined method `<<' for :"Hola Ruby":Symbol
  from (irb):2
  from from /.rbenv/versions/2.1.2/bin/irb:12:in `<main>'

En términos de un programa en Ruby, un símbolo generalmente representa una idea abstracta en lugar de representar un valor. Debido a esto Ruby trata de forma diferente a un símbolo en comparación a una cadena de texto. Como una cadena de texto es mutable, Ruby necesita guardar en memoria una copia de su contenido cada vez que se utiliza dicha cadena de texto.

irb(main):001:0> "hola mundo".object_id
=> 70144093356720
irb(main):002:0> "hola mundo".object_id
=> 70144093330220

El object_id de un objeto en Ruby, podemos pensar como una forma de representar su ubicación en en memoria, aunque esto no es del todo cierto ya que no indica su posición en la memoria física.

En el ejemplo anterior podemos ver como al utilizar la cadena cadena de texto, Ruby crea una copia por cada cadena de texto, esto lo podemos observar con solo ver el numero que retorna el envío del mensaje object_id.

Ahora veamos el mismo ejemplo, pero para símbolos.

irb(main):001:0> :hola_ruby.object_id
=> 457768
irb(main):002:0> :hola_ruby.object_id
=> 457768

Ahora el resultado del envío del mensaje object_id para ambos casos nos retorna el mismo valor, debido a esto podemos asumir que no importa cuantas ocasiones declaremos el mismo símbolo Ruby no crea copias del mismo, en su lugar los reutiliza.

Esto se debe a que los símbolos son almacenados en otra parte de memoria, donde el garbage collector no recicla el espacio de memoria de los símbolos; esto quiere decir que una vez que declaramos un símbolo, este permanece en memoria durante la vida del programa.

Podemos acceder a la estructura donde se almacenan los símbolos mediante el objeto Symbol.

irb(main):001:0> Symbol.all_symbols.size
=> 2934
irb(main):002:0> Symbol.all_symbols[1..30]
=> [:"<IFUNC>", :"<CFUNC>", :respond_to?, :"core#set_method_alias", :"core#set_variable_alias", :"core#undef_method", :"core#define_method", :"core#define_singleton_method", :"core#set_postexe", :each, :length, :size, :lambda, :intern, :gets, :succ, :method_missing, :send, :__send__, :initialize, :_, :__autoload__, :__classpath__, :__tmp_classpath__, :__classid__, :__attached__, :BasicObject, :Object, :Module, :Class]

Es posible realizar conversiones entre un símbolo y una cadena de texto y viceversa, en el caso de los símbolos, estos puede recibir el mensaje to_s y de esa forma transformar a cadena de texto, de igual forma una cadena de texto puede recibir el mensaje to_sym para obtener su representación en forma de símbolo.

irb(main):001:0> 'hola ruby'.to_sym
=> :"hola ruby"
irb(main):002:0> :hola_ruby.to_s
=> "hola_ruby"

Arreglos

Ruby cuenta con estructuras de datos que nos permiten almacenar información que tiene cierta relación entre sí. Un ejemplo de éste tipo de estructuras es el Arreglo o Array.

Un Array puede almacenar una lista de elementos homogéneos, es decir que pueden ser de tipos base diferentes, los cuales pueden ser referenciados por un indice, el cual representa su posición dentro de la lista. El indice de elementos de un Array inicia en la posición 0 - cero -.

Para representar un Array en Ruby, utilizamos los delimitadores [ y ] y cada elemento contenido está separado por una ,.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values.class
=> Array

Para acceder a unos de los elementos del Array solo pasamos su posición con el envío del mensaje [].

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values[0]
=> 1
irb(main):003:0> values[2]
=> 3

En Ruby, a diferencia de otros lenguajes, si pasamos una posición de indice donde no exista elemento no obtenemos un error de que el indice está fuera de rango, en su lugar simplemente obtenemos un nil

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values[10]
=> nil

En el caso de que pasemos una posición de indice negativa, entonces Ruby va a leer los elementos de derecha a izquierda, siendo el elemento más a la derecha el de posición -1.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values[-1]
=> 4
irb(main):002:0> values[-2]
=> 3
irb(main):002:0> values[-5]
=> nil

Si lo queremos es obtener de un Array un grupo de elementos contiguos, entonces podemos indicar un Rango o Range de elementos, en lugar de pasar solo el indice de una posición.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values[1..3]
=> [2, 3, 4]

Para agregar nuevos elementos al final un Array podemos utilizar el operador << o el mensaje push.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values << 5
=> [1, 2, 3, 4, 5]
irb(main):003:0> values
=> [1, 2, 3, 4, 5]
irb(main):004:0> values.push 6
=> [1, 2, 3, 4, 5, 6]
irb(main):005:0> values
=> [1, 2, 3, 4, 5, 6]

Pero si lo que queremos es agregar un elemento al principio del Array, entonces podemos hacer uso del mensaje unshift.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values.unshift 0
=> [0, 1, 2, 3, 4]
irb(main):003:0> values
=> [0, 1, 2, 3, 4]

A un Array es posible realizarle las operaciones aritméticas de + y *.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values + values
=> [1, 2, 3, 4, 1, 2, 3, 4]
irb(main):003:0> values * 3
=> [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4]

Un Array en Ruby es un objeto, por lo tanto puede recibir una serie de mensajes que nos permiten realizar operaciones interesantes, podemos observar que mensajes soporta un Array con el envío del mensaje 'methods' que ya vimos anteriormente.

irb(main):001:0> values = [1, 2, 3, 4]
=> [1, 2, 3, 4]
irb(main):002:0> values.rotate
=> [2, 3, 4, 1]
irb(main):003:0> values.empty?
=> false
irb(main):004:0> values.shuffle
=> [2, 3, 1, 4]
irb(main):005:0> values.join ', '
=> "1, 2, 3, 4"
irb(main):006:0> values.include? 3
=> true

Anteriormente en ésta sección de Array se mencionó el concepto de Rango para seleccionar un subconjunto de elementos de un Array; volveremos a utilizar un Rango para crear un Array que contenga un conjunto de elementos contiguos.

irb(main):001:0> (1..10).to_a
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
irb(main):002:0> ('c'..'m').to_a
=> ["c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m"]

Diccionarios

Otra de las estructuras de las que disponemos en Ruby son los diccionarios o simplemente Hash. Ésta estructura nos permite contener pares de elementos relacionados, donde una parte del par representa una llave y la segunda parte representa un valor.

En un Hash la llave nos sirve como elemento para acceder al valor con el que está relacionada.

irb(main):001:0> numbers = { 'uno' => 1, 'dos' => 2, 'tres' => 3 }
=> {"uno"=>1, "dos"=>2, "tres"=>3}
irb(main):002:0> numbers['dos']
=> 2

Como vemos en el ejemplo anterior los elementos de un Hash se delimitan por { y }, los elementos van separados por una , y el par se presenta con la llave a la izquierda y el valor a la derecha del => - Hashrocket -.

En un Hash no pueden haber 2 elementos con misma llave, pero si pueden haber 2 elementos con el mismo valor.

irb(main):001:0> numbers = { 'uno' => 1, 'dos' => 2, 'tres' => 3 }
=> {"uno"=>1, "dos"=>2, "tres"=>3}
irb(main):002:0> numbers['tres'] = 2
=> 2
irb(main):003:0> numbers
=> {"uno"=>1, "dos"=>2, "tres"=>2}

En Ruby la forma más común de representar la llave de un elemento es mediante el uso de símbolos.

irb(main):001:0> numbers = { :uno => 1, :dos => 2, :tres => 3 }
=> {:uno=>1, :dos=>2, :tres=>3}
irb(main):002:0> numbers[:dos]
=> 2

A partir de la versión 1.9 de Ruby la sintaxis de un Hash se puede representar sin el uso del =>.

irb(main):001:0> numbers = { uno: 1, dos: 2, tres: 3 }
=> {:uno=>1, :dos=>2, :tres=>3}
irb(main):002:0> numbers[:dos]
=> 2

Un Hash puede contener como valor un tipo de dato primitivo, como una cadena de texto o un entero, o bien un Array u otro Hash.

Finalmente, y no creo que sea sorpresa, un Hash es un objeto que puede recibir una numero de mensajes para realizar algunas operaciones con los datos que contiene.

irb(main):001:0> numbers = { uno: 1, dos: 2, tres: 3 }
=> {:uno=>1, :dos=>2, :tres=>3}
irb(main):002:0> numbers.has_key? :cuatro
=> false
irb(main):003:0> numbers.has_value? 1
=> true
irb(main):004:0> numbers.keys
=> [:uno, :dos, :tres]
irb(main):005:0> numbers.values
=> [1, 2, 3]

Métodos en Ruby

Hasta este momento se ha hecho referencia a que los objetos reciben mensajes, en el espíritu de la definición de Alan Kay, pero estos mensajes también se les conoce como métodos, nombre con el cual nos referiremos de ahora en adelante en el libro.

En Ruby al iniciar una terminal interactiva (IRB), automáticamente se crea un objeto raíz main, el cual es la instancia receptora cuando se define algún objeto dentro de Ruby, es fácil comprobar la existencia de este objeto, simplemente tenemos que iniciar una sesión en la terminal y ejecutar:

irb(main):001:0> self
=> main
irb(main):002:0> self.class
=> Object

Debido a esto, podemos definir métodos al objeto main sin la necesidad definir una clase - El tema de clases lo visitaremos un poco mas adelante -.

irb(main):001:0> def hello
irb(main):002:1>   puts 'Hola mundo'
irb(main):003:1> end
=> :hello
irb(main):004:0> hello
Hola mundo
=> nil

La palabra clave def seguida de un nombre valido define un método con ese nombre, los nombres de métodos al igual que las variables utilizan la convención de snake_case.

El cuerpo del método contiene el código de Ruby a ejecutar, por convención el cuerpo se identa 2 espacios a la derecha, el final del cuerpo del método se marca con la palabra clave end.

Para ejecutar el método simplemente introducimos el nombre en la consola y obtenemos el resultado esperado.

A diferencia de otros lenguajes no hay que especificar el tipo de datos que un método va a retornar o declararlas como void.

En Ruby la última evaluación que se realiza en el cuerpo de un método es el resultado que se retorna, es por eso que en el ejemplo anterior la ejecución del método hello retorna un nil.

En el siguiente ejemplo se puede apreciar que la evaluación de la última línea del cuerpo del método es el valor que se retorna.

irb(main):001:0> def circunference
irb(main):002:1>   ratio = 2
irb(main):003:1>   pi = 3.1416
irb(main):004:1>   pi * (ratio * ratio)
irb(main):005:1> end
=> :circunference
irb(main):006:0> circunference
=> 12.5664

En Ruby existe la palabra clave return pero como vimos en el ejemplo, no es necesario que la utilicemos de manera explícita, a menos de que quisiéramos terminar la ejecución de un método antes de que llegue al final del cuerpo.

irb(main):001:0> def circunference
irb(main):002:1>    ratio = 2
irb(main):003:1>    pi = 3.1416
irb(main):004:1>    return pi * (ratio * ratio)
irb(main):005:1> end
=> :circunference
irb(main):006:0> circunference
=> 12.5664

Un método que acepta parámetros puede ser un poco más interesante.

irb(main):001:0> def triangle_area(base, height)
irb(main):002:1>   (base * height) / 2.0
irb(main):003:1> end
=> :triangle_area
irb(main):004:0> triangle_area 3.0, 5.0
=> 15.0
irb(main):005:0> triangle_area
ArgumentError: wrong number of arguments (0 for 2)
from (irb):1:in `triangle_area'
from (irb):5
from /.rbenv/versions/2.1.2/bin/irb:12:in `<main>'

Como vemos en la ejecución de nuestra función, no es necesario especificar los paréntesis para indicar cuales son nuestros parámetros, solo son requeridos cuando queremos eliminar ambigüedad.

Si llamamos nuestra función sin pasar alguno o todos los parámetros obtenemos el error de "wrong number of arguments (X for Y)". Si queremos que alguno de esos parámetros sean opcionales entonces en la declaración debemos de asignarle un valor en caso de omisión.

irb(main):001:0> def triangle_area(base = 1, altura = 1)
irb(main):002:1>   (base * altura) / 2.0
irb(main):003:1> end
=> :triangle_area
irb(main):004:0> triangle_area
=> 0.5
irb(main):005:0> triangle_area 2
=> 1.0
irb(main):006:0> triangle_area 2, 3
=> 3.0

Los parámetros se pasan en el orden en que están definidos en la declaración de la función, pero en Ruby y en algunas librerías vamos a notar que estas aceptan parámetros con nombre; esto se logra aceptando solamente un parámetro como Hash y extrayendo los valores a variables independientes:

irb(main):001:0> def triangle_area(params = {})
irb(main):002:1>   base = params[:base] || 1
irb(main):003:1>   altura = params[:altura] || 1
irb(main):004:1>
irb(main):005:1*   (base * altura) / 2.0
irb(main):006:1> end
=> :triangle_area
irb(main):007:0> triangle_area altura: 2, base: 4
=> 4.0

Nota: El operador || evalúa el resultado de la izquierda primero y si es nil entonces evalúa el resultado de la derecha.

En este caso el Hash nos permite simular parámetros con nombre, por lo tanto podemos pasarlos en cualquier orden a la función, también es importante notar que anteriormente mencionamos que para declarar un Hash se tenia que realizar con las llaves { }, pero en el caso de pasar un Hash como parámetro a una función las llaves no son necesarias.

En Ruby 2.0 los parámetros con nombre han sido incluidos como parte de la funcionalidad, por lo tanto podemos reescribir la misma función con sintaxis de Ruby 2.0 como sigue:

irb(main):001:0> def triangle_area(base: 1, altura: 1)
irb(main):002:1>   (base * altura) / 2.0
irb(main):003:1> end
=> :triangle_area
irb(main):004:0> triangle_area altura: 2, base: 5
=> 5.0

Ya vimos varias combinaciones para definir parámetros a funciones en Ruby, esta ultima variación nos va a permitir recibir un numero ilimitado de parámetros, para tal efecto utilizamos el operador splat; este operador tiene solo una restricción, debe de ser el ultimo parámetro en la definición de una función.

irb(main):001:0> def multi_params(*params)
irb(main):002:1>   puts "Parametro 1 #{params[0]}"
irb(main):003:1>   puts "Parametro 2 #{params[1]}"
irb(main):004:1>   puts "Parametro 3 #{params[2]}"
irb(main):005:1>   puts "Parametro 4 #{params[3]}"
irb(main):006:1>   puts "Parametro 5 #{params[4]}"
irb(main):007:1> end
=> :multi_params
irb(main):008:0> multi_params 'a', 'b', 'c', 1, 2
Parametro 1 a
Parametro 2 b
Parametro 3 c
Parametro 4 1
Parametro 5 2
=> nil

Ruby permite múltiple asignación, podemos aprovechar esa funcionalidad para acceder a los parámetros múltiples desde dentro de la función en variables independientes.

irb(main):001:0> def multi_params(*params)
irb(main):002:1>   a, b, c, d, e, f = params
irb(main):003:1>
irb(main):004:1*   puts "Parametro 1 #{a}"
irb(main):005:1>   puts "Parametro 2 #{b}"
irb(main):006:1>   puts "Parametro 3 #{c}"
irb(main):007:1>   puts "Parametro 4 #{d}"
irb(main):008:1>   puts "Parametro 5 #{e}"
irb(main):009:1> end
=> :multi_params
irb(main):010:0> multi_params 'a', 'b', 'c', 1, 2
Parametro 1 a
Parametro 2 b
Parametro 3 c
Parametro 4 1
Parametro 5 2
=> nil

La múltiple asignación puede ser parcial, es decir no necesitamos proveer una variable para cada parámetro que reciba la función, para esto definimos una de las variables como splat.

irb(main):001:0> def multi_params(*params)
irb(main):002:1>   a, b, *c = params
irb(main):003:1>
irb(main):004:1*   puts "Parametro 1 #{a}"
irb(main):005:1>   puts "Parametro 2 #{b}"
irb(main):006:1>   puts "Resto de parametros #{c}"
irb(main):007:1> end
=> :multi_params
irb(main):008:0> multi_params 'a', 'b', 'c', 1, 2
Parametro 1 a
Parametro 2 b
Resto de parametros ["c", 1, 2]
=> nil

Como mencionamos anteriormente el operador splat para los parámetros debe de ser el último que se defina, en caso de que deseemos recibamos mas parámetros en la función entonces declaramos la función como sigue.

irb(main):001:0> def operacion(operador, *params)
irb(main):002:1>   params.inject(operador)
irb(main):003:1> end
=> :operacion
irb(main):004:0> operacion :+, 1, 2, 3, 4, 5
=> 15
irb(main):005:0> operacion :-, 1, 2, 3, 4, 5
=> -13
irb(main):006:0> operacion :*, 1, 2, 3, 4, 5
=> 120

Nota: La función inject es una función disponible para arreglos.

Bloques, Procs y Lambdas

Hasta este momento hemos visto como definir funciones por nombre y como ejecutarlas, ahora vamos a ver una de las características que hacen a Ruby ser … Ruby.

Iniciaremos con los bloques, ya que son los más comunes en Ruby, un bloque nos permite definir funciones anónimas las cuales mantienen referencias al entorno donde fueron definidas, es decir pueden acceder a variables que posiblemente ya no estén disponibles en el entorno de ejecución - scope -; a este tipo de funcionalidad se le conoce como Closures ; esta es una característica básica de los lenguajes funcionales.

Vamos a ver en términos prácticos que es un bloque en Ruby.

irb(main):001:0> 10.times do |i|
irb(main):002:1*   puts "El valor es #{i}"
irb(main):003:1> end
El valor es 0
El valor es 1
El valor es 2
El valor es 3
El valor es 4
El valor es 5
El valor es 6
El valor es 7
El valor es 8
El valor es 9

En este caso la función times un iterador el cual se describe como iniciando desde 0 para cada ciclo guarda el valor en i e incrementa su valor en 1 hasta que i sea menor a 10, pero esta función acepta un parámetro especial, un bloque, este bloque esta delimitado por la palabra clave do, y a continuación se enumeran los parámetros de este bloque con las barras ||, y a continuación se escribe el cuerpo de la función y se indica la terminación de la función anónima con la palabra clave end.

El código que se encuentra entre do y end es una función que a diferencia de las funciones que vimos anteriormente no cuenta con un nombre por lo tanto no puede llamarse en otras partes de nuestro programa.

Los bloques en Ruby tienen una sintaxis alterna que no modifica la forma en como funcionan.

irb(main):001:0> 10.times {|i| puts "El valor es #{i}"}
El valor es 0
El valor es 1
El valor es 2
El valor es 3
El valor es 4
El valor es 5
El valor es 6
El valor es 7
El valor es 8
El valor es 9

Anteriormente en la definición de Closure se indicó que aun bloque puede mantener referencias a las variables definidas en su entorno y por lo tanto puede hacer uso de ellas, el siguiente ejemplo muestra este caso.

irb(main):001:0> factor  = 2
=> 2
irb(main):002:0> 10.times do |i|
irb(main):003:1*   puts "El valor es #{factor * i}"
irb(main):004:1> end
El valor es 0
El valor es 2
El valor es 4
El valor es 6
El valor es 8
El valor es 10
El valor es 12
El valor es 14
El valor es 16
El valor es 18

Aquí podemos ver como la variable factor fue declarada fuera del cuerpo del bloque y aún así es posible utilizarla dentro de la definición del mismo.

Las clases que implementan el modulo Enumerable, son unas de las que más explotan esta funcionalidad de bloques, como ejemplo tenemos a la clase Array.

irb(main):001:0> (1..20).select{|i| i.odd?}
=> [1, 3, 5, 7, 9, 11, 13, 15, 17, 19]
irb(main):002:0> (1..20).select{|i| i.even?}
=> [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
irb(main):003:0> (1..20).select{|i| i%3 == 0}
=> [3, 6, 9, 12, 15, 18]
irb(main):004:0> (1..20).detect{|i| i == 6}
=> 6
irb(main):005:0> (1..20).detect{|i| i == 60}
=> nil

Ahora que conocemos que un bloque veamos como podemos implementar una función que reciba un bloque como parámetro.

irb(main):001:0> def time
irb(main):002:1>   start = Time.now
irb(main):003:1>   result = yield
irb(main):004:1>   puts "Completado en #{Time.now - start}"
irb(main):005:1> end
=> :time
irb(main):006:0>
irb(main):007:0* time do
irb(main):008:1*   sleep 5
irb(main):009:1> end
=> Completado en 5.00021

Vemos que no tenemos que realizar un declaración especial para que una función acepte un bloque como parámetro, pero dentro de la función para poder ejecutar el código del bloque necesitamos de realizar una llamada a la función yield.

Ahora si ejecutamos la misma función sin pasar un bloque vamos a obtener un error.

irb(main):001:0> time
LocalJumpError: no block given (yield)
    from (irb):9:in `time'
   from (irb):16
   from /.rbenv/versions/2.1.1/bin/irb:01:in `<main>'

El error indica que la función time intentó ejecutar la llamada a la función yield pero no pasamos un bloque para ser ejecutado. Esto es sencillo de solucionar.

irb(main):001:0> def time
irb(main):002:1>   start = Time.now
irb(main):003:1>   result = yield if block_given?
irb(main):004:1>   puts "Completado en #{Time.now - start}"
irb(main):005:1> end
=> :time
irb(main):006:0> time
=> Completado en 5.0e-06

Con la verificación de block_given? podemos verificar si la función recibió un bloque como parámetro o no y entonces realizar la llamada a yield si es pertinente.

Otra forma de indicar en la firma de nuestra función de forma más explícita que podemos aceptar un bloque es definiendo un parámetro para este propósito. La única restricción es que este parámetro debe ser el último definido en la firma de la función, anteriormente cuando hablamos del parámetro de tipo splat habíamos dicho que debe ser el último parámetro lo cual es cierto siempre y cuando no decidamos aceptar un bloque.

irb(main):001:0> def calculate_numbers(*numbers, &block)
irb(main):002:1>   new_numbers = []
irb(main):003:1>
irb(main):004:1*   numbers.each do |number|
irb(main):005:2*     new_numbers << block.call(number)
irb(main):006:2>   end
irb(main):007:1>
irb(main):008:1*   new_numbers
irb(main):009:1> end
=> :calculate_numbers
irb(main):010:0>
irb(main):011:0* calculate_numbers 1, 2, 3, 4 do |num|
irb(main):012:1*   num * 2
irb(main):013:1> end
=> [2, 4, 6, 8]

En el ejemplo anterior el parámetro &block es quien se encarga de recibir nuestro bloque, y vemos que para poder ejecutarlo es necesario llamar al método call. También podemos observar que en la llamada call podemos pasar parámetros que nuestra función anónima espera recibir.

En este punto muy probablemente ya nos dimos cuenta que la gran diferencia entre pasar un bloque a una función que no declara específicamente un parámetro para este propósito y una función que si lo hace, es que en el caso del último ejemplo block es un objeto. Si modificamos un poco nuestro código podemos confirmar que block es un objeto de tipo Proc.

irb(main):001:0> def calculate_numbers(*numbers, &block)
irb(main):002:1>   new_numbers = []
irb(main):003:1>
irb(main):004:1*   puts block.class
irb(main):005:1>   numbers.each do |number|
irb(main):006:2*     new_numbers << block.call(number)
irb(main):007:2>   end
irb(main):008:1>
irb(main):009:1*   new_numbers
irb(main):010:1> end
=> :calculate_numbers
irb(main):011:0>
irb(main):012:0* calculate_numbers 1, 2, 3, 4 do |num|
irb(main):013:1*   num * 2
irb(main):014:1> end
Proc
=> [2, 4, 6, 8]

Esta es la segunda variante de Closures que tenemos en Ruby, la gran ventaja de Proc es que podemos mantener una referencia y pasarla como parámetro a otros métodos.

irb(main):001:0> def receive_proc(&block)
irb(main):002:1>   puts "receive_proc"
irb(main):003:1>   block.call
irb(main):004:1> end
=> :receive_proc
irb(main):005:0>
irb(main):006:0* def test_proc(&block)
irb(main):007:1>   puts "test_proc"
irb(main):008:1>   receive_proc(&block)
irb(main):009:1> end
=> :test_proc
irb(main):010:0>
irb(main):011:0* test_proc do
irb(main):012:1*   puts "Being called from different method"
irb(main):013:1> end
test_proc
receive_proc
Being called from different method
=> nil

Mejor aún, podemos declarar una función anónima y guardarla como un Proc para poder ser utilizada más adelante en nuestro programa, esta es una característica que permite a Ruby transformar código a datos.

irb(main):001:0> multiply_by_two = Proc.new {|num| num * 2 }
=> #<Proc:0x007f88ab5c24f8@(irb):1>
irb(main):002:0>
irb(main):003:0* def calculate_numbers(*numbers, &block)
irb(main):004:1>   new_numbers = []
irb(main):005:1>
irb(main):006:1*   numbers.each do |number|
irb(main):007:2*     new_numbers << block.call(number)
irb(main):008:2>   end
irb(main):009:1>
irb(main):010:1*   new_numbers
irb(main):011:1> end
=> :calculate_numbers
irb(main):012:0>
irb(main):013:0* calculate_numbers 1, 2, 3, 4, &multiply_by_two
=> [2, 4, 6, 8]

Es posible declarar un Proc con la sintaxis alterna disponible a partir de Ruby 2.0

irb(main):001:0> multiply_by_two = ->(num) { num * 2 }
=> #<Proc:0x007fdb50a68500@(irb):1 (lambda)>

Si ponemos un poco de atención al mensaje que recibimos después de declarar nuestra función anónima, vemos que tiene una anotación que dice lambda. Esto nos lleva al tercer concepto a cubrir en esta sección.

Una función anónima de tipo lambda es ejecutada con la llamada al método call tal y como lo hacemos con un Proc.

irb(main):001:0> multiply_by_two.call 3
=> 6

Otra forma declarar un lambda es a través de la palabra clave lambda.

irb(main):001:0> multiply_by_two = lambda{|num| num * 2}
=> #<Proc:0x007fb55b0d4a68@(irb):1 (lambda)>

En realidad Proc y lambda son extremadamente parecidas, utilizamos ambas para declarar funciones anónimas dentro del Ruby, que como ya vimos nos da la ventaja de poder pasar código como si fuera datos en la forma de argumentos a otras funciones. Aunque las dos son muy parecidas existen algunas diferencias que nos lleva a usar una u otra.

Veamos el siguiente ejemplo:

irb(main):001:0> display_number_p = Proc.new{|num| puts num}
=> #<Proc:0x007fb55bd351e0@(irb):1>
irb(main):002:0> display_number_p.call

=> nil
irb(main):003:0> display_number_l = lambda{|num| puts num}
=> #<Proc:0x007fb55bd169e8@(irb):3 (lambda)>
irb(main):004:0> display_number_l.call
ArgumentError: wrong number of arguments (0 for 1)
    from (irb):3:in `block in irb_binding'
   from (irb):4:in `call'
   from (irb):4
   from /.rbenv/versions/2.1.1/bin/irb:4:in `<main>'

Primeramente declaramos en la variable display_number_p un Proc que acepta un parámetro, pero al momento de ejecutar el Proc no proveemos del parámetro y la función es ejecutada sin problemas.

Después declaramos en la variable display_number_l un lambda, que al igual que el Proc permite recibir un parámetro, pero al momento de ejecutar el lambda sin proveer del parámetro tenemos el error ArgumentError: wrong number of arguments (0 for 1) es decir el lambda se queja por la falta del parámetro.

Otra de las diferencia entre un Proc y un lambda es la forma en como tratan el control de flujo, por ejemplo usemos la palabra clave return.

irb(main):001:0> def display_messages(&block)
irb(main):002:1>   puts "Starting display_messages"
irb(main):003:1>   block.call
irb(main):004:1>   puts "display_messages ended"
irb(main):005:1> end
=> :display_messages
irb(main):006:0>
irb(main):007:0* message_proc = Proc.new do
irb(main):008:1*   puts "Running inside the Proc"
irb(main):009:1>   return
irb(main):010:1> end
=> #<Proc:0x007f827a8ab070@(irb):7>
irb(main):011:0>
irb(main):012:0* message_lambda = lambda do
irb(main):013:1*   puts "Running inside the lambda"
irb(main):014:1>   return
irb(main):015:1> end
=> #<Proc:0x007f827acb0a58@(irb):12 (lambda)>
irb(main):016:0>
irb(main):017:0* display_messages &message_proc
Starting display_messages
Running inside the Proc
LocalJumpError: unexpected return
    from (irb):9:in `block in irb_binding'
   from (irb):3:in `call'
   from (irb):3:in `display_messages'
    from (irb):17
    from /.rbenv/versions/2.1.1/bin/irb:11:in `<main>'
irb(main):018:0>
irb(main):019:0* display_messages &message_lambda
Starting display_messages
Running inside the lambda
display_messages ended
=> nil

Si observamos la definición del display_messages veremos que despliega un mensaje, después ejecuta el bloque anónimo que se le pase, ya sea un Proc o una lambda y finalmente despliega un mensaje.

Cuando ejecutamos display_messages y le pasamos un Proc, en Ruby 2.0 tenemos un error LocalJumpError: unexpected return, el cual nos indica que no podemos usar return dentro de un Proc. En versiones anteriores de Ruby no recibimos el mismo error, el efecto seria que el return afecta al método display_messages terminándolo, es decir nunca veríamos el mensaje display_messages ended.

En el caso de ejecutar display_messages con un lambda como parámetro, vemos que return no genera un error y que return solo afecta al código contenido dentro del lambda, es decir regresa el control de ejecución a display_messages.

Clases

Desde el inicio de este capítulo se mencionó que Ruby es un lenguaje orientado a objetos y hasta este momento lo hemos comprobado con algunas de las secciones previas en las que trabajamos con objetos que podemos instanciar a partir de las clases disponibles en la librería estándar de Ruby.

En esta sección veremos como es que podemos crear nuestras propias clases y conoceremos lo que Ruby nos ofrece como lenguaje en este ámbito.

La palabra reservada class es la que nos permite definir una nueva clase. La convención de nombre para una clase en Ruby es que el nombre es capitalizado, es decir la primera letra es mayúscula y el resto se escribe en minúscula, cuando el nombre de la clase es una palabra compuesta se aplica la misma regla.

irb(main):001:0> class Person
irb(main):002:1> end
=> nil
irb(main):003:0> person = Person.new
=> #<Person:0x007f892c83e7a0>
irb(main):004:0> person.class
=> Person

Una vez que tenemos nuestra clase definida es posible crear una instancia de la misma, tal y como se muestra en la línea 3. La llamada al método new es el que nos devuelve una instancia de la clase.

Vamos a otorgarle comportamiento a nuestra clase para que se un poco más interesante.

irb(main):005:0> class Person
irb(main):006:1> def say_hello
irb(main):007:2> puts 'Hello'
irb(main):008:2> end
irb(main):009:1> end
=> :say_hello
irb(main):010:0> person.say_hello
Hello
=> nil

Aquí hay algo muy importante a notar, definimos el método say_hello en nuestra clase Person, pero sucedió de una forma muy específica de Ruby, reabrimos nuestra clase para cambiar la forma en como funciona, y podemos apreciar este concepto en la línea 10 donde no fue necesario crear una nueva instancia para que la variable person adquiriera la nueva funcionalidad. Esta característica es propia de los lenguajes dinámicos como Ruby, en donde podemos cambiar código en tiempo de ejecución.

El método say_hello fue agregado en nuestra clase como un método de instancia, es decir que solamente se puede utilizar el método sobre un objeto que sea instancia de nuestra clase.

Vamos a reabrir nuestra clase para darle un nuevo comportamiento.

irb(main):011:0> class Person
irb(main):012:1> def full_name
irb(main):013:2> puts 'John Doe'
irb(main):014:2> end
irb(main):015:1> end
=> :full_name
irb(main):016:0> person.full_name
John Doe
=> nil

Ahora agregamos el método full_name el cual imprime un nombre, pero no es muy útil ya que no importa cuantas instancias tengamos de Person siempre va a imprimir el mismo nombre. Para arreglar este problema necesitamos que nuestra clase pueda guardar datos. En este caso necesitamos de variables de instancia que nos van a permitir guardar información que va a ser visible únicamente dentro del contexto de cada instancia.

Para inicializar los datos vamos a necesitar de un constructor en nuestra clase que permita que le pasemos el nombre al momento de inicializar nuestra instancia.

irb(main):017:0> class Person
irb(main):018:1> def initialize(first_name, last_name)
irb(main):019:2> @first_name = first_name
irb(main):020:2> @last_name = last_name
irb(main):021:2> end
irb(main):022:1> end
=> :initialize
irb(main):023:0> person = Person.new 'Mario', 'Chavez'
=> #<Person:0x007fb735370c28 @first_name="Mario", @last_name="Chavez">

Al inicializar una nueva instancia de Person las variables @first_name y @last_name se inicializan con los valores que pasamos en el constructor. Ahora vamos a redefinir el método full_name que escribimos antes para que utilice los valores de nuestras variables de instancia.

irb(main):024:0> class Person
irb(main):025:1> def full_name
irb(main):026:2> "#{@first_name} #{@last_name}"
irb(main):027:2> end
irb(main):028:1> end
=> :full_name
irb(main):029:0> person.full_name
=> "Mario Chavez"

Ya vimos que nuestro objeto puede mantener estado, el cual hasta este momento no es solo accesible desde el método full_name pero que pasa si queremos acceder a los nombres por separado desde fuera de nuestro objeto? Para tal efecto necesitas crear un par de accesores o propiedades.

irb(main):029:0> class Person
irb(main):030:1> def first_name
irb(main):031:2> @first_name
irb(main):032:2> end
irb(main):033:1> def last_name
irb(main):034:2> @last_name
irb(main):035:2> end
irb(main):036:1> end
=> :last_name
irb(main):037:0> person.first_name
=> "Mario"
irb(main):038:0> person.last_name
=> "Chavez"

Ambos accesores son de modo sólo lectura, en Ruby hay una forma más simple de escribirlos.

irb(main):039:0> class Person
irb(main):040:1> attr_reader :first_name, :last_name
irb(main):041:1> end
=> nil
irb(main):042:0> person.last_name
=> "Chavez"

Con la palabra clave attr_reader seguida de las símbolos de los nombres de los accesores se inyectan en nuestra clase la definición de los métodos para leer el valor de las variables.

Hasta este momento nuestra clase funciona prácticamente en modo de sólo lectura. El incluir funcionalidad para poder acceder y modificar datos internos en la clase en muy sencillo.

irb(main):043:0> class Person
irb(main):044:1> def address=(value)
irb(main):045:2> @address = value
irb(main):046:2> end
irb(main):047:1> def address
irb(main):048:2> @address
irb(main):049:2> end
irb(main):050:1> end
=> :address
irb(main):051:0> person.address = 'Ciudad de Mexico'
=> "Ciudad de Mexico"
irb(main):052:0> person.address
=> "Ciudad de Mexico"
irb(main):053:0> person
=> #<Person:0x007f9f6b3e9510 @first_name="Mario", @last_name="Chavez", @address="Ciudad de Mexico">

Obviamente tal como en el accesor de sólo lectura hay una forma simple en Ruby de implementar un accesor de lectura/escritura con attr_accessor, al igual que en el caso anterior simplemente listamos con símbolos los nombres de los accesores que deseamos agregar a nuestra clase.

irb(main):054:0> class Person
irb(main):055:1> attr_accessor :address
irb(main):056:1> end
=> nil
irb(main):057:0> person.address
=> "Ciudad de Mexico"

Hasta este momento todos los métodos que hemos escrito en nuestra clase son métodos de instancia, es decir solo están disponible cuando creamos un objeto a partir de nuestra clase, pero hay ocasiones en donde necesitamos métodos que estén disponibles desde la definición de la clase, es decir que sean métodos de clase.

irb(main):058:0> class Person
irb(main):059:1>   def self.say_hello
irb(main):060:2>     puts "Saying hello from no one"
irb(main):061:2>   end
irb(main):062:1> end
=> :say_hello
irb(main):063:0> Person.say_hello
Saying hello from no one
=> nil

Como podemos apreciar en el ejemplo, no fue necesario crear una instancia de Person para llamar al método say_hello. En este ejemplo introducimos una palabra clave self. self es una palabra clave un tanto compleja, por el momento nos será suficiente con saber que self es la referencia a la clase Person.

Otra forma de poder escribir este mismo código en Ruby es de la siguiente forma:

irb(main):064:0> class Person
irb(main):065:1>
irb(main):066:1*   class << self
irb(main):067:2>     def say_hello
irb(main):068:3>       puts "Saying hello from no one"
irb(main):069:3>     end
irb(main):070:2>   end
irb(main):071:1>
irb(main):072:1* end
=> :say_hello
irb(main):073:0> Person.say_hello
Saying hello from no one
=> nil

Esta notación tiene el mismo efecto que en la notación previa.

Ahora veamos el concepto de herencia en Ruby. Al ser Ruby un lenguaje orientado a objetos, es posible definir una clase padre y a partir de ésta definir clases hijas o heredadas. La única limitante que tiene Ruby con respecto a otros lenguajes de programación es que una clase hija solo puede tener herencia de una clase padre, pero veremos más adelante que esto no representa ningun problema.

irb(main):001:0> class Person
irb(main):002:1>   attr_reader :first_name, :last_name
irb(main):003:1>   attr_accessor :address
irb(main):004:1>
irb(main):005:1*   def initialize(first_name, last_name)
irb(main):006:2>     @first_name = first_name
irb(main):007:2>     @last_name = last_name
irb(main):008:2>   end
irb(main):009:1>
irb(main):010:1*   def full_name
irb(main):011:2>     "#{first_name} #{last_name}"
irb(main):012:2>   end
irb(main):013:1> end
=> :full_name
irb(main):014:0> class Employee < Person
irb(main):015:1> end
=> nil
irb(main):016:0> employee = Employee.new 'John', 'Doe'
=> #<Employee:0x007fa7a5cda678 @first_name="John", @last_name="Doe">
irb(main):017:0> employee.full_name
=> "John Doe"

En éste ejemplo vemos que usamos < para denotar que Employee hereda de Person. Podemos hacer modificaciones a las clase Employee pero esas modificaciones solo afectarán a las instancias que derivan de Employee.

irb(main):018:0> person = Person.new 'Jane', 'Doe'
=> #<Person:0x007fa7a5cbdff0 @first_name="Jane", @last_name="Doe">
irb(main):019:0> class Employee < Person
irb(main):020:1>   attr_accessor :area
irb(main):021:1> end
=> nil
irb(main):022:0> person.area = :finance
NoMethodError: undefined method `area=' for #<Person:0x007fa7a5cbdff0 @first_name="Jane", @last_name="Doe">
   from (irb):22
   from /.rbenv/versions/2.1.1/bin/irb:11:in `<main>'
irb(main):023:0> employee.area = :payroll
=> :payroll

Cambios en Person afectaran a Employee pero no al revés. Es posible en una clase hija el sobreescribir un método de la clase padre.

irb(main):024:0> class Employee < Person
irb(main):025:1>   def full_name
irb(main):026:2>     "#{last_name}, #{first_name}"
irb(main):027:2>   end
irb(main):028:1> end
=> :full_name
irb(main):029:0> person.full_name
=> "Jane Doe"
irb(main):030:0> employee.full_name
=> "Doe, John"

En este caso reemplazamos totalmente la funcionalidad del método de la clase padre, pero es posible ejecutar el método original proveído por la clase padre y tener lógica adicional en el método de la clase hija.

irb(main):031:0> class Employee < Person
irb(main):032:1>   def full_name
irb(main):033:2>     super.sub ' ', ', '
irb(main):034:2>   end
irb(main):035:1> end
=> :full_name
irb(main):036:0>
irb(main):037:0*
irb(main):038:0* person.full_name
=> "Jane Doe"
irb(main):039:0> employee.full_name
=> "John, Doe"

Mediante el uso de la palabra clave super desde el método de la clase hija delegamos al método original de la clase padre y regresamos en control.

En muchos lenguajes hoy en día es posible crear namespaces para crear separaciones lógicas o agrupar de clases en base un contexto específico. Con Ruby podemos hacer uso de la palabra clave module para crear estos namespaces.

irb(main):001:0> module Geometry
irb(main):002:1>   class Point
irb(main):003:2>     attr_accessor :x, :y
irb(main):004:2>
irb(main):005:2*     def initialize(x, y)
irb(main):006:3>       @x = x
irb(main):007:3>       @y = y
irb(main):008:3>     end
irb(main):009:2>   end
irb(main):010:1> end
=> :initialize
irb(main):011:0>
irb(main):012:0* point1 = Geometry::Point.new 1, 2
=> #<Geometry::Point:0x007f93fabba360 @x=1, @y=2>

Como vemos en el ejemplo una vez que ponemos a una clase dentro de un namespace para hacer referencia a ésta es necesario usara el nombre completo de la clase para poder hacer uso de la misma.

Pero los módulo en Ruby tienen una funcionalidad adicional donde es posible "mezclarlos" en una clase. El poder realizar un mixin en Ruby nos permite extender la funcionalidad de una clase combinando funcionalidad a diferencia de hacerlo por herencia y la limitación que ya se comento en Ruby.

irb(main):012:0* point1 = Geometry::Point.new 1, 2
irb(main):013:0> point1.to_s
=> "#<Geometry::Point:0x007fc7a1c51118>"
irb(main):014:0> module CustomToS
irb(main):015:1>   def to_s
irb(main):016:2>     "Geometry::Point #: (#{x},#{y})"
irb(main):017:2>   end
irb(main):018:1> end
=> :to_s
irb(main):019:0> module Geometry
irb(main):020:1>   class Point
irb(main):021:2>     include CustomToS
irb(main):022:2>   end
irb(main):023:1> end
=> Geometry::Point
irb(main):024:0> point1.to_s
=> "Geometry::Point #: (1,2)"

En este ejemplo tenemos la clase Geometry::Point de la cual creamos una instancia y llamamos el método to_s, el cual es el método estándar que todos los objetos de Ruby adquieren. Podemos escribir un modulo, en este caso CustomToS para reemplazar el método to_s y que nos muestre otro formato como resultado. La inclusión del módulo CustomToS fue simple con la palabra clave include en la definición de nuestra clase.

En este caso en particular el módulo CustomToS está muy ligado a nuestra clase Geometry::Point ya que tiene conocimiento de cuales son las propiedades que deseamos imprimir en la llamada a to_s así como tener explícitamente la definición de la clase Geometry::Point, esto es fácil de solucionar haciendo que el modulo CustomToS no esté tan ligado a ésta clase y se pueda usar de forma genérica en otras clases.

irb(main):025:0> module CustomToS
irb(main):026:1>   def to_s
irb(main):027:2>     "#{self.class.name} #: #{data_to_s}"
irb(main):028:2>   end
irb(main):029:1> end
=> :to_s
irb(main):030:0> module Geometry
irb(main):031:1>   class Point
irb(main):032:2>     private
irb(main):033:2>     def data_to_s
irb(main):034:3>       "(#{x}, #{y})"
irb(main):035:3>     end
irb(main):036:2>   end
irb(main):037:1> end
=> :data_to_s
irb(main):038:0> point1.to_s
=> "Geometry::Point #: (1, 2)"

Tenemos el mismo efecto con este cambio, pero nuestro módulo CustomToS se vuelve más genérico y puede reutilizarse en otras clases con funcionalidad distinta, primeramente porque en lugar de tener explícitamente el nombre de la clase para que estamos mostrando la información ahora pregunta a través de la palabra clave self el nombre de la misma. Segundo, el módulo ahora requiere de un contrato con la clase que use, este contrato espera que la clase donde se incluya el módulo responda al método data_to_s.

Así como pudimos incluir un módulo en una clase para proveer de mayor funcionalidad a nuestros objetos instancias a partir de dicha clase, podemos extender una clase con métodos de clase o estáticos a partir de un módulo.

irb(main):039:0> module PointInitializer
irb(main):040:1>   def new_centered(position)
irb(main):041:2>     self.new(position, position)
irb(main):042:2>   end
irb(main):043:1> end
=> :new_centered
irb(main):044:0>
irb(main):045:0* module Geometry
irb(main):046:1>   class Point
irb(main):047:2>     extend PointInitializer
irb(main):048:2>   end
irb(main):049:1> end
=> Geometry::Point
irb(main):050:0> point2 = Geometry::Point.new_centered(3)
=> #<Geometry::Point:0x007fc7a1a29d68 @x=3, @y=3>
irb(main):051:0> point2.to_s
=> "Geometry::Point #: (3, 3)"

Haciendo uso de la palabra clave extend ahora nuestro módulo agrega métodos a la clase, en este caso agregamos un nuevo inicializador para Geometry::Point llamado new_centered.

Los módulos en Ruby son una herramienta para reutilizar código a través de la composición en lugar de la herencia clásica en un lenguaje orientado a objetos.

Metaprogramación

Al inicio del libro se hizo mención que una de las características más importantes de Ruby como lenguaje es su habilidad para poder realizar metaprogramación.

La metaprogramación por sí sola requiere de un libro completo, es por eso que en esta sección solamente vamos a ver algunos ejemplos básicos.

Anteriormente se habia hecho mención de que en Ruby todo es un objeto, una forma sencilla de probarlo es revisando la herencia de nuestras clases.

irb(main):052:0> Geometry::Point.ancestors
=> [Geometry::Point, CustomToS, Object, Kernel, BasicObject]

Vemos que para nuestra clase Geometry::Point en algún punto de la herencia tiene a Object como padre. Vamos a usar Object de forma directa para crear un objeto.

irb(main):053:0> an_object = Object.new
=> #<Object:0x007fc7a1a0d280>
irb(main):054:0>
irb(main):055:0* def an_object.set_variable=(var)
irb(main):056:1>   @instance_variable = var
irb(main):057:1> end
=> :set_variable=
irb(main):058:0>
irb(main):059:0* def an_object.get_variable
irb(main):060:1>   @instance_variable
irb(main):061:1> end
=> :get_variable
irb(main):062:0>
irb(main):063:0* an_object.set_variable = 'New Object
=> "New Object"
irb(main):064:0> an_object.get_variable
=> "New Object"

En el ejemplo vemos como podemos crear una instancia de Object y le definimos 2 métodos set_variable y get_variable los cuales solo están disponibles para nuestra instancia, si creamos otro objeto y tratamos de usar alguno de estos métodos vemos que obtenemos un error.

irb(main):065:0> other_object = Object.new
=> #<Object:0x007fc7a1b4c5b0>
irb(main):066:0> other_object.set_variable = 'Other Object'
NoMethodError: undefined method `set_variable=' for #<Object:0x007fc7a1b4c5b0>
   from (irb):66
   from /.rbenv/versions/2.1.2/bin/irb:11:in `<main>'

Ya vimos que podemos crear objetos en tiempo de ejecución y agregarles funcionalidad al momento ahora vamos a crear una clase en tiempo de ejecución.

irb(main):068:0> Person = Class.new
=> Person
irb(main):069:0> def Person.who_ami
irb(main):070:1>   self.name
irb(main):071:1> end
=> :who_ami
irb(main):072:0> Person.who_ami
=> "Person"

Vamos a trabajar un poco sobre nuestra nueva clase para darle un poco de funcionalidad.

irb(main):073:0* Person.class_eval do
irb(main):074:1*   attr_accessor :name
irb(main):075:1>
irb(main):076:1*   def reverse_name
irb(main):077:2>     name.reverse
irb(main):078:2>   end
irb(main):079:1> end
=> :reverse_name
irb(main):080:0>
irb(main):081:0* person = Person.new
=> #<Person:0x007fc7a19549b0>
irb(main):082:0> person.name = 'John'
=> "John"
irb(main):083:0> person.reverse_name
=> "nhoJ"

En tiempo de ejecución hemos creado una clase y le hemos dado comportamiento a nuestra clase a través del métodos class_eval.

Con ambos ejemplo posiblemente ya nos dimos cuenta de que en Ruby es posible escribir programas que escriban programas en base a la información de su entorno, lo que convierte a Ruby en una herramienta muy poderosa pero también en una herramienta que hay que manejarla con cuidado.

Vamos volviendo a nuestra clase Geometry::Point y el módulo CustomToS para ver los siguientes ejemplos.

El módulo CustomToS contiene un contrato en donde la clase que incluya este módulo debe de tener el método data_to_s, ¿qué pasa si incluimos el módulo en una clase que no implemente el contrato?

irb(main):84:0> class Triangle
irb(main):85:1>   include CustomToS
irb(main):86:1> end
=> Triangle
irb(main):87:0>
irb(main):88:0* t = Triangle.new
=> #<Triangle:0x007fc7a203a478>
irb(main):89:0> t.to_s
NameError: undefined local variable or method `data_to_s' for #<Triangle:0x007fc7a203a478>
   from (irb):27:in `to_s'
   from (irb):89
   from /.rbenv/versions/2.1.2/bin/irb:11:in `<main>'

Al momento de realizar la llamada a to_s tenemos un error de que el método o variable data_to_s no existe. Una forma de prevenir este error y avisar al desarrollador que tiene que seguir el contrato se muestra en el siguiente ejemplo.

irb(main):106:0> module CustomToS
irb(main):107:1>   def to_s
irb(main):108:2>     data = if self.respond_to?(:data_to_s, true)
irb(main):109:3>       data_to_s
irb(main):110:3>     else
irb(main):111:3*       "Please implement method #data_to_s in your class #{self.class.name}"
irb(main):112:3>     end
irb(main):113:2>
irb(main):114:2*     "#{self.class.name} #: #{data}"
irb(main):115:2>   end
irb(main):116:1> end
=> :to_s
irb(main):117:0> t.to_s
=> "Triangle #: Please implement method #data_to_s in your class Triangle"

En este caso primero preguntamos si la clase donde se incluyó nuestro módulo responde al método data_to_s antes de intentar hacer la llamada, esto lo logramos a través de respond_to?.

Ahora vamos cambiando un poco de contexto y enfocarnos a la clase Geometry::Point. Supongamos que queremos tener un método para incrementar el valor de x o de y, una solución para este problema es la siguiente.

irb(main):118:0* module Geometry
irb(main):119:1>   class Point
irb(main):120:2>     def increment(coordenate)
irb(main):121:3>       @x +=1 if coordenate == :x
irb(main):122:3>       @y +=1 if coordenate == :y
irb(main):123:3>     end
irb(main):124:2>   end
irb(main):125:1> end
=> :increment
irb(main):126:0>
irb(main):140:0* point = Geometry::Point.new(1, 2)
=> #<Geometry::Point:0x007fc7a1bd2908 @x=1, @y=2>
irb(main):127:0> point.to_s
=> "Geometry::Point #: (1, 2)"
irb(main):128:0> point.increment(:x)
=> nil
irb(main):129:0> point.to_s
=> "Geometry::Point #: (2, 2)"

Ahora que pasa si nos piden un método especializado para incrementar x o y estaríamos posiblemente pensando en agregar increment_x e increment_y pero vamos mejor tomando ventaja de la metaprogramación en Ruby.

En Ruby existe un método especial en nuestras clases llamada method_missing, el cual es ejecutado cuando en un objeto tratamos de hacer una llamada a un método inexistente. Podemos tomar ventaja de esta característica de Ruby para nuestra implementación.

irb(main):130:0> module Geometry
irb(main):131:1>   class Point
irb(main):132:2>     def method_missing(method, *args, &block)
irb(main):133:3>       last_char = method[-1]
irb(main):134:3>       if ['x', 'y'].include?(last_char)
irb(main):135:4>         send(:increment, last_char.to_sym)
irb(main):136:4>       else
irb(main):137:4*         super
irb(main):138:4>       end
irb(main):139:3>     end
irb(main):140:2>   end
irb(main):141:1> end
=> :method_missing
irb(main):142:0>
irb(main):143:0* point.to_s
=> "Geometry::Point #: (2, 2)"
irb(main):144:0> point.increment_x
=> nil
irb(main):145:0> point.to_s
=> "Geometry::Point #: (3, 2)"
irb(main):146:0> point.increment_y
=> 3
irb(main):147:0> point.to_s
=> "Geometry::Point #: (3, 3)"
irb(main):148:0> point.increment_z
NoMethodError: undefined method `increment_z' for #<Geometry::Point:0x007f9869ad33c0 @x=3, @y=2>
   from (irb):132:in `method_missing'
   from (irb):148
   from /.rbenv/versions/2.1.2/bin/irb:11:in `<main>'

Ahora nuestra clase Geometry::Point entiende que es lo que tiene que hacer cuando se hace una llamada al método increment_ con el nombre de alguna de las coordenadas y vemos que falla cuando tratamos de ejecutarlo con una coordenada que no existe.

La pieza clave en nuestra implementación de method_missing es el uso del método send el cual permite ejecutar de forma dinámica un método.

irb(main):149:0> point.send('to_s')
=> "Geometry::Point #: (3, 3)"
irb(main):150:0> point.send(:to_s)
=> "Geometry::Point #: (3, 3)"

Como vemos en el ejemplo send puede recibir el nombre del método a ejecutar como una cadena de texto o como un símbolo.

Como vimos en los ejemplos, el dinamismo de Ruby es increíble y usándolo de forma responsable podemos hacer programas que se adapten y funciones en diferentes circunstancias.

Conclusiones.

En este primer capítulo conocimos un poco de la historia que hay detrás de Ruby, su concepción filosófica y técnicamente algunas de las cualidades del lenguaje.

Es imposible enseñar Ruby en un sólo capítulo pero lo aquí mostrado nos servirá durante el resto del libro para entender los ejemplos y conocer un poco.

El resto del capítulo está disponible en el libro en formatos para Kindle, ePub y PDF.

Comprar el libro

Aprendiendo Ruby on Rails 4.0

Mario Alberto Chávez Cárdenas

Copyright (C) 2013 Mario Alberto Chávez Cárdenas.