Translate

sábado, 25 de mayo de 2024

Para proteger un tipo de datos en erlang


Los tipos de datos básicos de Erlang son fáciles de detectar visualmente: las tuplas tienen llaves, las listas tienen corchetes, las cadenas están entre comillas dobles, etc. Por lo tanto, hacer cumplir un determinado tipo de datos ha sido posible con la coincidencia de patrones: una función head/1 tomar una lista solo podría aceptar listas porque de lo contrario, la coincidencia ([H|_]) habría fallado.

Sin embargo, tuvimos un problema con los valores numéricos porque no podíamos especificar rangos. En consecuencia, utilizamos guardias en funciones sobre la temperatura, la edad para conducir, etc. Ahora nos topamos con otro obstáculo. ¿Cómo podríamos escribir una protección que garantice que los patrones coincidan con datos de un tipo específico, como números, átomos o cadenas de bits?

Hay funciones dedicadas a esta tarea. Tomarán un único argumento y devolverán verdadero si el tipo es correcto, falso en caso contrario. Son parte de las pocas funciones permitidas en las expresiones de protección y se denominan BIF de prueba de tipo:


is_atom/1           is_binary/1        

is_bitstring/1      is_boolean/1        is_builtin/3       

is_float/1          is_function/1       is_function/2      

is_integer/1        is_list/1           is_number/1        

is_pid/1            is_port/1           is_record/2        

is_record/3         is_reference/1      is_tuple/1         


Se pueden utilizar como cualquier otra expresión de guardia, siempre que se permitan expresiones de guardia. Quizás te preguntes por qué no existe una función que simplemente proporcione el tipo del término que se está evaluando (algo parecido a type_of(X) -> Type). La respuesta es bastante simple. Erlang se trata de programar para los casos correctos: solo programa para lo que sabe que sucederá y lo que espera. Todo lo demás debería provocar errores lo antes posible. Aunque esto pueda parecer una locura, es de esperar que las explicaciones que obtendrá en Errores y excepciones aclaren las cosas. 

Los BIF de prueba de tipo constituyen más de la mitad de las funciones permitidas en las expresiones de protección. El resto también son BIF, pero no representan pruebas de tipo. Estos son:
abs(Number), bit_size(Bitstring), byte_size(Bitstring), element(N, Tuple), float(Term), hd(List), length(List), node(), node(Pid|Ref|Port), round(Number), self(), size(Tuple|Bitstring), tl(List), trunc(Number), tuple_size(Tuple).

Las funciones nodo/1 y self/0 están relacionadas con Erlang distribuido y procesos/actores. Eventualmente los usaremos, pero todavía tenemos otros temas que cubrir antes de eso.

Puede parecer que las estructuras de datos de Erlang son relativamente limitadas, pero las listas y tuplas suelen ser suficientes para construir otras estructuras complejas sin preocuparse por nada. Como ejemplo, el nodo básico de un árbol binario podría representarse como {nodo, Valor, Izquierda, Derecha}, donde Izquierda y Derecha son nodos similares o tuplas vacías. También podría representarme como:

{person, {name, <<"Fred T-H">>},
{qualities, ["handsome", "smart", "honest", "objective"]},
{faults, ["liar"]},
{skills, ["programming", "bass guitar", "underwater breakdancing"]}}.

Lo que muestra que al anidar tuplas y enumerarlas y llenarlas con datos, podemos obtener estructuras de datos complejas y construir funciones para operar con ellas.

En la versión R13B04 se agregó el BIF binario_to_term/2, que le permite deserializar datos de la misma manera que lo haría binario_to_term/1, excepto que el segundo argumento es una lista de opciones. Si pasa [seguro], el binario no se decodificará si contiene átomos desconocidos o funciones anónimas, lo que podría agotar la memoria.


viernes, 24 de mayo de 2024

Patrones de lista en Gleam

 


import gleam/int

import gleam/io

import gleam/list


