Crystal, el sucesor de Ruby

Ruby es un lenguaje de programación muy popular creado por Yukihiro Matsumoto en el año mil novecientos noventa y tres por el hecho de que no le acababan de persuadir lenguajes como Perl o bien Python y deseaba un scripting language con un auténtico soporte a la POO. Indudablemente creó un lenguaje que satisface a muchos programadores no obstante, una de las recurrentes fricciones que se generan en el mundo es la de como es el sistema de tipos más conveniente.

Al utilizar Ruby duck typing, se dificulta o bien torna imposible efectuar ciertos análisis deseables en los programas escritos. Indudablemente esta es la razón de que los autores de Crystal hayan optado por imitar Ruby utilizando un sistema de tipos estático.

Objetivos de Crystal

Los propósitos marcados por los desarrolladores de Crystal son:

Sintaxis afín a la de Ruby
Tipado estático con inferencia
Simple integración con C
Evaluación y generación de código en tiempo de colección (vía macros)
Generación de código nativo eficaz

Realmente y siempre y en todo momento a mi modo de ver, el tipado estático no es un requisito en si mismo, sino más bien la manera de lograr análisis del código y generación de código eficaz. Es el cambio de tipado activo (y duck) a estático donde está el reto, la complejidad y el éxito si consiguen hacerlo de forma práctica y eficaz.

Tipado estático y también inferencia

Resulta un tanto curiosa la manera en que Crystal aplica las reglas de inferencia (parte 1 y parte dos) y las estructuras aceptadas, probablemente debido al requisito de ajustarse en la medida de lo posible a la sintaxis de Ruby. Por poner un ejemplo el próximo código es válido:

a = 1 # a is Int32
a.abs # O.K., Int32 has a method 'abs'
a = hello # a is now String
a.size # O.K., String has a method 'size'

Esto fuerza por lo general a que Crystal deba lidiar con uniones de tipos en tiempo de ejecución, produciendo estructuras como:

struct Int32OrString undefined

Si bien efectúa cierta optimizaciones cuando reconoce que en determinadas ramas del código el tipo actual de determinado identificador es uno en la unión, esto es, no siempre y en todo momento debe valorar type_id en tiempo de ejecución.

Con esta capacidad, ahora Crystal puede efectuar un análisis que Ruby no podía como por poner un ejemplo en:

if n > tres
a = 1
a.abs
else
a = hello
end
a.size

Donde puede intuirse que la pretensión del programador era devolver bien el valor absoluto de a bien la longitud de la cadena hello y a.size ha de estar al final y en el bloque else (no fuese). Crystal advierte con un fallo que el procedimiento .size no existe para todos las clases posibles en la unión actual (Int32 en un caso así).

En cuanto al resto, semeja que la inferencia solamente examina el AST de abajo arriba, requiriendo en todo instante que los modelos estén definidos. Para el caso de genéricos en Crystal, deja libre el tipo hasta el momento en que quede definido en el empleo y produciendo uniones de tipos de ser preciso. Esto causa que, de entrada, las capacidades del sistema de tipos esté al mismo nivel que otros como en Java o bien C#.

El tipo NoReturn

Como Crystal conlleva el género de cada expresión, emplea el tipo NoReturn para señalar que nada es devuelto como en:

a = …
if a == 1
a = hello
puts a.size
raise Ouch!
else
a = dos
end
a # here `a` cánido only ne `Int32`

Deduciendo que, de seguir con else, el género de a va a ser Int32. Si bien de apariencia puntual, un buen empleo de la inferencia de este género puede ser útil para la detección de edge cases que puede ser aprovechado por herramientas de análisis de cobertura y testing.

Filtrado de tipos

De forma reflexiva y dada la unión de tipos precedente, en ciertos casos puede examinarse si el valor de la unión corresponde a un tipo o bien a otro como en ambos casos siguientes:

if a.is_a?(Int32)
a.abs
end

if a.responds_to?(:abs)
a.abs
end

En el primero se medita sobre si a es un Int32 y en el segundo si el tipo que contiene a en ese instante acepta el procedimiento abs.

Desafortunadamente, existen relaciones no resueltas en esta estrategia que hace que no sea consistente y deba parchearse:

a = some_condition ? 1 : nil
if !a
else
a.abs # should be O.K., but now gives fallo
end

De forma adicional, dicha reflexión semeja marchar solamente en variables locales y no en miembros de instancia y clausuras. Semeja por ende un empleo limitadísimo y obvio de reflexión.

Macros

Crystal tiene un lenguaje propio de macros afín a otros lenguajes en que podríamos resaltar la posibilidad de delimitar hooks cuando se generan ciertas situaciones al compendiar, por servirnos de un ejemplo cuando se procura invocar un procedimiento ignoto method_missing.

# macro `inherited`
class Parent
macro inherited
def undefined
1
end
end
end

class Child 1

# macro `method_missing`
macro method_missing(call)
print Got , undefined, with , undefined, arguments, 'n'
end

foo # Prints: Got foo with 0 arguments
bar 'a', 'b' # Prints: Got bar with dos arguments

A mi parecer, extender el lenguaje a través de macros o bien utilizarlas para producir estrategias por encima del lenguaje me semeja muy peligroso, aunque deja efectuar análisis interesantes como quien, cuando y de qué forma hereda cierta clase.

Rendimiento

Uno de los aspectos esenciales que semeja quiere solucionar Crystal es el desempeño, señalando que está próximo a C. Sin embargo, utilizando el propio ejemplo que utilizan se aprecia un degradamiento esencial. Comparando:

require big
require option_parser

def fib(n)
a = BigInt.new(0)
b = BigInt.new(1)
n.times do
a += b
a, b = b, a
end
a
end

n = diez

OptionParser.parse! do |parser|
parser.banner = Usage: fib
parser.on(-n NUMBER, Fib ordinal to print) o
end

puts (fib(n) por ciento un millón)

# [josejuan@centella crystal] dólares americanos crystal build test.cr