Desarrollar debería ser divertido!

Hoy me gustaría hablar de Ruby, un lenguaje de programación con el que nunca he tenido la suerte de trabajar profesionalmente, pero al que siempre he estado vinculado. Se caracteriza por estar diseñado y pensando por y para la persona que desarrolla, lo que hace de él unos de los lenguajes más divertidos, si no el que más.

¿Qué es Ruby?

Por dar un poco de contexto y como se puede encontrar fácilmente por internet:

Ruby es un lenguaje de programación interpretado, reflexivo y orientado a objetos, creado por el programador japonés Yukihiro “Matz” Matsumoto, quien comenzó a trabajar en Ruby en 1993, y lo presentó públicamente en 1995. Combina una sintaxis inspirada en Python y Perl con características de programación orientada a objetos similares a lenguajes como Smalltalk.

Si queréis saber un poco más os recomiendo enormemente este articulo de Carla donde ella lo explica mucho mejor y más detallado.

Everything in Ruby is an Object

Si tuviera que destacar algo de Ruby es que todo son objetos y cuando digo todo es realmente todo.

Desde los Strings, pasando por los Integers, Arrays, Hashes o incluso operadores como +, -, \*, / o los booleanos true/false como veremos más adelante.

Que en Ruby todo sean objetos tiene una serie de consecuencias que debemos tener muy en cuenta. Si sumamos que todas las clases en Ruby se pueden extender junto con qué operaciones matemáticas como la suma son en realidad métodos, podemos hacer prácticamente lo que queramos con nuestro código.

Esto que puede parecer algo extraño lo podemos ver de forma más clara con el siguiente ejemplo.

En Ruby es posible preguntarle a cualquier objeto si es capaz de responder a una llamada a un método en concreto haciendo uso de #respond_to?.

1.respond_to? :-         # => true
1.respond_to? :+         # => true
1.respond_to? :times     # => true
1.respond_to? :downcase  # => false

Como podemos ver incluso las operaciones matemáticas como la suma y la resta en Ruby no son operadores como en otros lenguajes. En Ruby son métodos de una clase, en este caso la clase Integer.

Veámoslo con otro ejemplo:

1.send("-", 1)  # => 0
4.send("+", 5)  # => 9
2.send("*", 3)  # => 6
8.send("/", 4)  # => 2

En Ruby es posible enviar un mensaje a cualquier objeto utilizando el método #send. Lo que hará que ese método sea invocado pasándole los argumentos necesarios.

Para acabar con esta pequeña introducción me gustaría mencionar otro método muy importante y en que se basa gran parte de la conocida “magia” tanto de Ruby como de Rails.

Todo objeto en Ruby puede definir como quiero comportarse si se llama a un método que no tiene definido.

Esto es posible implementando un método llamado method_missing, un método que recibe como primer parámetro un símbolo con el nombre del método y una lista de parámetros extra los cuales son los parámetros que se han enviado en el mensaje.

class Roman
  def roman_to_int(method_name)
    # ...
  end
  def method_missing(method_name)
    roman_to_int method_name.to_s
  end
end

roman = Roman.new
roman.iv      # => 4
roman.xxiii   # => 23
roman.mm      # => 2000

Un gran poder conlleva una gran responsabilidad

Como hemos podido ver, Ruby es un lenguaje súper dinámico en el que prácticamente todo está permitido.

Esto es algo que puede ser realmente positivo, pero que si no se usa de la forma correcta puede ser un auténtico infierno.

Imaginemos que queremos cambiar el comportamiento de unos de los tipos primitivos del lenguaje como puede ser la clase Integer, bastaría con “abrir” la clase en cuestión y sobreescribir aquellos métodos a los que queremos cambiar su comportamiento:

class Integer
  def +(other)
    self - other
  end

  def -(other)
    self + other
  end
end

1 + 1  #=> 0
1 - 1  #=> 2

En el ejemplo anterior hemos invertido el comportamiento de las sumas y las restas.

Estaréis pensando ¿por qué?, ¿para qué? Y con toda la razón del mundo jajaja. Este es un caso extremo y sin mucho sentido, pero creo que demuestra muy bien las posibilidades que ofrece el lenguaje.

Ruby es muy divertido

Después de una breve introducción dejadme que os explique el porqué de este post.