pub fn main() {

  let x = list.repeat(int.random(5), times: int.random(3))

  io.debug(x)


  let result = case x {

    [] -> "Empty list"

    [1] -> "List of just 1"

    [4, ..] -> "List starting with 4"

    [_, _] -> "List of 2 elements"

    _ -> "Some other list"

  }

  io.debug(result)

}

Las listas y los valores que contienen pueden coincidir con patrones en expresiones de caso.

Los patrones de lista coinciden en longitudes específicas de lista. El patrón [] coincide con una lista vacía y el patrón [_] coincide con una lista con un elemento. No coincidirán en listas con otras longitudes.

El patrón de distribución... se puede utilizar para que coincida con el resto de la lista. El patrón [1, ..] coincide con cualquier lista que comience con 1. El patrón [_, _, ..] coincide con cualquier lista que tenga al menos dos elementos.


lunes, 20 de mayo de 2024

Lenguajes utilizados en los proyectos apache

Tal vez hace mucho que Apache publico este gráfico pero yo recién lo veo : 



Como se puede ver Java es el lenguaje más utilizado por los proyectos de apache, seguido de python, c++, c, javascript, scala, C#, go, perl, etc ... 




domingo, 19 de mayo de 2024

Patrones en texto de Gleam


import gleam/io


pub fn main() {

  io.debug(get_name("Hello, Joe"))

  io.debug(get_name("Hello, Mike"))

  io.debug(get_name("System still working?"))

}


fn get_name(x: String) -> String {

  case x {

    "Hello, " <> name -> name

    _ -> "Unknown"

  }

}

Se puede utilizar patrones en cadenas de caracteres, el operador <> se puede usar para hacer coincidir cadenas con un prefijo específico.

El patrón "Hello, " <> nombre coincide con cualquier cadena que comience con "Hello, " y asigna el resto de la cadena al nombre de la variable.

jueves, 16 de mayo de 2024

Hacer un servicio con gRPC y go-zero


Hagamos un hola mundo con go-zero utilizando gRPC. Para empezar vamos a hacer el archivo .proto : 

syntax = "proto3";


package hello;


option go_package = "./hello";


message Request {

}


message Response {

  string msg = 1;

}


service Hello {

  rpc Ping(Request) returns(Response);

}


$ goctl rpc protoc hello.proto --go_out=server --go-grpc_out=server --zrpc_out=server

Done.

luego hacemos : 

cd server

go mod tidy 


Completamos el archivo server/internal/logic/pinglogic.go


func (l *PingLogic) Ping(in *hello.Request) (*hello.Response, error) {

return &hello.Response{ Msg: "pong" }, nil

}


y luego en el archivo server/etc/hello.yaml agregamos que estamos trabajando en modo dev: 


Name: hello.rpc
ListenOn: 0.0.0.0:8080
Mode: dev

y por ultimo corremos el proyecto: 

go run hello.go



martes, 14 de mayo de 2024

Conversiones de tipos en Erlang


Erlang, como muchos lenguajes, cambia el tipo de un término al convertirlo en otro. Esto se hace con la ayuda de funciones integradas, ya que muchas de las conversiones no se pudieron implementar en Erlang. Cada una de estas funciones toma la forma <tipo>_to_<tipo> y se implementa en el módulo erlang. Éstos son algunos de ellos:


1> erlang:list_to_integer("54").

54

2> erlang:integer_to_list(54).

"54"

3> erlang:list_to_integer("54.32").

** exception error: bad argument

     in function  list_to_integer/1

        called as list_to_integer("54.32")

4> erlang:list_to_float("54.32").

54.32

5> erlang:atom_to_list(true).

"true"

6> erlang:list_to_bitstring("hi there").

<<"hi there">>

7> erlang:bitstring_to_list(<<"hi there">>).

"hi there"


Etcétera. Estamos topando con un problema de lenguaje: debido a que se usa el esquema <tipo>_to_<tipo>, cada vez que se agrega un nuevo tipo al lenguaje, es necesario agregar una gran cantidad de BIF de conversión. Aquí está la lista:


atom_to_binary/2, atom_to_list/1, binary_to_atom/2, binary_to_existing_atom/2, binary_to_list/1, bitstring_to_list/1, binary_to_term/1, float_to_list/1, fun_to_list/1, integer_to_list/1, integer_to_list/2, iolist_to_binary/1, iolist_to_atom/1, list_to_atom/1, list_to_binary/1, list_to_bitstring/1, list_to_existing_atom/1, list_to_float/1, list_to_integer/2, list_to_pid/1, list_to_tuple/1, pid_to_list/1, port_to_list/1, ref_to_list/1, term_to_binary/1, term_to_binary/2 and tuple_to_list/1.



lunes, 13 de mayo de 2024

Tipos en Erlang


Erlang se escribe dinámicamente: cada error se detecta en tiempo de ejecución y el compilador no siempre le gritará al compilar módulos donde las cosas pueden resultar en fallas.

Un punto de fricción clásico entre los defensores de los lenguajes de tipado estático y dinámico tiene que ver con la seguridad del software que se escribe. Una idea sugerida con frecuencia es que los buenos sistemas de tipo estático con compiladores que los aplican con fervor detectarán la mayoría de los errores que esperan ocurrir antes de que puedas ejecutar el código. Como tal, los lenguajes escritos estáticamente deben considerarse más seguros que sus homólogos dinámicos. Si bien esto podría ser cierto en comparación con muchos lenguajes dinámicos, Erlang no está de acuerdo y ciertamente tiene un historial que lo demuestra. El mejor ejemplo son los nueve nueves (99,9999999%) de disponibilidad que se ofrecen en los conmutadores ATM Ericsson AXD 301, que constan de más de 1 millón de líneas de código Erlang. Tenga en cuenta que esto no es una indicación de que ninguno de los componentes de un sistema basado en Erlang haya fallado, sino que un sistema de conmutación general estuvo disponible el 99,9999999 % del tiempo, incluidas las interrupciones planificadas. Esto se debe en parte a que Erlang se basa en la noción de que una falla en uno de los componentes no debería afectar a todo el sistema. Se tienen en cuenta los errores provenientes del programador, fallas de hardware o [algunas] fallas de red: el lenguaje incluye características que le permitirán distribuir un programa a diferentes nodos, manejar errores inesperados y nunca dejar de ejecutarse.

Para abreviar, mientras que la mayoría de los lenguajes y sistemas de tipos tienen como objetivo hacer que un programa esté libre de errores, Erlang utiliza una estrategia en la que se supone que los errores ocurrirán de todos modos y se asegura de cubrir estos casos: El sistema de tipos dinámicos de Erlang no es una barrera para confiabilidad y seguridad de los programas. Esto suena como un montón de palabras proféticas, pero verás cómo se hace.

Históricamente se eligió la escritura dinámica por razones simples; Aquellos que implementaron Erlang al principio procedían en su mayoría de lenguajes escritos dinámicamente y, como tal, tener Erlang dinámico era la opción más natural para ellos.

Erlang también está fuertemente tipado. Un lenguaje débilmente tipado haría conversiones de tipos implícitas entre términos. Si Erlang tuviera un tipo débil, posiblemente podríamos hacer la operación 6 = 5 + "1". mientras que en la práctica, se lanzará una excepción:


1> 6 + "1".

** exception error: bad argument in an arithmetic expression

     in operator  +/2

        called as 6 + "1"


Por supuesto, hay ocasiones en las que es posible que desee convertir un tipo de datos en otro: cambiar cadenas normales en cadenas de bits para almacenarlas o un número entero en un número de punto flotante. La biblioteca estándar de Erlang proporciona una serie de funciones para hacerlo.



domingo, 12 de mayo de 2024

Patrones variables en Gleam

 


import gleam/int

import gleam/io


pub fn main() {

  let result = case int.random(5) {

    // Match specific values

    0 -> "Zero"

    1 -> "One"


    // Match any other value and assign it to a variable

    other -> "It is " <> int.to_string(other)

  }

  io.debug(result)

Los patrones en expresiones de caso también pueden asignar variables.

Cuando se utiliza un nombre de variable en un patrón, el valor con el que se compara se asigna a ese nombre y se puede utilizar en el cuerpo de esa cláusula. Como en other en el ejemplo. 

jueves, 9 de mayo de 2024

Que es un Higher-Kinded Type de Scala?


En Scala, un Higher-Kinded Type (HKT) es un tipo parametrizado que en sí mismo toma otro tipo parametrizado. Esto permite a los tipos ser abstractos sobre otros tipos parametrizados, lo que proporciona una mayor flexibilidad y abstracción en el diseño de bibliotecas y abstracciones de programación.

Para entender mejor qué es un HKT, es útil revisar algunos conceptos básicos:

Tipo parametrizado: En Scala, un tipo parametrizado es un tipo que toma uno o más parámetros de tipo. Por ejemplo, List[A] es un tipo parametrizado que toma un parámetro de tipo A.

Tipo de orden superior (Higher-Order Type): Un tipo de orden superior es un tipo que acepta otros tipos como parámetros. Por ejemplo, en el contexto de las funciones, una función de orden superior es una función que toma otra función como argumento.

Higher-Kinded Type (HKT): Un HKT es un tipo parametrizado que en sí mismo toma otro tipo parametrizado. En Scala, se denota utilizando el operador [_] o [*]. Por ejemplo, Option[_] o F[_] son HKTs, ya que pueden tomar tipos parametrizados como Option[Int] o Option[String].

Los HKTs son útiles en el contexto de la programación funcional y el diseño de bibliotecas genéricas. Permiten escribir código genérico que puede trabajar con diferentes tipos de datos sin conocer los detalles específicos de esos tipos. Por ejemplo, muchas bibliotecas de efectos en Scala, como Cats o Scalaz, utilizan HKTs para proporcionar abstracciones sobre diferentes tipos de efectos o contenedores de datos. Esto permite a los desarrolladores escribir código genérico que puede manipular efectos de diferentes tipos sin necesidad de modificar el código para cada tipo específico.

Vamos a crear una abstracción genérica para trabajar con contenedores de datos, independientemente de su tipo específico:


// Definición de un Higher-Kinded Type (HKT) F[_]

trait Container[F[_]] {

  def put[A](value: A): F[A]

  def get[A](container: F[A]): A

}


// Implementación de Container para List

object ListContainer extends Container[List] {

  def put[A](value: A): List[A] = List(value)

  def get[A](container: List[A]): A = container.head

}


// Implementación de Container para Option

object OptionContainer extends Container[Option] {

  def put[A](value: A): Option[A] = Some(value)

  def get[A](container: Option[A]): A = container.getOrElse(throw new NoSuchElementException("Empty container"))

}


object Main {

  def main(args: Array[String]): Unit = {

    // Uso de ListContainer

    val list = ListContainer.put(42)

    println("Value in list: " + ListContainer.get(list)) // Imprime: Value in list: 42

    

    // Uso de OptionContainer

    val option = OptionContainer.put(42)

    println("Value in option: " + OptionContainer.get(option)) // Imprime: Value in option: 42

  }

}

En este ejemplo, Container[F[_]] es un HKT que representa un contenedor genérico. La interfaz Container define métodos put y get que permiten poner y obtener valores de un contenedor F de tipo F[_]. Luego, proporcionamos implementaciones específicas de Container para diferentes tipos de contenedores: List y Option.

Esta abstracción nos permite escribir código genérico que funciona con cualquier tipo de contenedor, sin necesidad de conocer los detalles internos de cada uno. Por ejemplo, podemos utilizar ListContainer para trabajar con listas y OptionContainer para trabajar con opciones, todo usando la misma interfaz genérica Container. Esto proporciona una gran flexibilidad y reutilización de código en nuestras aplicaciones.

En Haskell, el concepto de Higher-Kinded Type (HKT) se manifiesta a través de los tipos de datos parametrizados que también son constructores de tipos. Esto permite definir abstracciones genéricas que pueden trabajar con diferentes tipos de datos sin conocer los detalles específicos de esos tipos.

Supongamos que queremos definir una abstracción genérica para trabajar con estructuras de datos que actúan como contenedores. Podemos utilizar un tipo de dato parametrizado f, donde f es un tipo de constructor de tipo, para representar nuestro contenedor genérico. Luego, definimos funciones genéricas que operan en este contenedor:


haskell

Copy code

-- Definición de un tipo de dato parametrizado f

class Container f where

  put :: a -> f a

  get :: f a -> a


-- Implementación de Container para List

instance Container [] where

  put x = [x]

  get (x:_) = x

  get _     = error "Empty list"


-- Implementación de Container para Maybe

instance Container Maybe where

  put = Just

  get (Just x) = x

  get Nothing  = error "Nothing"


-- Ejemplo de uso

main :: IO ()

main = do

  let list = put 42 :: [Int]

  putStrLn $ "Value in list: " ++ show (get list) -- Imprime: Value in list: 42

  

  let maybeVal = put 42 :: Maybe Int

  putStrLn $ "Value in Maybe: " ++ show (get maybeVal) -- Imprime: Value in Maybe: 42

En este ejemplo, Container es una clase de tipo que representa nuestra abstracción genérica para contenedores. La función put toma un valor y lo coloca en el contenedor, mientras que get extrae un valor del contenedor. Luego, proporcionamos instancias de Container para tipos específicos de contenedores como [] (lista) y Maybe.

Usando esta abstracción, podemos escribir código genérico que funciona con cualquier tipo de contenedor sin preocuparnos por los detalles internos de cada uno. Esto proporciona una gran flexibilidad y reutilización de código en nuestras aplicaciones Haskell.

miércoles, 8 de mayo de 2024

Que es la Covarianza, Contravarianza y Invarianza?


Ya sé que estos son temas super basicos pero siempre que hablan de eso me los confundo. Por eso voy a hacer este post para ver si porfin puedo fijar las ideas. 

La varianza y la covarianza son conceptos importantes en el contexto de los tipos genérico. Estos conceptos se refieren a cómo se relacionan los tipos genéricos cuando se consideran subtipos y super tipos. Veamos una explicación de cada uno:

Covarianza: La covarianza se refiere a la relación entre tipos genéricos donde la relación de subtipos se mantiene en la misma dirección que la relación de subtipos de los parámetros de tipo. En otras palabras, si tenemos un tipo T y otro tipo U donde U es un subtipo de T, entonces podemos decir que List<U> es un subtipo de List<T>. Esto significa que podemos asignar una lista de subtipos a una lista de supertipos sin necesidad de conversión explícita. La covarianza se utiliza típicamente en situaciones donde solo se lee de una estructura de datos, como en secuencias o enumeraciones.

Veamos un ejemplo en C#: 

using System;

using System.Collections.Generic;


class Program

{

    static void Main()

    {

        // Covarianza en IEnumerable<T>

        IEnumerable<string> strings = new List<string> { "hello", "world" };

        PrintItems(strings);

    }


    static void PrintItems(IEnumerable<object> items)

    {

        foreach (var item in items)

        {

            Console.WriteLine(item);

        }

    }

}

Veamos un ejemplo en Java: 

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Covarianza en List<? extends Number>
        List<Integer> integers = new ArrayList<>();
        integers.add(1);
        integers.add(2);
        printNumbers(integers);
    }

    static void printNumbers(List<? extends Number> numbers) {
        for (Number number : numbers) {
            System.out.println(number);
        }
    }
}

Y en scala: 

class Main {
  def main(args: Array[String]): Unit = {
    // Covarianza en List[+A]
    val strings: List[String] = List("hello", "world")
    printItems(strings)
  }

  def printItems(items: List[Any]): Unit = {
    items.foreach(println)
  }
}

Contravarianza: La contravarianza es el opuesto de la covarianza. Se refiere a la relación entre tipos genéricos donde la relación de subtipos se invierte en comparación con la relación de subtipos de los parámetros de tipo. En otras palabras, si tenemos un tipo T y otro tipo U donde U es un subtipo de T, entonces podemos decir que Action<T> es un subtipo de Action<U>. Esto significa que podemos asignar una función que acepta supertipos a una variable de función que acepta subtipos. La contravarianza se utiliza típicamente en situaciones donde solo se escribe en una estructura de datos, como en consumidores de datos o comparadores.

Veamos un ejemplo en Java: 


import java.util.function.Consumer;


public class Main {

    public static void main(String[] args) {

        // Contravarianza en Consumer<? super String>

        Consumer<Object> printString = Main::printString;

        printString.accept("hello");

    }


    static void printString(Object s) {

        System.out.println(s);

    }

}


Y en Scala: 

class Main {
  def main(args: Array[String]): Unit = {
    // Contravarianza en Function1[-T, +R]
    val printString: Any => Unit = Main.printString
    printString("hello")
  }
}

object Main {
  def printString(s: String): Unit = {
    println(s)
  }
}

Invarianza: La invarianza es el tercer escenario, donde no hay una relación de subtipos entre tipos genéricos. En otras palabras, List<T> no es ni un subtipo ni un supertipo de List<U> si T y U son tipos diferentes, incluso si U es un subtipo de T o viceversa. En este caso, necesitamos una conversión explícita para asignar entre los dos tipos.

En C#, la covarianza y la contravarianza se expresan a través de modificadores como out y in en declaraciones de tipo genérico. En Java, estos conceptos se implementan a través de la notación <? extends T> para la covarianza y <? super T> para la contravarianza en tipos genéricos. Es importante entender estos conceptos para escribir código genérico seguro y comprensible.

No recuerdo haber utilizado la contravarianza pero no se me hace algo tan común. Y ustedes, han utilizado estas técnicas? 


martes, 7 de mayo de 2024

Expresiones de casos en Gleam


import gleam/int

import gleam/io


pub fn main() {

  let x = int.random(5)

  io.debug(x)


  let result = case x {

    // Match specific values

    0 -> "Zero"

    1 -> "One"


    // Match any other value

    _ -> "Other"

  }

  io.debug(result)

}


La expresión de caso es el tipo más común de control de flujo en el código Gleam. Es similar al switch en otros lenguajes, pero más poderoso que la mayoría.

Permite al programador decir "si los datos tienen esta forma, entonces ejecute este código", un proceso llamado coincidencia de patrones.

Gleam realiza una verificación exhaustiva para garantizar que los patrones en una expresión de caso cubran todos los valores posibles. Con esto, puede tener la confianza de que su lógica está actualizada para el diseño de los datos con los que está trabajando.


sábado, 4 de mayo de 2024

¿Cuándo deberíamos usar funciones, if o case... of en Erlang ?


Cuál usar es bastante difícil de responder. La diferencia entre llamadas a funciones y case... of es mínima: de hecho, se representan de la misma manera a bajo nivel, y usar una u otra de manera efectiva tiene el mismo costo en términos de rendimiento. Una diferencia entre ambos es cuando es necesario evaluar más de un argumento: función (A, B) -> ... fin. puede tener guards y valores para comparar con A y B, pero una expresión de caso debería formularse de manera similar a:

case {A,B} of

Pattern Guards -> ...

end.


Esta forma rara vez se ve y puede sorprender un poco al lector. En situaciones similares, utilizar una llamada a función podría ser más apropiado. Por otro lado, la función insert/2 que habíamos escrito anteriormente es posiblemente más limpia tal como está en lugar de tener una llamada de función inmediata para rastrear una simple cláusula verdadera o falsa.

Entonces, la otra pregunta es ¿por qué usarías if, dados los casos y las funciones son lo suficientemente flexibles como para abarcarlos incluso a través de guards? La razón detrás de if es bastante simple: se agregó al lenguaje como una forma breve de tener guards sin necesidad de escribir toda la parte de pattern matching cuando no era necesario.

