El lenguaje de programación Gleam

Baltasar García Perez-Schofield - Feb 14 - - Dev Community

Lucy, Gleam's mascot

Por una cuestión totalmente colateral, y para mi propia sorpresa, me he puesto a explorar el lenguaje de programación Gleam.

Probablemente se trata de que es un lenguaje que apunta a la sencillez, y soportando únicamente la programación funcional, en lugar de la mucho más común programación imperativa. Una forma de practicar programación funcional podría ser utilizar exclusivamente lambdas en Python, por ejemplo, si no se desea utilizar un lenguaje de programación totalmente nuevo.

Si normalmente estamos, con la programación imperativa, a la tríada: secuencia, decisión e iteración. Es decir, las instrucciones se ejecutan una después de la otra (de arriba a abajo, para entendernos), se pueden tomar decisiones para dirigir el flujo de ejecución hacia unas u otras instrucciones, y además, es posible repetir una secuencia de instrucciones.

En programación funcional, aunque seguimos teniendo secuencia, lo más parecido a la decisión es el llamado pattern matching (se puede aducir que más o menos es lo mismo, aunque esta última es mucho más expresiva y potente), y recursividad (no hay repetición). La recursividad suele ser vista como una ayuda a la forma declarativa de expresar un algoritmo.

Por ejemplo, si queremos calcular el factorial de un número n...

El factorial de un número entero positivo n, se define como el producto de todos los números enteros positivos desde 1 hasta n.

Podemos, imperativamente, pensar en recorrer la secuencia de números enteros menores que n, multiplicándolos entre ellos. Lo siguientes ejemplos usan JavaScript.

function factorial(n)
{
    let toret = 1;

    for(let i = 2; i <= n; ++i) {
        toret *= i;
    }

    return toret;
}
Enter fullscreen mode Exit fullscreen mode

Mientras que declarativamente, podríamos decir que nos concentramos en la definición del algoritmo a implementar.

function factorial(n)
{
    let toret = 1;

    if ( n > 1 ) {
        toret *= factorial( n - 1 );        
    }

    return toret;
}
Enter fullscreen mode Exit fullscreen mode

Como ya sabemos, la recursividad se basa en dos casos: el caso base (en el ejemplo de arriba, cuando n es 1 y la recursión termina), y el caso normal (multiplicar n por el factorial de n - 1).