Cuando digo que Ruby es un lenguaje divertido me refiero a que está pensado y diseñado para qué la persona que desarrolla no tenga que preocuparse de muchos detalles a bajo nivel como suele ocurrir en la mayoría de los lenguajes.

Algunos ejemplos habituales son:

  • Acceder a listas con índices.
  • Manipulación de strings.
  • Iterar o aplicar transformaciones sobres listas.
  • Trabajar con fechas.

Strings

Como decia antes, cuando trabajamos con Strings es muy habitual tener que hacer transformaciones, búsquedas o validaciones sobre los mismos.

Este tipo de operaciones en Ruby están muy pulidas:

hello_world = 'hello World!'   # => "hello World!"
hello_world.capitalize         # => "Hello world!"
hello_world.downcase           # => "hello world!"
hello_world.upcase             # => "HELLO WORLD!"
hello_world.swapcase           # => "hELLO wORLD!"

hello_world.size               # => 12
hello_world.length             # => 12

hello_world.empty?             # => false
hello_world.end_with?('ello')  # => false
hello_world.start_with?('h')   # => true
hello_world.include?('hello')  # => true
hello_world.scan(/\w+/)        # => ["hello", "World!"]

hello_world.reverse            # => "!dlroW olleh"
"abc\r\n".chop                 # => "abc"

hello_worlds.chars                        # => ["h", "e", "l", "l", "o", " ", "W", "o", "r", "l", "d", "!"]

'00'.succ                      # => "01"
'01'.prev                      # => "00"
'aa'.succ                      # => "ab"
'ab'.prev                      # => "aa"
'AA'.succ                      # => "AB"
'AB'.prev                      # => "AA"

Y esto son solo los más destacados para mi gusto, en la API de la clase String podéis encontrar mucha más información.

Integers

Con respecto a los enteros pasa algo muy similar, en Ruby existen multitud de métodos super útiles que le hacen la vida mucho más fácil a quien los usa:

1.pred                       # => 0
1.succ                       # => 2
1.odd?                       # => false
1.even?                      # => true
1.zero?                      # => false
10.remainder 2               # => 0

5.times { |i| puts i }       # => 0, 1, 2, 3, 4
5.upto(10) { |i| puts i }    # => 5, 6, 7, 8, 9, 10
10.downto(5) { |i| puts i }  # => 10, 9, 8, 7, 6, 5

Como podéis ver la generación de listas o la manipulación de enteros es una pasada.

Arrays

En cuanto a listas más es de lo mismo:

[0, 1, :foo].all?                            # => true
[nil, 0, false].any?                         # => true
[0, 1, 2].include?(2)                        # => true
[nil, 0, nil, 1, nil, 2, nil].compact        # => [0, 1, 2]
[0, 0, 1, 1, 2, 2].uniq                      # => [0, 1, 2]
[0, 1].cycle(2)                              # => 0, 1, 0, 1
[0, 1, 2].permutation(2)                     # => [0, 1] [0, 2] [1, 0] [1, 2] [2, 0] [2, 1]

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.first                                # => 1
numbers.last                                 # => 10
numbers.at(2)                                # => 3
numbers.take(3)                              # => [1, 2, 3]
numbers.sample                               # => 3
numbers.sample                               # => 8
numbers.shuffle                              # => [2, 5, 1, 6, 4, 10, 3, 9, 7, 8]

numbers.map {|element| element.class }       # => [Symbol, String, Integer]
numbers.filter(&:even?)                      # => [2, 4, 6, 8, 10]
numbers.reject(&:even?)                      # => [1, 3, 5, 7, 9]

numbers = [:a, :b, :a, :c]
numbers.tally                                # => {a: 2, b: 1: c: 1}

numbers = [:a0, :a1, :a2, :a3]
more_numbers = [:b0, :b1, :b2, :b3]
numbers.zip(more_numbers)                    # => [[:a0, :b0], [:a1, :b1], [:a2, :b2], [:a3, :b3]]

Recuerdo perfectamente la primera vez que vi los métodos first y last como un punto de inflexión donde tuve claro que Ruby iba a ser para mí.

Hashes

La API para los Hashes no tiene nada que envidiar a la de los Arrays como podéis ver a continuación:

hash = {foo: 0, bar: 1, baz: 2}
hash.keys                                              # => [:foo, :bar, :baz]
hash.has_key? :foo                                     # => true
hash.values                                            # => [0, 1, 2]
hash.has_value? 10                                     # => false
hash.each {|key, value| puts "#{key}: #{value}"}       # => [foo: 0, bar: 1, baz: 2]
hash.fetch(:bar)                                       # => 1
hash.fetch(:var, 10)                                   # => 10

another_hash = {bam: 5, bat:6}
hash.merge(another_hash)                               # => {:foo=>0, :bar=>1, :baz=>2, :bam=>5, :bat=>6}

Ranges

Otro de esos métodos que me marcaron mucho cuando lo descubrí es el método cover? cuando estamos trabajando con rangos de cualquier tipo. Os dejo algún ejemplo más para que os que podáis hacer una idea:

range_from_one_to_three = (1...4)
range_from_one_to_three.to_a              # => [1, 2, 3]

range_from_one_to_four = (1..4)
range_from_one_to_four.to_a               # => [1, 2, 3, 4]

range_from_one_to_four.include?(1)        # => true
range_from_one_to_four.member?(1)         # => true
range_from_one_to_four.cover?(1)          # => true
range_from_one_to_four.cover?(1)          # => true
range_from_one_to_four.cover?(0)          # => false

range_from_one_to_four.count              # => 4
range_from_one_to_four.max                # => 4
range_from_one_to_four.min                # => 1
range_from_one_to_four.minmax             # => [1, 4]
range_from_one_to_four.first              # => 1
range_from_one_to_four.last               # => 4

Dates

Y por último lo que para mucha gente es la joya de la corona, las fechas.

Si has tenido que pegarte alguna vez con ellas seguro que pensarás como yo en tanto en cuanto a que suelen dar muchos problemas.

En cambio en Ruby, y mucho más en Rails como veremos después, tener que trabajar con fechas es mucho más intuitivo y sencillo a la vez que potente:

Date::MONTHNAMES      # => [nil, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
Date::DAYNAMES        # => ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]

Date.leap?(1900)      # => true
Date.leap?(1901)      # => false

Date.today.to_s       # => "2022-07-06"

today = Date.today    # => <Date: 2023-09-30 ((2460218j,0s,0n),+0s,2299161j)>
today.prev_day        # => <Date: 2023-09-29 ((2460217j,0s,0n),+0s,2299161j)>
today.prev_month      # => <Date: 2023-08-30 ((2460187j,0s,0n),+0s,2299161j)>
today.prev_year       # => <Date: 2022-09-30 ((2459853j,0s,0n),+0s,2299161j)>
today.next_day        # => <Date: 2023-10-01 ((2460219j,0s,0n),+0s,2299161j)>
today.next_month      # => <Date: 2023-10-30 ((2460248j,0s,0n),+0s,2299161j)>
today.next_year       # => <Date: 2024-09-30 ((2460584j,0s,0n),+0s,2299161j)>

today.day             # => 30
today.month           # => 9
today.year            # => 2023

today.monday?         # => false
today.saturday?       # => true

Se que es mucho código, pero quería que fuera muy visual y que luego cada persona si quiere saber más pueda ir a la documentación oficial e indagar todo lo que quiera.

Rails

Si conoces Ruby es casi imposible que no conozcas Rails, su framework web más conocido y usado.

Rails añade una API extra por encima de la ya bastante completa de Ruby como hemos visto antes. Para profundizar un poco más sobre ella voy a repasar las principales clases como hice antes y mostrando algunos ejemplos.

Strings

Cuando llegas a Rails y descubres métodos como titleize o pluralize lo normal es que te explote la cabeza ¿Se puede crear un framework pensando más en la gente que lo va a usar? Yo sinceramente creo que no.

Algunos ejemplos extra:

''.blank?                          # => true
'   '.blank?                       # => true
"\t\n\r".blank?                    # => true
' blah '.blank?                    # => false

hello_string = "hello"
hello_string.first                 # => "h"
hello_string.first(3)              # => "hel"
hello_string.last                  # => "o"
hello_string.last(2)               # => "lo"

'man from the boondocks'.titleize  # => "Man From The Boondocks"

'post'.pluralize                   # => "posts"
'person'.pluralize                 # => "people"
'sheep'.pluralize                  # => "sheep"
'words'.pluralize                  # => "words"
'the blue mailman'.pluralize       # => "the blue mailmen"
'apple'.pluralize(1)               # => "apple"
'apple'.pluralize(2)               # => "apples"
'ley'.pluralize(:es)               # => "leyes"
'ley'.pluralize(1, :es)            # => "ley"

Integers

En lo que respecta a los números enteros, es más de lo mismo. Una API sencilla, limpia y súper intuitiva de usar:

2.days              # => 2 days
2.months            # => 2 months
2.years             # => 2 years

6.multiple_of? 5    # => false
10.multiple_of? 2   # => true

1.ordinal           # => "st"
1.ordinalize        # => "1st"

Arrays

En Rails el acceso a elementos de una lista va mucho más allá del método first visto en Ruby, aquí puedes leer el quinto elemento de una lista de la misma forma que lo harías verbalmente!!!.

Otros puntos que me parecen muy destacables son la posibilidad de construir queries para usar en una URL o generar una frase a partir de una lista.

Veamos algunos ejemplos:

["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd")  # => ["David", "Rafael"]

%w( a b c d e ).first                     # => "a"
%w( a b c d e ).fifth                     # => "e"
(1..42).to_a.forty_two                    # => 42

['Rails', 'coding'].to_query('hobbies')   # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"

['one'].to_sentence                       # => "one"
['one', 'two'].to_sentence                # => "one and two"
['one', 'two', 'three'].to_sentence       # => "one, two, and three"
['uno', 'dos'].to_sentence(locale: :es)   # => "uno y dos"

Hashes

La generación de queries es algo que también es posible hacer a partir de un Hash:

{name: 'David', nationality: 'Danish'}.to_query  # => "name=David&nationality=Danish"
{name: 'David', nationality: 'Danish'}.to_param  # => "name=David&nationality=Danish"

Ranges

En cuanto a los rangos, Rails introduce el método overlaps? que le da mucha semántica a la acción que se quiere realizar:

range_from_one_to_five = (1..5)
range_from_four_to_six = (4..6)
range_from_seven_to_nine = (7..9)

range_from_one_to_five.overlaps? range_from_four_to_six  # => true
range_from_one_to_five.overlaps? range_from_seven_to_nine  # => false

Date

Y por último las fecha, si en Ruby ya comentaba que era una API increíble, en Rails esto se queda corto. Que sea posible crear fechas con un 1.day.ago o 1.weeek.from_now es algo que de verdad a mi no deja de sorprenderme. No lo he visto en ningún otro lenguaje y no entiendo el porqué.

Os dejo algunos ejemplos más:

today = Date.today      # => Sat, 30 Sep 2023
today.yesterday         # => Fri, 29 Sep 2023
today.tomorrow          # => Sun, 01 Oct 2023

1.day.ago               # => Fri, 29 Sep 2023 00:00:00.000000000 UTC +00:00
1.day.from_now          # => Sun, 01 Oct 2023 16:59:30.905937392 UTC +00:00
1.week.ago              # => Sat, 23 Sep 2023 16:58:25.386179884 UTC +00:00 
1.week.from_now         # => Sat, 07 Oct 2023 17:00:24.977591325 UTC +00:00
1.year.ago              # => Fri, 30 Sep 2022 16:58:46.306488181 UTC +00:00
1.year.from_now         # => Fri, 30 Sep 2022 16:58:46.306488181 UTC +00:00

today.beginning_of_day  # => Sat, 30 Sep 2023 00:00:00.000000000 UTC +00:00
today.middle_of_day     # => Sat, 30 Sep 2023 12:00:00.000000000 UTC +00:00
today.end_of_day        # => Sat, 30 Sep 2023 23:59:59.999999999 UTC +00:00

Date.yesterday.between?(1.week.ago, Date.today)  # => true
Date.tomorrow.between?(1.week.ago, Date.today)   # => false

Conclusiones

Ya para cerrar sí que me gustaría dejar una reflexión.

Con el paso de los años los nuevos lenguajes de programación se han ido haciendo cada vez más potentes, pero para mi entender también más complejos y aburridos.

Es indudable que temas como la seguridad, el rendimiento o la seguridad en forma de tipos es algo muy a tener en cuenta, pero ¿porque se dejó de crear lenguajes divertidos?

Espero que os haya gustado este post algo diferente al resto!