Por supuesto, todo esto tiene que ver más con las preferencias personales y con lo que puedes encontrar con más frecuencia. No hay una buena respuesta sólida. La comunidad de Erlang todavía debate. 

miércoles, 1 de mayo de 2024

Depreciaciones o marcar funciones como viejas en Gleam


pub fn main() {

  old_function()

  new_function()

}


@deprecated("Use new_function instead")

fn old_function() {

  Nil

}


fn new_function() {

  Nil

}


y la salida sería: 


warning: Deprecated value used

  ┌─ /src/main.gleam:2:3

  │

2 │   old_function()

  │   ^^^^^^^^^^^^ This value has been deprecated


Las funciones y otras definiciones se pueden marcar como obsoletas utilizando el atributo @deprecated.

Si se hace referencia a una función obsoleta, el compilador emitirá una advertencia, informándole al programador que debe actualizar su código.

El atributo de obsolescencia recibe un mensaje y este se mostrará al usuario en la advertencia. En el mensaje, se explica al usuario el nuevo enfoque o la función de reemplazo, o diríjalo a la documentación sobre cómo actualizar.

Case ... of de Erlang


Si la expresión if es como una Guards, Case ... of  es como el encabezado de la función completa: puedes tener la coincidencia de patrones complejos que puedes usar con cada argumento, ¡y puedes tener Guards!

Escribiremos la función de agregar para conjuntos (una colección de valores únicos) que representaremos como una lista desordenada:


insert(X,[]) ->

    [X];

insert(X,Set) ->

    case lists:member(X,Set) of

        true  -> Set;

        false -> [X|Set]

    end.


Si enviamos un conjunto vacío y un término X para agregar, nos devuelve una lista que contiene solo X. De lo contrario, la función de listas: member/2 verifica si un elemento es parte de una lista y devuelve verdadero si es, falso si no lo es. En el caso de que ya tuviéramos el elemento X en el conjunto, no necesitamos modificar la lista. De lo contrario, agregamos X como primer elemento de la lista.

En este caso, la combinación de patrones fue realmente sencilla. Puede volverse más complejo:


beach(Temperature) ->

    case Temperature of

        {celsius, N} when N >= 20, N =< 45 ->

            'favorable';

        {kelvin, N} when N >= 293, N =< 318 ->

            'scientifically favorable';

        {fahrenheit, N} when N >= 68, N =< 113 ->

            'favorable in the US';

        _ ->

            'avoid beach'

    end.


Aquí, la respuesta de "¿es el momento adecuado para ir a la playa?" se da en 3 sistemas de temperatura diferentes: grados Celsius, Kelvin y Fahrenheit. El pattern matching y guards se combinan para devolver una respuesta que satisfaga todos los usos. Como se señaló anteriormente, case... of expresiones son más o menos lo mismo que un grupo de cabezales funcionales con guards. De hecho podríamos haber escrito nuestro código de la siguiente manera:


beachf({celsius, N}) when N >= 20, N =< 45 ->

    'favorable';

...

beachf(_) ->

    'avoid beach'.


Esto plantea la pregunta: ¿cuándo deberíamos usar funciones if o case... of ?

Comentarios de documentación




//// A module containing some unusual functions and types.


/// A type where the value can never be constructed.

/// Can you work out why?

pub type Never {

  Never(Never)

}


/// Call a function twice with an initial value.

///

pub fn twice(argument: value, my_function: fn(value) -> value) -> value {

  my_function(my_function(argument))

}


/// Call a function three times with an initial value.

///

pub fn thrice(argument: value, my_function: fn(value) -> value) -> value {

  my_function(my_function(my_function(argument)))

}


La documentación y los comentarios son herramientas importantes para hacer que sea más fácil trabajar y comprender el código.

Además de los // comentarios regulares, Gleam tiene /// y //// comentarios que se utilizan para adjuntar documentación al código.

/// se utiliza para documentar tipos y funciones, y debe colocarse inmediatamente antes del tipo o función que se está documentando.

//// se utiliza para documentar módulos y debe colocarse en la parte superior del módulo.