fn factorial(n)
{
    case n >= 1 {
        False -> 1
        True -> n * factorial(n - 1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Contrasta el uso de if con case en Gleam. Si estamos dispuestos a no cumplir con todas las posibilidades, podemos utilizar un case más idiomático:

fn factorial(n)
{
    case n {
        0 -> 1
        1 -> 1
        _ -> n * factorial(n - 1)
    }
}
Enter fullscreen mode Exit fullscreen mode

...el único problema es que siendo n un valor negativo, se provoca un bucle infinito. Quizás deberíamos tratarlo mejor como un error, aunque esto complica y enreda bastante el código (las definiciones de tipos son opcionales):

fn factorial(n: Int) -> Result(Int, Nil)
{
    case n < 0 {
        True -> Error(Nil)
        False -> {
            case n {
                0 -> Ok(1)
                1 -> Ok(1)
                _ -> Ok(n * result.unwrap(factorial(n - 1), 1))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Tenemos que utilizar result.unwrap() para extraer del resultado los datos, asumiendo que no hay una condición de error.

Y especialmente, la manera de recibir el resultado:

pub fn main() {
        case factorial(6) {
            Ok(x) -> io.println(int.to_string(x))
            Error(Nil) -> io.println("Indefinido")
        }
}
Enter fullscreen mode Exit fullscreen mode

Bueno, más o menos esto es el uso más básico de Gleam. Solo hay que tener en cuenta que funciones importantes como int.to_string(i: Int) -> String, no están relacionadas con una clase Int, sino que son paquetes presentes en la librería estándar que se importan.

import gleam/io
import gleam/int
import gleam/result


fn factorial(n: Int) -> Result(Int, Nil)
{
    case n < 0 {
        True -> Error(Nil)
        False -> {
            case n {
                0 -> Ok(1)
                1 -> Ok(1)
                _ -> Ok(n * result.unwrap(factorial(n - 1), 1))
            }
        }
    }
}



pub fn main() {
        case factorial(6) {
            Ok(x) -> io.println(int.to_string(x))
            Error(Nil) -> io.println("Indefinido")
        }
}
Enter fullscreen mode Exit fullscreen mode

Una curiosidad es que no hay una instrucción return, sino que directamente se retorna el resultado de la última expresión.

Tenemos booleanos (Bool), enteros (Int), números reales (Float) y cadenas de caracteres (String). Por supuesto, tenemos acceso a estructuras de datos más complejas, como tuplas(Tuple), listas (List), y diccionarios (Dict).

La diferencia entre listas y tuplas es que las primeras están restringidas a un solo tipo. Por ejemplo, [1, 2, 3] seguiría el tipo List(Int). Las tuplas, en cambio, pueden contener elementos de cualquier tipo entre sus elementos. Por ejemplo, las tuplas pueden utilizarse como un recurso para potenciar los case. La siguiente es la función fizzbuzz(n: Int) -> String, que devuelve el número pasado, o "Fizz" en caso de que sea múltiplo de 3, "Buzz" si es múltiplo de 5, o "FizzBuzz" si es múltiplo de ambos.

fn fizzbuzz(i: Int) -> String {
        case i % 3 == 0, i % 5 == 0 {
                True, True -> "FizzBuzz"
                _, True -> "Buzz"
                True, _ -> "Fizz"
                _, _ -> int.to_string(i)
        }
}
Enter fullscreen mode Exit fullscreen mode

Otra curiosidad es que las funciones pueden "encadenarse", de forma que el primer elemento es omitido pues es el valor que se pasa de una función a otra.

pub fn main() {
        list.range(1, 20)
                |> list.map(fn(n) {fizzbuzz(n)})
                |> list.map(fn(n) {io.println(n)})
}
Enter fullscreen mode Exit fullscreen mode

También arriba vemos cómo crear una función anónima: fn(<lista_params>) { instrucciones... }; es la misma sintaxis que cualquier función, pero obviando el nombre de la misma.

Así, la función list.range(<inicio>: Int, <final>: Int) -> List(Int) crea una lista entre los parámetros inicio y fin, ambos inclusive. Esa lista se pasa a la función list.map(l: List(T), fn), que aplica a cada elemento de una lista l la función fn, devolviendo una nueva lista que contiene tantos elementos como tenía l, pero con el resultado de aplicar fn sobre cada uno de ellos (incluso el tipo puede cambiar, claro). El siguiente código muestra por pantalla los números del 10, 20, 30, 40 y 50, cada uno en una lista.

list.range(1, 5)
    |> list.map(fn(x) { x * 10 })
    |> list.map(int.to_string)
    |> list.map(io.println)
Enter fullscreen mode Exit fullscreen mode

La primera línea crea la lista [1, 2, 3, 4, 5]. El primer mapping multiplica cada número por 10, obteniendo la nueva lista [10, 20, 30, 40, 50], el segundo mapping aplica la función int.to_string(x: Int) -> String a cada elemento en la lista, convirtiéndolo a cadena de caracteres. El tercer mapping aplica a cada elemento la función io.println(s: string), que visualiza s por pantalla (y salta de línea).

Cuando una función no es más que el parámetro de la misma llamando a la función f, se puede colocar f en su lugar. Así, en lugar de fn(x) {int.to_string(x)}, podemos escribir tan solo int.to_string.

Automaticemos ahora la obtención de la secuencia de Fibonacci, que haremos comenzar en el par 0, 1. A partir de ahí, el número i-ésimo se obtiene a partir de la suma de los valores anteriores i - 1 e i - 2. Así, el tercer valor en la secuencia será 1 (obteniendo [0, 1, 1]), el siguiente 2 ([0, 1, 1, 2]), el siguiente 3 ([0, 1, 1, 2, 3]), el siguiente 5 ([0, 1, 1, 2, 3, 5]), y así sucesivamente.

Así que, para obtener la lista para n - 1, teniendo en cuenta que la función se llamará fibo, haremos:

let l_fibo_n_1 = fibo(n - 1)
Enter fullscreen mode Exit fullscreen mode

De aquí tendremos que obtener el último y el penúltimo elemento. Gleam proporciona las funciones list.first(l: List(T)) -> T y list.last(l: List(T)) -> T, que devuelven el primer elemento de la lista, y el último, respectivamente. Hay que tener en cuenta que obtener el primer elemento tiene un coste computacional de O(1), mientras que obtener el último de O(n), siendo n el número de elementos en la lista. Por cierto, tenemos list.length(l: List(T)) -> Int, que nos proporciona el número de elementos de la lista... es O(n). Aún siendo listas enlazadas, teniendo en cuenta que son inmutables, me sorprende que un detalle como este exija recorrer toda la lista.

Además, Gleam nos proporciona la función list.drop(l: List(T), n: Int) -> List(T) que devuelve de una lista l, una nueva lista con los n primeros elementos eliminados. Por ejemplo, dada la lista l [1, 2, 3, 4, 5], list.drop(l, 3) devolvería la lista [4, 5].

Así, para obtener el último y el penúltimo elemento de la lista, utilizaremos list.drop() para eliminar todos los elementos de la lista excepto los dos últimos. Así, de esta lista de dos elementos, la primera posición será el penúltimo elemento, mientras que la segunda será el último elemento.

let l_fibo_n_1 = fibo(n - 1)
let lasts = l_fibo_n_1
    |> list.drop(list.length(l_fibo_n_1) - 2)

let v_fibo_n_1 = result.unwrap(lasts |> list.last(), 0)
let v_fibo_n_2 = result.unwrap(lasts |> list.first(), 0)
list.append(l_fibo_n_1, [v_fibo_n_1 + v_fibo_n_2])
Enter fullscreen mode Exit fullscreen mode

La única alternativa a esto consistiría en llamar a fibo(n - 1) para encontrar el último elemento, y de nuevo a fibo(n - 1) para el penúltimo, lo que sería extraordinariamente ineficiente.

La llamada a Result.unwrap(r: Result(T, U)) -> T que de un resultado obtiene la parte de los datos, sin importar si se ha producido un error o no (puesto que sabemos que no se producirá un error, ya que tenemos garantizado que como mínimo la lista obtenida mediante la llamada a fibo(n - 1) será, en el peor de los casos, [0, 1].

Finalmente, la llamada a list.append(l1, l2: List(T)) -> List(T) produce una nueva lista que se compondrá de la concatenación de l1 y l2.

El código completo aparece a continuación. ¿Y tú, has programado en algún lenguaje funcional?

import gleam/io
import gleam/list
import gleam/int
import gleam/result
import gleam/string


pub fn fibo(n: Int) -> List(Int) {
    case n {
        0 -> []
        1 -> [0]
        2 -> [0, 1]
        _ -> case n > 0 {
            True -> {
                let l_fibo_n_1 = fibo(n - 1)
                let lasts = l_fibo_n_1
                            |> list.drop(list.length(l_fibo_n_1) - 2)
                let v_fibo_n_1 = result.unwrap(lasts |> list.last(), 0)
                let v_fibo_n_2 = result.unwrap(lasts |> list.first(), 0)
                list.append(l_fibo_n_1, [v_fibo_n_1 + v_fibo_n_2])
              }
              False -> []
          }
    }
}


pub fn main() {
    io.println("Fibo: ")
    fibo(40)
    |> list.map(int.to_string)
    |> string.join(", ")
    |> io.println
}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .