Elixir - Guía rápida

Elixir es un lenguaje dinámico y funcional diseñado para crear aplicaciones escalables y fáciles de mantener. Aprovecha la máquina virtual Erlang, conocida por ejecutar sistemas de baja latencia, distribuidos y tolerantes a fallas, al mismo tiempo que se utiliza con éxito en el desarrollo web y el dominio del software integrado.

Elixir es un lenguaje funcional y dinámico construido sobre Erlang y Erlang VM. Erlang es un lenguaje que fue escrito originalmente en 1986 por Ericsson para ayudar a resolver problemas de telefonía como distribución, tolerancia a fallas y concurrencia. Elixir, escrito por José Valim, extiende Erlang y proporciona una sintaxis más amigable en Erlang VM. Lo hace manteniendo el rendimiento al mismo nivel que Erlang.

Características de Elixir

Analicemos ahora algunas características importantes de Elixir:

  • Scalability - Todo el código de Elixir se ejecuta dentro de procesos ligeros que están aislados e intercambian información a través de mensajes.

  • Fault Tolerance- Elixir proporciona supervisores que describen cómo reiniciar partes de su sistema cuando las cosas van mal, volviendo a un estado inicial conocido que está garantizado para funcionar. Esto asegura que su aplicación / plataforma nunca se caiga.

  • Functional Programming - La programación funcional promueve un estilo de codificación que ayuda a los desarrolladores a escribir código breve, rápido y fácil de mantener.

  • Build tools- Elixir se envía con un conjunto de herramientas de desarrollo. Mix es una de esas herramientas que facilita la creación de proyectos, la gestión de tareas, la ejecución de pruebas, etc. También tiene su propio administrador de paquetes, Hex.

  • Erlang Compatibility - Elixir se ejecuta en Erlang VM, lo que brinda a los desarrolladores acceso completo al ecosistema de Erlang.

Para ejecutar Elixir, debe configurarlo localmente en su sistema.

Para instalar Elixir, primero necesitará Erlang. En algunas plataformas, los paquetes de Elixir vienen con Erlang.

Instalación de Elixir

Entendamos ahora la instalación de Elixir en diferentes sistemas operativos.

Configuración de Windows

Para instalar Elixir en Windows, descargue el instalador de https://repo.hex.pm/elixirwebsetup.exe y simplemente haz clic Nextpara continuar con todos los pasos. Lo tendrá en su sistema local.

Si tiene algún problema al instalarlo, puede consultar esta página para obtener más información.

Configuración de Mac

Si tiene Homebrew instalado, asegúrese de que sea la última versión. Para actualizar, use el siguiente comando:

brew update

Ahora, instale Elixir usando el comando que se proporciona a continuación:

brew install elixir

Configuración de Ubuntu / Debian

Los pasos para instalar Elixir en una configuración de Ubuntu / Debian son los siguientes:

Agregar repositorio de Erlang Solutions -

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo 
dpkg -i erlang-solutions_1.0_all.deb 
sudo apt-get update

Instale la plataforma Erlang / OTP y todas sus aplicaciones -

sudo apt-get install esl-erlang

Instalar Elixir -

sudo apt-get install elixir

Otras distribuciones de Linux

Si tiene alguna otra distribución de Linux, visite esta página para configurar elixir en su sistema local.

Prueba de la configuración

Para probar la configuración de Elixir en su sistema, abra su terminal e ingrese iex en él. Abrirá el caparazón interactivo de elixir como el siguiente:

Erlang/OTP 19 [erts-8.0] [source-6dc93c1] [64-bit] 
[smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]  

Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help) 
iex(1)>

Elixir ahora está configurado correctamente en su sistema.

Comenzaremos con el programa habitual 'Hola mundo'.

Para iniciar el shell interactivo de Elixir, ingrese el siguiente comando.

iex

Después de que comience el shell, use el IO.putsfunción para "poner" la cadena en la salida de la consola. Ingrese lo siguiente en su caparazón de Elixir:

IO.puts "Hello world"

En este tutorial, usaremos el modo de script Elixir donde guardaremos el código Elixir en un archivo con la extensión .ex. Mantengamos ahora el código anterior en eltest.exarchivo. En el paso siguiente, lo ejecutaremos usandoelixirc-

IO.puts "Hello world"

Intentemos ahora ejecutar el programa anterior de la siguiente manera:

$elixirc test.ex

El programa anterior genera el siguiente resultado:

Hello World

Aquí estamos llamando a una función IO.putspara generar una cadena a nuestra consola como salida. Esta función también se puede llamar como lo hacemos en C, C ++, Java, etc., proporcionando argumentos entre paréntesis después del nombre de la función:

IO.puts("Hello world")

Comentarios

Los comentarios de una sola línea comienzan con un símbolo '#'. No hay comentarios de varias líneas, pero puede apilar varios comentarios. Por ejemplo

#This is a comment in Elixir

Finales de línea

No hay finales de línea obligatorios como ';' en Elixir. Sin embargo, podemos tener varias declaraciones en la misma línea, usando ';'. Por ejemplo,

IO.puts("Hello"); IO.puts("World!")

El programa anterior genera el siguiente resultado:

Hello 
World!

Identificadores

Los identificadores como variables, nombres de funciones se utilizan para identificar una variable, función, etc. En Elixir, puede nombrar sus identificadores comenzando con un alfabeto en minúscula con números, guiones bajos y letras mayúsculas a partir de entonces. Esta convención de nomenclatura se conoce comúnmente como snake_case. Por ejemplo, a continuación se muestran algunos identificadores válidos en Elixir:

var1       variable_2      one_M0r3_variable

Tenga en cuenta que las variables también se pueden nombrar con un guión bajo inicial. Un valor que no está destinado a ser utilizado debe asignarse a _ oa una variable que comience con un guión bajo -

_some_random_value = 42

También elixir se basa en guiones bajos para hacer que las funciones sean privadas para los módulos. Si asigna un nombre a una función con un guión bajo inicial en un módulo e importa ese módulo, esta función no se importará.

Hay muchas más complejidades relacionadas con la denominación de funciones en Elixir que analizaremos en los próximos capítulos.

Palabras reservadas

Las siguientes palabras están reservadas y no se pueden utilizar como variables, módulos o nombres de funciones.

after     and     catch     do     inbits     inlist     nil     else     end 
not     or     false     fn     in     rescue     true     when     xor 
__MODULE__    __FILE__    __DIR__    __ENV__    __CALLER__

Para usar cualquier idioma, debe comprender los tipos de datos básicos que admite el idioma. En este capítulo, discutiremos 7 tipos de datos básicos admitidos por el lenguaje elixir: enteros, flotantes, booleanos, átomos, cadenas, listas y tuplas.

Tipos numéricos

Elixir, como cualquier otro lenguaje de programación, admite tanto enteros como flotantes. Si abre su capa de elixir e ingresa cualquier número entero o flotante como entrada, devolverá su valor. Por ejemplo,

42

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

42

También puede definir números en bases octales, hexadecimales y binarias.

Octal

Para definir un número en base octal, antepóngalo con '0o'. Por ejemplo, 0o52 en octal equivale a 42 en decimal.

Hexadecimal

Para definir un número en base decimal, antepóngalo con '0x'. Por ejemplo, 0xF1 en hexadecimal equivale a 241 en decimal.

Binario

Para definir un número en base binaria, antepóngalo con '0b'. Por ejemplo, 0b1101 en binario es equivalente a 13 en decimal.

Elixir admite doble precisión de 64 bits para números de punto flotante. Y también se pueden definir utilizando un estilo de exponenciación. Por ejemplo, 10145230000 se puede escribir como 1.014523e10

Átomos

Los átomos son constantes cuyo nombre es su valor. Se pueden crear utilizando el símbolo de color (:). Por ejemplo,

:hello

Booleanos

Soportes de elixir true y falsecomo booleanos. De hecho, ambos valores están unidos a los átomos: verdadero y: falso respectivamente.

Instrumentos de cuerda

Las cadenas de Elixir se insertan entre comillas dobles y están codificadas en UTF-8. Pueden abarcar varias líneas y contener interpolaciones. Para definir una cadena, simplemente introdúzcala entre comillas dobles:

"Hello world"

Para definir cadenas de varias líneas, usamos una sintaxis similar a Python con comillas dobles triples:

"""
Hello
World!
"""

Aprenderemos sobre cadenas, binarios y listas de caracteres (similares a cadenas) en profundidad en el capítulo de cadenas.

Binarios

Los binarios son secuencias de bytes encerrados en << >> separados por una coma. Por ejemplo,

<< 65, 68, 75>>

Los binarios se utilizan principalmente para manejar datos relacionados con bits y bytes, si tiene alguno. Pueden, de forma predeterminada, almacenar de 0 a 255 en cada valor. Este límite de tamaño se puede aumentar usando la función de tamaño que dice cuántos bits se deben tomar para almacenar ese valor. Por ejemplo,

<<65, 255, 289::size(15)>>

Liza

Elixir usa corchetes para especificar una lista de valores. Los valores pueden ser de cualquier tipo. Por ejemplo,

[1, "Hello", :an_atom, true]

Las listas vienen con funciones incorporadas para el principio y el final de la lista denominada hd y tl que devuelven el principio y el final de la lista respectivamente. A veces, cuando crea una lista, devolverá una lista de caracteres. Esto se debe a que cuando elixir ve una lista de caracteres ASCII imprimibles, la imprime como una lista de caracteres. Tenga en cuenta que las cadenas y las listas de caracteres no son iguales. Analizaremos las listas con más detalle en capítulos posteriores.

Tuplas

Elixir utiliza llaves para definir tuplas. Como las listas, las tuplas pueden contener cualquier valor.

{ 1, "Hello", :an_atom, true

Aquí surge una pregunta: ¿por qué proporcionar tanto lists y tuplescuando ambos trabajan de la misma manera? Bueno, tienen diferentes implementaciones.

  • Las listas en realidad se almacenan como listas enlazadas, por lo que las inserciones y eliminaciones son muy rápidas en las listas.

  • Las tuplas, por otro lado, se almacenan en un bloque de memoria contiguo, lo que agiliza el acceso a ellas, pero agrega un costo adicional en inserciones y eliminaciones.

Una variable nos proporciona un almacenamiento con nombre que nuestros programas pueden manipular. Cada variable en Elixir tiene un tipo específico, que determina el tamaño y el diseño de la memoria de la variable; el rango de valores que se pueden almacenar dentro de esa memoria; y el conjunto de operaciones que se pueden aplicar a la variable.

Tipos de variables

Elixir admite los siguientes tipos básicos de variables.

Entero

Estos se utilizan para enteros. Tienen un tamaño de 32 bits en una arquitectura de 32 bits y de 64 bits en una arquitectura de 64 bits. Los enteros siempre se firman con elixir. Si un número entero comienza a expandirse en tamaño por encima de su límite, elixir conviértalo en un entero grande que ocupa memoria en un rango de 3 a n palabras, lo que pueda caber en la memoria.

Flotadores

Los flotadores tienen una precisión de 64 bits en elixir. También son como números enteros en términos de memoria. Al definir un flotante, se puede utilizar la notación exponencial.

Booleano

Pueden tomar 2 valores que sean verdaderos o falsos.

Instrumentos de cuerda

Las cadenas están codificadas en utf-8 en elixir. Tienen un módulo de cadenas que proporciona mucha funcionalidad al programador para manipular cadenas.

Funciones anónimas / Lambdas

Estas son funciones que se pueden definir y asignar a una variable, que luego se puede utilizar para llamar a esta función.

Colecciones

Hay muchos tipos de colecciones disponibles en Elixir. Algunos de ellos son listas, tuplas, mapas, binarios, etc. Estos se analizarán en capítulos posteriores.

Declaración de variable

Una declaración de variable le dice al intérprete dónde y cuánto crear el almacenamiento para la variable. Elixir no nos permite simplemente declarar una variable. Una variable debe declararse y asignarse un valor al mismo tiempo. Por ejemplo, para crear una variable llamada vida y asignarle un valor 42, hacemos lo siguiente:

life = 42

Esto vinculará la vida de la variable al valor 42. Si queremos reasignar a esta variable un nuevo valor, podemos hacerlo usando la misma sintaxis anterior, es decir,

life = "Hello world"

Nomenclatura variable

Las variables de nomenclatura siguen un snake_caseconvención en Elixir, es decir, todas las variables deben comenzar con una letra minúscula, seguida de 0 o más letras (tanto mayúsculas como minúsculas), seguidas al final de un opcional '?' O '!'.

Los nombres de las variables también se pueden comenzar con un guión bajo inicial, pero deben usarse solo cuando se ignora la variable, es decir, esa variable no se usará nuevamente pero es necesario asignarla a algo.

Impresión de variables

En el shell interactivo, las variables se imprimirán si solo ingresa el nombre de la variable. Por ejemplo, si crea una variable:

life = 42

Y ingrese 'vida' en su caparazón, obtendrá el resultado como:

42

Pero si desea enviar una variable a la consola (cuando ejecuta un script externo desde un archivo), debe proporcionar la variable como entrada para IO.puts función -

life = 42  
IO.puts life

o

life = 42 
IO.puts(life)

Esto le dará el siguiente resultado:

42

Un operador es un símbolo que le dice al compilador que realice manipulaciones matemáticas o lógicas específicas. Hay MUCHOS operadores proporcionados por elixir. Se dividen en las siguientes categorías:

  • Operadores aritméticos
  • Operadores de comparación
  • operadores booleanos
  • Operadores varios

Operadores aritméticos

La siguiente tabla muestra todos los operadores aritméticos compatibles con el lenguaje Elixir. Asumir variableA tiene 10 y variable B tiene 20, entonces -

Mostrar ejemplos

Operador Descripción Ejemplo
+ Suma 2 números. A + B dará 30
- Resta el segundo número del primero. AB dará -10
* Multiplica dos números. A * B dará 200
/ Divide el primer número del segundo. Esto arroja los números en flotantes y da un resultado flotante A / B dará 0,5.
div Esta función se usa para obtener el cociente en la división. div (10,20) dará 0
movimiento rápido del ojo Esta función se utiliza para obtener el resto de la división. rem (A, B) dará 10

Operadores de comparación

Los operadores de comparación en Elixir son en su mayoría comunes a los que se proporcionan en la mayoría de los otros idiomas. La siguiente tabla resume los operadores de comparación en Elixir. Asumir variableA tiene 10 y variable B tiene 20, entonces -

Mostrar ejemplos

Operador Descripción Ejemplo
== Comprueba si el valor de la izquierda es igual al valor de la derecha (el tipo arroja valores si no son del mismo tipo). A == B dará falso
! = Comprueba si el valor de la izquierda no es igual al valor de la derecha. A! = B dará verdadero
=== Comprueba si el tipo de valor de la izquierda es igual al tipo de valor de la derecha, si es así, marca el mismo valor. A === B dará falso
! == Igual que el anterior, pero verifica la desigualdad en lugar de la igualdad. A! == B dará verdadero
> Comprueba si el valor del operando izquierdo es mayor que el valor del operando derecho; si es así, entonces la condición se vuelve verdadera. A> B dará falso
< Comprueba si el valor del operando izquierdo es menor que el valor del operando derecho; si es así, entonces la condición se vuelve verdadera. A <B dará verdadero
> = Comprueba si el valor del operando izquierdo es mayor o igual que el valor del operando derecho; si es así, entonces la condición se vuelve verdadera. A> = B dará falso
<= Comprueba si el valor del operando izquierdo es menor o igual que el valor del operando derecho; si es así, entonces la condición se vuelve verdadera. A <= B dará verdadero

Operadores logicos

Elixir proporciona 6 operadores lógicos: y, o, no, &&, || y!. Los primeros tres,and or notson operadores booleanos estrictos, lo que significa que esperan que su primer argumento sea un booleano. El argumento no booleano generará un error. Mientras que los siguientes tres,&&, || and !son no estrictos, no requieren que tengamos el primer valor estrictamente como booleano. Funcionan de la misma manera que sus contrapartes estrictas. Asumir variableA se mantiene verdadero y variable B tiene 20, entonces -

Mostrar ejemplos

Operador Descripción Ejemplo
y Comprueba si ambos valores proporcionados son verdaderos, si es así, devuelve el valor de la segunda variable. (Lógico y). A y B darán 20
o Comprueba si alguno de los valores proporcionados es verdadero. Devuelve el valor que sea verdadero. Else devuelve falso. (Lógico o). A o B darán verdadero
no Operador unario que invierte el valor de la entrada dada. no A dará falso
&& No estricto and. Funciona igual queand pero no espera que el primer argumento sea booleano. B && A dará 20
|| No estricto or. Funciona igual queor pero no espera que el primer argumento sea booleano. B || Un daré verdad
! No estricto not. Funciona igual quenot pero no espera que el argumento sea booleano. ! A dará falso

NOTE −y , o , && y || || son operadores de cortocircuito. Esto significa que si el primer argumento deandes falso, entonces no comprobará más el segundo. Y si el primer argumento deores verdadero, entonces no buscará el segundo. Por ejemplo,

false and raise("An error")  
#This won't raise an error as raise function wont get executed because of short
#circuiting nature of and operator

Operadores bit a bit

Los operadores bit a bit trabajan en bits y realizan operaciones bit a bit. Elixir proporciona módulos bit a bit como parte del paqueteBitwise, por lo que para usarlos, debe usar el módulo bit a bit. Para usarlo, ingrese el siguiente comando en su shell:

use Bitwise

Suponga que A es 5 y B es 6 para los siguientes ejemplos:

Mostrar ejemplos

Operador Descripción Ejemplo
&&& Bitwise y el operador copia un bit al resultado si existe en ambos operandos. A &&& B dará 4
||| Bit a bit o el operador copia un bit al resultado si existe en cualquiera de los operandos. A ||| B dará 7
>>> El operador de desplazamiento a la derecha bit a bit desplaza los primeros bits del operando a la derecha por el número especificado en el segundo operando. A >>> B dará 0
<<< El operador de desplazamiento a la izquierda bit a bit desplaza los primeros bits del operando a la izquierda por el número especificado en el segundo operando. A <<< B dará 320
^^^ El operador XOR bit a bit copia un bit al resultado solo si es diferente en ambos operandos. A ^^^ B dará 3
~~~ Unario bit a bit no invierte los bits en el número dado. ~~~ A dará -6

Operadores varios

Aparte de los operadores anteriores, Elixir también proporciona una gama de otros operadores como Concatenation Operator, Match Operator, Pin Operator, Pipe Operator, String Match Operator, Code Point Operator, Capture Operator, Ternary Operator que lo convierten en un lenguaje bastante poderoso.

Mostrar ejemplos

La coincidencia de patrones es una técnica que Elixir hereda de Erlang. Es una técnica muy poderosa que nos permite extraer subestructuras más simples de estructuras de datos complicadas como listas, tuplas, mapas, etc.

Un partido tiene 2 partes principales, una left y un rightlado. El lado derecho es una estructura de datos de cualquier tipo. El lado izquierdo intenta hacer coincidir la estructura de datos del lado derecho y vincular cualquier variable del lado izquierdo a la subestructura respectiva del lado derecho. Si no se encuentra una coincidencia, el operador genera un error.

La coincidencia más simple es una variable solitaria a la izquierda y cualquier estructura de datos a la derecha. This variable will match anything. Por ejemplo,

x = 12
x = "Hello"
IO.puts(x)

Puede colocar variables dentro de una estructura para poder capturar una subestructura. Por ejemplo,

[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ]
IO.puts(var_1)
IO.puts(var_2)

Esto almacenará los valores, {"First variable"}en var_1 y"Second variable"en var_2 . También hay un especial_ variable (o variables con el prefijo '_') que funciona exactamente como otras variables pero dice elixir, "Make sure something is here, but I don't care exactly what it is.". En el ejemplo anterior, _unused_var era una de esas variables.

Podemos unir patrones más complicados usando esta técnica. porexample si desea desenvolver y obtener un número en una tupla que está dentro de una lista que a su vez está en una lista, puede usar el siguiente comando:

[_, [_, {a}]] = ["Random string", [:an_atom, {24}]]
IO.puts(a)

El programa anterior genera el siguiente resultado:

24

Esto unirá a a 24. Los demás valores se ignoran ya que estamos usando '_'.

En la coincidencia de patrones, si usamos una variable en el right, se utiliza su valor. Si desea utilizar el valor de una variable de la izquierda, deberá utilizar el operador de alfiler.

Por ejemplo, si tiene una variable "a" que tiene un valor de 25 y desea que coincida con otra variable "b" que tiene un valor de 25, entonces debe ingresar -

a = 25
b = 25
^a = b

La última línea coincide con el valor actual de a, en lugar de asignarlo, al valor de b. Si tenemos un conjunto de lados izquierdo y derecho que no coinciden, el operador de coincidencia genera un error. Por ejemplo, si intentamos hacer coincidir una tupla con una lista o una lista de tamaño 2 con una lista de tamaño 3, se mostrará un error.

Las estructuras de toma de decisiones requieren que el programador especifique una o más condiciones para ser evaluadas o probadas por el programa, junto con una declaración o declaraciones que se ejecutarán si se determina que la condición es truey, opcionalmente, otras sentencias que se ejecutarán si se determina que la condición es false.

A continuación se muestra la forma general de una estructura de toma de decisiones típica que se encuentra en la mayor parte del lenguaje de programación:

Elixir proporciona construcciones condicionales if / else como muchos otros lenguajes de programación. También tiene unconddeclaración que llama al primer valor verdadero que encuentra. Case es otra declaración de flujo de control que utiliza la coincidencia de patrones para controlar el flujo del programa. Echemos un vistazo profundo a ellos.

Elixir proporciona los siguientes tipos de declaraciones para la toma de decisiones. Haga clic en los siguientes enlaces para verificar su detalle.

No Señor. Declaración y descripción
1 si declaración

Una sentencia if consta de una expresión booleana seguida de do, una o más sentencias ejecutables y finalmente una endpalabra clave. El código en la instrucción if se ejecuta solo si la condición booleana se evalúa como verdadera.

2 declaración if..else

Una instrucción if puede ir seguida de una instrucción else opcional (dentro del bloque do..end), que se ejecuta cuando la expresión booleana es falsa.

3 a menos que la declaración

Una instrucción a menos que tenga el mismo cuerpo que una instrucción if. El código dentro de la instrucción less se ejecuta solo cuando la condición especificada es falsa.

4 a menos que ... otra declaración

Una sentencia a menos que ... else tiene el mismo cuerpo que una sentencia if ... else. El código dentro de la instrucción less se ejecuta solo cuando la condición especificada es falsa.

5 cond

Se usa una instrucción cond cuando queremos ejecutar código sobre la base de varias condiciones. Funciona como una construcción if ... else if ... .else en varios otros lenguajes de programación.

6 caso

La declaración de caso se puede considerar como un reemplazo de la declaración de cambio en lenguajes imperativos. Case toma una variable / literal y le aplica una coincidencia de patrones con diferentes casos. Si algún caso coincide, Elixir ejecuta el código asociado con ese caso y sale de la declaración del caso.

Las cadenas de Elixir se insertan entre comillas dobles y están codificadas en UTF-8. A diferencia de C y C ++, donde las cadenas predeterminadas están codificadas en ASCII y solo son posibles 256 caracteres diferentes, UTF-8 consta de 1.112.064 puntos de código. Esto significa que la codificación UTF-8 consta de muchos caracteres posibles diferentes. Dado que las cadenas usan utf-8, también podemos usar símbolos como: ö, ł, etc.

Crear una cadena

Para crear una variable de cadena, simplemente asigne una cadena a una variable:

str = "Hello world"

Para imprimir esto en su consola, simplemente llame al IO.puts función y pasarle la variable str -

str = str = "Hello world" 
IO.puts(str)

El programa anterior genera el siguiente resultado:

Hello World

Cadenas vacías

Puede crear una cadena vacía utilizando el literal de cadena, "". Por ejemplo,

a = ""
if String.length(a) === 0 do
   IO.puts("a is an empty string")
end

El programa anterior genera el siguiente resultado.

a is an empty string

Interpolación de cadenas

La interpolación de cadenas es una forma de construir un nuevo valor de cadena a partir de una combinación de constantes, variables, literales y expresiones al incluir sus valores dentro de una cadena literal. Elixir admite la interpolación de cadenas, para usar una variable en una cadena, al escribirla, envuélvala con llaves y anteponga las llaves con una'#' firmar.

Por ejemplo,

x = "Apocalypse" 
y = "X-men #{x}"
IO.puts(y)

Esto tomará el valor de x y lo sustituirá por y. El código anterior generará el siguiente resultado:

X-men Apocalypse

Concatenación de cadenas

Ya hemos visto el uso de la concatenación de cadenas en capítulos anteriores. El operador '<>' se usa para concatenar cadenas en Elixir. Para concatenar 2 cadenas,

x = "Dark"
y = "Knight"
z = x <> " " <> y
IO.puts(z)

El código anterior genera el siguiente resultado:

Dark Knight

Longitud de la cuerda

Para obtener la longitud de la cadena, usamos el String.lengthfunción. Pasa la cadena como parámetro y te mostrará su tamaño. Por ejemplo,

IO.puts(String.length("Hello"))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

5

Inversión de una cuerda

Para invertir una cadena, pásala a la función String.reverse. Por ejemplo,

IO.puts(String.reverse("Elixir"))

El programa anterior genera el siguiente resultado:

rixilE

Comparación de cadenas

Para comparar 2 cadenas, podemos usar los operadores == o ===. Por ejemplo,

var_1 = "Hello world"
var_2 = "Hello Elixir"
if var_1 === var_2 do
   IO.puts("#{var_1} and #{var_2} are the same")
else
   IO.puts("#{var_1} and #{var_2} are not the same")
end

El programa anterior genera el siguiente resultado:

Hello world and Hello elixir are not the same.

Coincidencia de cadenas

Ya hemos visto el uso del operador de coincidencia de cadenas = ~. Para comprobar si una cadena coincide con una expresión regular, también podemos usar el operador de coincidencia de cadenas o String.match? función. Por ejemplo,

IO.puts(String.match?("foo", ~r/foo/))
IO.puts(String.match?("bar", ~r/foo/))

El programa anterior genera el siguiente resultado:

true 
false

Esto mismo también se puede lograr usando el operador = ~. Por ejemplo,

IO.puts("foo" =~ ~r/foo/)

El programa anterior genera el siguiente resultado:

true

Funciones de cadena

Elixir admite una gran cantidad de funciones relacionadas con cadenas, algunas de las más utilizadas se enumeran en la siguiente tabla.

No Señor. Función y su finalidad
1

at(string, position)

Devuelve el grafema en la posición de la cadena utf8 dada. Si la posición es mayor que la longitud de la cadena, devuelve nil

2

capitalize(string)

Convierte el primer carácter de la cadena dada a mayúsculas y el resto a minúsculas

3

contains?(string, contents)

Comprueba si la cadena contiene alguno de los contenidos dados.

4

downcase(string)

Convierte todos los caracteres en la cadena dada a minúsculas

5

ends_with?(string, suffixes)

Devuelve verdadero si la cadena termina con cualquiera de los sufijos dados

6

first(string)

Devuelve el primer grafema de una cadena utf8, nulo si la cadena está vacía

7

last(string)

Devuelve el último grafema de una cadena utf8, nulo si la cadena está vacía

8

replace(subject, pattern, replacement, options \\ [])

Devuelve una nueva cadena creada al reemplazar ocurrencias de patrón en el sujeto con reemplazo

9

slice(string, start, len)

Devuelve una subcadena que comienza en el inicio del desplazamiento y de longitud len

10

split(string)

Divide una cadena en subcadenas en cada aparición de espacio en blanco Unicode con los espacios en blanco iniciales y finales ignorados. Los grupos de espacios en blanco se tratan como una sola aparición. Las divisiones no ocurren en espacios en blanco que no se rompen

11

upcase(string)

Convierte todos los caracteres en la cadena dada a mayúsculas

Binarios

Un binario es solo una secuencia de bytes. Los binarios se definen usando<< >>. Por ejemplo:

<< 0, 1, 2, 3 >>

Por supuesto, esos bytes se pueden organizar de cualquier manera, incluso en una secuencia que no los convierte en una cadena válida. Por ejemplo,

<< 239, 191, 191 >>

Las cadenas también son binarios. Y el operador de concatenación de cadenas<> es en realidad un operador de concatenación binario:

IO.puts(<< 0, 1 >> <> << 2, 3 >>)

El código anterior genera el siguiente resultado:

<< 0, 1, 2, 3 >>

Tenga en cuenta el carácter ł. Dado que está codificado en utf-8, esta representación de caracteres ocupa 2 bytes.

Dado que cada número representado en un binario está destinado a ser un byte, cuando este valor sube de 255, se trunca. Para evitar esto, usamos el modificador de tamaño para especificar cuántos bits queremos que tome ese número. Por ejemplo

IO.puts(<< 256 >>) # truncated, it'll print << 0 >>
IO.puts(<< 256 :: size(16) >>) #Takes 16 bits/2 bytes, will print << 1, 0 >>

El programa anterior generará el siguiente resultado:

<< 0 >>
<< 1, 0 >>

También podemos usar el modificador utf8, si un carácter es un punto de código, se producirá en la salida; más los bytes -

IO.puts(<< 256 :: utf8 >>)

El programa anterior genera el siguiente resultado:

Ā

También tenemos una función llamada is_binaryque comprueba si una variable dada es binaria. Tenga en cuenta que solo las variables que se almacenan como múltiplos de 8 bits son binarios.

Bitstrings

Si definimos un binario usando el modificador de tamaño y le pasamos un valor que no es múltiplo de 8, terminamos con una cadena de bits en lugar de un binario. Por ejemplo,

bs = << 1 :: size(1) >>
IO.puts(bs)
IO.puts(is_binary(bs))
IO.puts(is_bitstring(bs))

El programa anterior genera el siguiente resultado:

<< 1::size(1) >>
false
true

Esto significa que la variable bsno es un binario sino más bien una cadena de bits. También podemos decir que un binario es una cadena de bits donde el número de bits es divisible por 8. La coincidencia de patrones funciona tanto en binarios como en cadenas de bits de la misma manera.

Una lista de caracteres no es más que una lista de personajes. Considere el siguiente programa para entender lo mismo.

IO.puts('Hello')
IO.puts(is_list('Hello'))

El programa anterior genera el siguiente resultado:

Hello
true

En lugar de contener bytes, una lista de caracteres contiene los puntos de código de los caracteres entre comillas simples. So while the double-quotes represent a string (i.e. a binary), singlequotes represent a char list (i.e. a list). Tenga en cuenta que IEx generará solo puntos de código como salida si alguno de los caracteres está fuera del rango ASCII.

Las listas de caracteres se utilizan principalmente cuando se interactúa con Erlang, en particular las bibliotecas antiguas que no aceptan binarios como argumentos. Puede convertir una lista de caracteres en una cadena y viceversa utilizando las funciones to_string (char_list) y to_char_list (string) -

IO.puts(is_list(to_char_list("hełło")))
IO.puts(is_binary(to_string ('hełło')))

El programa anterior genera el siguiente resultado:

true
true

NOTE - Las funciones to_string y to_char_list son polimórficos, es decir, pueden tomar múltiples tipos de entrada como átomos, enteros y convertirlos en cadenas y listas de caracteres respectivamente.

Listas (vinculadas)

Una lista enlazada es una lista heterogénea de elementos que se almacenan en diferentes ubicaciones en la memoria y de los que se hace un seguimiento mediante el uso de referencias. Las listas enlazadas son estructuras de datos especialmente utilizadas en programación funcional.

Elixir usa corchetes para especificar una lista de valores. Los valores pueden ser de cualquier tipo:

[1, 2, true, 3]

Cuando Elixir ve una lista de números ASCII imprimibles, Elixir lo imprimirá como una lista de caracteres (literalmente, una lista de caracteres). Siempre que vea un valor en IEx y no esté seguro de cuál es, puede usar eli función para recuperar información sobre él.

IO.puts([104, 101, 108, 108, 111])

Los caracteres anteriores de la lista se pueden imprimir. Cuando se ejecuta el programa anterior, produce el siguiente resultado:

hello

También puede definir listas al revés, utilizando comillas simples:

IO.puts(is_list('Hello'))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

true

Tenga en cuenta que las representaciones entre comillas simples y dobles no son equivalentes en Elixir ya que están representadas por diferentes tipos.

Longitud de una lista

Para encontrar la longitud de una lista, usamos la función de longitud como en el siguiente programa:

IO.puts(length([1, 2, :true, "str"]))

El programa anterior genera el siguiente resultado:

4

Concatenación y resta

Se pueden concatenar y restar dos listas usando el ++ y --operadores. Considere el siguiente ejemplo para comprender las funciones.

IO.puts([1, 2, 3] ++ [4, 5, 6])
IO.puts([1, true, 2, false, 3, true] -- [true, false])

Esto le dará una cadena concatenada en el primer caso y una cadena restada en el segundo. El programa anterior genera el siguiente resultado:

[1, 2, 3, 4, 5, 6]
[1, 2, 3, true]

Cabeza y cola de una lista

La cabeza es el primer elemento de una lista y la cola es el resto de una lista. Se pueden recuperar con las funcioneshd y tl. Asignemos una lista a una variable y recuperemos su cabeza y su cola.

list = [1, 2, 3]
IO.puts(hd(list))
IO.puts(tl(list))

Esto nos dará el principio y el final de la lista como salida. El programa anterior genera el siguiente resultado:

1
[2, 3]

Note - Obtener la cabeza o la cola de una lista vacía es un error.

Otras funciones de lista

La biblioteca estándar de Elixir proporciona una gran cantidad de funciones para manejar listas. Echaremos un vistazo a algunos de ellos aquí. Puedes ver el resto aquí Lista .

S.no. Nombre y descripción de la función
1

delete(list, item)

Elimina el elemento dado de la lista. Devuelve una lista sin el artículo. Si el elemento aparece más de una vez en la lista, solo se elimina la primera aparición.

2

delete_at(list, index)

Produce una nueva lista eliminando el valor en el índice especificado. Los índices negativos indican un desplazamiento desde el final de la lista. Si el índice está fuera de los límites, se devuelve la lista original.

3

first(list)

Devuelve el primer elemento de la lista o nulo si la lista está vacía.

4

flatten(list)

Aplana la lista dada de listas anidadas.

5

insert_at(list, index, value)

Devuelve una lista con el valor insertado en el índice especificado. Tenga en cuenta que el índice está limitado a la longitud de la lista. Los índices negativos indican un desplazamiento desde el final de la lista.

6

last(list)

Devuelve el último elemento de la lista o nulo si la lista está vacía.

Tuplas

Las tuplas también son estructuras de datos que almacenan una serie de otras estructuras dentro de ellas. A diferencia de las listas, almacenan elementos en un bloque de memoria contiguo. Esto significa que acceder a un elemento de tupla por índice o obtener el tamaño de la tupla es una operación rápida. Los índices comienzan desde cero.

Elixir utiliza llaves para definir tuplas. Como las listas, las tuplas pueden contener cualquier valor:

{:ok, "hello"}

Duración de una tupla

Para obtener la longitud de una tupla, use el tuple_size funciona como en el siguiente programa -

IO.puts(tuple_size({:ok, "hello"}))

El programa anterior genera el siguiente resultado:

2

Agregar un valor

Para agregar un valor a la tupla, use la función Tuple.append -

tuple = {:ok, "Hello"}
Tuple.append(tuple, :world)

Esto creará y devolverá una nueva tupla: {: ok, "Hola",: mundo}

Insertar un valor

Para insertar un valor en una posición dada, podemos usar el Tuple.insert_at función o la put_elemfunción. Considere el siguiente ejemplo para entender lo mismo:

tuple = {:bar, :baz}
new_tuple_1 = Tuple.insert_at(tuple, 0, :foo)
new_tuple_2 = put_elem(tuple, 1, :foobar)

Darse cuenta de put_elem y insert_atdevolvió nuevas tuplas. La tupla original almacenada en la variable de tupla no se modificó porque los tipos de datos de Elixir son inmutables. Al ser inmutable, el código Elixir es más fácil de razonar, ya que nunca debe preocuparse si un código en particular está mutando su estructura de datos en su lugar.

Tuplas frente a listas

¿Cuál es la diferencia entre listas y tuplas?

Las listas se almacenan en la memoria como listas enlazadas, lo que significa que cada elemento de una lista mantiene su valor y apunta al siguiente elemento hasta que se llega al final de la lista. Llamamos a cada par de valor y puntero una celda de contras. Esto significa que acceder a la longitud de una lista es una operación lineal: necesitamos recorrer toda la lista para calcular su tamaño. Actualizar una lista es rápido siempre que agreguemos elementos.

Las tuplas, por otro lado, se almacenan de forma contigua en la memoria. Esto significa que obtener el tamaño de la tupla o acceder a un elemento por índice es rápido. Sin embargo, actualizar o agregar elementos a las tuplas es costoso porque requiere copiar toda la tupla en la memoria.

Hasta ahora, no hemos discutido ninguna estructura de datos asociativa, es decir, estructuras de datos que puedan asociar un cierto valor (o múltiples valores) a una clave. Los diferentes lenguajes llaman a estas características con diferentes nombres como diccionarios, hashes, matrices asociativas, etc.

En Elixir, tenemos dos estructuras de datos asociativas principales: listas de palabras clave y mapas. En este capítulo, nos centraremos en las listas de palabras clave.

En muchos lenguajes de programación funcional, es común utilizar una lista de tuplas de 2 elementos como representación de una estructura de datos asociativa. En Elixir, cuando tenemos una lista de tuplas y el primer elemento de la tupla (es decir, la clave) es un átomo, lo llamamos lista de palabras clave. Considere el siguiente ejemplo para entender lo mismo:

list = [{:a, 1}, {:b, 2}]

Elixir admite una sintaxis especial para definir tales listas. Podemos colocar los dos puntos al final de cada átomo y deshacernos de las tuplas por completo. Por ejemplo,

list_1 = [{:a, 1}, {:b, 2}]
list_2 = [a: 1, b: 2]
IO.puts(list_1 == list_2)

El programa anterior generará el siguiente resultado:

true

Ambos representan una lista de palabras clave. Dado que las listas de palabras clave también son listas, podemos usar todas las operaciones que usamos en las listas en ellas.

Para recuperar el valor asociado con un átomo en la lista de palabras clave, pase el átomo como [] después del nombre de la lista -

list = [a: 1, b: 2]
IO.puts(list[:a])

El programa anterior genera el siguiente resultado:

1

Las listas de palabras clave tienen tres características especiales:

  • Las claves deben ser átomos.
  • Las claves se ordenan, según lo especificado por el desarrollador.
  • Las llaves se pueden entregar más de una vez.

Para manipular listas de palabras clave, Elixir proporciona el módulo de palabras clave . No obstante, recuerde que las listas de palabras clave son simplemente listas y, como tales, proporcionan las mismas características de rendimiento lineal que las listas. Cuanto más larga sea la lista, más tardará en encontrar una clave, contar el número de elementos, etc. Por esta razón, las listas de palabras clave se utilizan en Elixir principalmente como opciones. Si necesita almacenar muchos elementos o garantizar asociados de una tecla con un valor único máximo, debe usar mapas en su lugar.

Accediendo a una clave

Para acceder a los valores asociados con una clave dada, usamos el Keyword.getfunción. Devuelve el primer valor asociado con la clave dada. Para obtener todos los valores, usamos la función Keyword.get_values. Por ejemplo

kl = [a: 1, a: 2, b: 3] 
IO.puts(Keyword.get(kl, :a)) 
IO.puts(Keyword.get_values(kl))

El programa anterior generará el siguiente resultado:

1
[1, 2]

Insertar una llave

Para agregar un nuevo valor, use Keyword.put_new. Si la clave ya existe, su valor permanece sin cambios -

kl = [a: 1, a: 2, b: 3]
kl_new = Keyword.put_new(kl, :c, 5)
IO.puts(Keyword.get(kl_new, :c))

Cuando se ejecuta el programa anterior, produce una nueva lista de palabras clave con la clave adicional, cy genera el siguiente resultado:

5

Eliminar una clave

Si desea eliminar todas las entradas de una clave, utilice Keyword.delete; para eliminar solo la primera entrada de una clave, utilice Keyword.delete_first.

kl = [a: 1, a: 2, b: 3, c: 0]
kl = Keyword.delete_first(kl, :b)
kl = Keyword.delete(kl, :a)

IO.puts(Keyword.get(kl, :a))
IO.puts(Keyword.get(kl, :b))
IO.puts(Keyword.get(kl, :c))

Esto eliminará la primera b en la Lista y todos los aen la lista. Cuando se ejecuta el programa anterior, generará el siguiente resultado:

0

Las listas de palabras clave son una forma conveniente de abordar el contenido almacenado en listas por clave, pero debajo, Elixir sigue recorriendo la lista. Eso podría ser adecuado si tiene otros planes para esa lista que requieren recorrerlo todo, pero puede ser una sobrecarga innecesaria si planea usar claves como su único enfoque para los datos.

Aquí es donde los mapas vienen a rescatarte. Siempre que necesite un almacén de valores-clave, los mapas son la estructura de datos a la que "acudir" en Elixir.

Crear un mapa

Se crea un mapa con la sintaxis% {}:

map = %{:a => 1, 2 => :b}

En comparación con las listas de palabras clave, ya podemos ver dos diferencias:

  • Los mapas permiten cualquier valor como clave.
  • Las claves de Maps no siguen ningún orden.

Accediendo a una clave

Para acceder al valor asociado con una clave, Maps usa la misma sintaxis que las listas de palabras clave:

map = %{:a => 1, 2 => :b}
IO.puts(map[:a])
IO.puts(map[2])

Cuando se ejecuta el programa anterior, genera el siguiente resultado:

1
b

Insertar una llave

Para insertar una clave en un mapa, usamos el Dict.put_new función que toma el mapa, la nueva clave y el nuevo valor como argumentos -

map = %{:a => 1, 2 => :b}
new_map = Dict.put_new(map, :new_val, "value") 
IO.puts(new_map[:new_val])

Esto insertará el par clave-valor :new_val - "value"en un mapa nuevo. Cuando se ejecuta el programa anterior, genera el siguiente resultado:

"value"

Actualizar un valor

Para actualizar un valor ya presente en el mapa, puede usar la siguiente sintaxis:

map = %{:a => 1, 2 => :b}
new_map = %{ map | a: 25}
IO.puts(new_map[:a])

Cuando se ejecuta el programa anterior, genera el siguiente resultado:

25

La coincidencia de patrones

A diferencia de las listas de palabras clave, los mapas son muy útiles con la coincidencia de patrones. Cuando se utiliza un mapa en un patrón, siempre coincidirá en un subconjunto del valor dado:

%{:a => a} = %{:a => 1, 2 => :b}
IO.puts(a)

El programa anterior genera el siguiente resultado:

1

Esto coincidirá a con 1. Y por lo tanto, generará la salida como1.

Como se muestra arriba, un mapa coincide siempre que las claves del patrón existan en el mapa dado. Por lo tanto, un mapa vacío coincide con todos los mapas.

Las variables se pueden usar al acceder, hacer coincidir y agregar claves de mapa:

n = 1
map = %{n => :one}
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}

El módulo Mapa proporciona una API muy similar al módulo Palabra clave con funciones de conveniencia para manipular mapas. Puede utilizar funciones como laMap.get, Map.delete, para manipular mapas.

Mapas con claves Atom

Los mapas vienen con algunas propiedades interesantes. Cuando todas las claves en un mapa son átomos, puede usar la sintaxis de palabras clave para su conveniencia:

map = %{:a => 1, 2 => :b} 
IO.puts(map.a)

Otra propiedad interesante de los mapas es que proporcionan su propia sintaxis para actualizar y acceder a las claves de átomo:

map = %{:a => 1, 2 => :b}
IO.puts(map.a)

El programa anterior genera el siguiente resultado:

1

Tenga en cuenta que para acceder a las claves de átomo de esta manera, debería existir o el programa no funcionará.

En Elixir, agrupamos varias funciones en módulos. Ya hemos utilizado diferentes módulos en los capítulos anteriores como el módulo String, módulo Bitwise, módulo Tuple, etc.

Para crear nuestros propios módulos en Elixir, usamos el defmodulemacro. Usamos eldef macro para definir funciones en ese módulo -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

En las siguientes secciones, nuestros ejemplos aumentarán de tamaño y puede ser complicado escribirlos todos en el shell. Necesitamos aprender a compilar código de Elixir y también a ejecutar scripts de Elixir.

Compilacion

Siempre es conveniente escribir módulos en archivos para que puedan compilarse y reutilizarse. Supongamos que tenemos un archivo llamado math.ex con el siguiente contenido:

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Podemos compilar los archivos usando el comando -elixirc :

$ elixirc math.ex

Esto generará un archivo llamado Elixir.Math.beamque contiene el bytecode para el módulo definido. Si empezamosiexnuevamente, nuestra definición de módulo estará disponible (siempre que iex se inicie en el mismo directorio en el que se encuentra el archivo de código de bytes). Por ejemplo,

IO.puts(Math.sum(1, 2))

El programa anterior generará el siguiente resultado:

3

Modo con guión

Además de la extensión de archivo Elixir .ex, Elixir también admite .exsarchivos para secuencias de comandos. Elixir trata ambos archivos exactamente de la misma manera, la única diferencia está en el objetivo..ex Los archivos están destinados a compilarse, mientras que los archivos .exs se utilizan para scripting. Cuando se ejecutan, ambas extensiones compilan y cargan sus módulos en la memoria, aunque solo.ex los archivos escriben su código de bytes en el disco en el formato de archivos .beam.

Por ejemplo, si quisiéramos ejecutar el Math.sum en el mismo archivo, podemos usar los .exs de la siguiente manera:

Math.exs

defmodule Math do
   def sum(a, b) do
      a + b
   end
end
IO.puts(Math.sum(1, 2))

Podemos ejecutarlo usando el comando Elixir -

$ elixir math.exs

El programa anterior generará el siguiente resultado:

3

El archivo se compilará en la memoria y se ejecutará, imprimiendo “3” como resultado. No se creará ningún archivo de código de bytes.

Anidamiento de módulos

Los módulos se pueden anidar en Elixir. Esta característica del lenguaje nos ayuda a organizar nuestro código de una mejor manera. Para crear módulos anidados, usamos la siguiente sintaxis:

defmodule Foo do
   #Foo module code here
   defmodule Bar do
      #Bar module code here
   end
end

El ejemplo anterior definirá dos módulos: Foo y Foo.Bar. Se puede acceder al segundo comoBar dentro Foosiempre que estén en el mismo ámbito léxico. Si, más tarde, elBar El módulo se mueve fuera de la definición del módulo Foo, debe ser referenciado por su nombre completo (Foo.Bar) o se debe establecer un alias usando la directiva de alias discutida en el capítulo de alias.

Note- En Elixir, no es necesario definir el módulo Foo para definir el módulo Foo.Bar, ya que el lenguaje traduce todos los nombres de los módulos a átomos. Puede definir módulos anidados arbitrariamente sin definir ningún módulo en la cadena. Por ejemplo, puede definirFoo.Bar.Baz sin definir Foo o Foo.Bar.

Para facilitar la reutilización del software, Elixir proporciona tres directivas: alias, require y import. También proporciona una macro llamada uso que se resume a continuación:

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Ensure the module is compiled and available (usually for macros)
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

Entendamos ahora en detalle sobre cada directiva.

alias

La directiva de alias le permite configurar alias para cualquier nombre de módulo dado. Por ejemplo, si quiere dar un alias'Str' al módulo String, simplemente puede escribir -

alias String, as: Str
IO.puts(Str.length("Hello"))

El programa anterior genera el siguiente resultado:

5

Se le da un alias al String módulo como Str. Ahora, cuando llamamos a cualquier función usando el literal Str, en realidad hace referencia alStringmódulo. Esto es muy útil cuando usamos nombres de módulos muy largos y queremos sustituirlos por otros más cortos en el ámbito actual.

NOTE - Alias MUST comience con una letra mayúscula.

Los alias son válidos solo dentro del lexical scope se llaman. Por ejemplo, si tiene 2 módulos en un archivo y crea un alias dentro de uno de los módulos, ese alias no será accesible en el segundo módulo.

Si da el nombre de un módulo incorporado, como String o Tuple, como alias de algún otro módulo, para acceder al módulo incorporado, deberá anteponerlo con "Elixir.". Por ejemplo,

alias List, as: String
#Now when we use String we are actually using List.
#To use the string module: 
IO.puts(Elixir.String.length("Hello"))

Cuando se ejecuta el programa anterior, genera el siguiente resultado:

5

exigir

Elixir proporciona macros como mecanismo de metaprogramación (escribir código que genera código).

Las macros son fragmentos de código que se ejecutan y expanden durante la compilación. Esto significa que, para utilizar una macro, debemos garantizar que su módulo y su implementación estén disponibles durante la compilación. Esto se hace con elrequire directiva.

Integer.is_odd(3)

Cuando se ejecuta el programa anterior, generará el siguiente resultado:

** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1

En Elixir, Integer.is_odd se define como un macro. Esta macro se puede utilizar como protección. Esto significa que, para invocarInteger.is_odd, necesitaremos el módulo Integer.

Utilizar el require Integer función y ejecute el programa como se muestra a continuación.

require Integer
Integer.is_odd(3)

Esta vez, el programa se ejecutará y producirá la salida como: true.

En general, no se requiere un módulo antes de su uso, excepto si queremos usar las macros disponibles en ese módulo. Un intento de llamar a una macro que no se cargó generará un error. Tenga en cuenta que, al igual que la directiva alias, require también tiene un ámbito léxico . Hablaremos más sobre macros en un capítulo posterior.

importar

Usamos el importdirectiva para acceder fácilmente a funciones o macros de otros módulos sin utilizar el nombre completo. Por ejemplo, si queremos usar elduplicate función del módulo List varias veces, simplemente podemos importarlo.

import List, only: [duplicate: 2]

En este caso, estamos importando solo la función duplicada (con una longitud de lista de argumentos 2) de List. A pesar de que:only es opcional, se recomienda su uso para evitar importar todas las funciones de un módulo dado dentro del espacio de nombres. :except también se podría dar como una opción para importar todo en un módulo excepto una lista de funciones.

los import la directiva también apoya :macros y :functions para ser dado a :only. Por ejemplo, para importar todas las macros, un usuario puede escribir:

import Integer, only: :macros

Tenga en cuenta que la importación también es Lexically scopedal igual que las directivas require y alias. También tenga en cuenta que'import'ing a module also 'require's it.

utilizar

Aunque no es una directiva, use es una macro estrechamente relacionada con requireque le permite utilizar un módulo en el contexto actual. Los desarrolladores utilizan con frecuencia la macro de uso para incorporar funciones externas al ámbito léxico actual, a menudo módulos. Entendamos la directiva de uso a través de un ejemplo:

defmodule Example do 
   use Feature, option: :value 
end

El uso es una macro que transforma lo anterior en:

defmodule Example do
   require Feature
   Feature.__using__(option: :value)
end

los use Module primero requiere el módulo y luego llama al __using__macro en el módulo. Elixir tiene grandes capacidades de metaprogramación y tiene macros para generar código en tiempo de compilación. La macro _ _using__ se llama en la instancia anterior y el código se inyecta en nuestro contexto local. El contexto local es donde se llamó a la macro de uso en el momento de la compilación.

Una función es un conjunto de declaraciones organizadas juntas para realizar una tarea específica. Las funciones en programación funcionan principalmente como funciones en matemáticas. Le das a las funciones algo de entrada, generan resultados basados ​​en la entrada proporcionada.

Hay 2 tipos de funciones en Elixir:

Función anónima

Funciones definidas mediante el fn..end constructson funciones anónimas. Estas funciones a veces también se denominan lambdas. Se utilizan asignándolos a nombres de variables.

Función nombrada

Funciones definidas mediante el def keywordson funciones nombradas. Estas son funciones nativas proporcionadas en Elixir.

Funciones anónimas

Como su nombre lo indica, una función anónima no tiene nombre. Estos se pasan con frecuencia a otras funciones. Para definir una función anónima en Elixir, necesitamos elfn y endpalabras clave. Dentro de estos, podemos definir cualquier número de parámetros y cuerpos de función separados por->. Por ejemplo,

sum = fn (a, b) -> a + b end
IO.puts(sum.(1, 5))

Cuando se ejecuta el programa anterior, se ejecuta, genera el siguiente resultado:

6

Tenga en cuenta que estas funciones no se llaman como las funciones nombradas. Tenemos una '.'entre el nombre de la función y sus argumentos.

Uso del operador de captura

También podemos definir estas funciones usando el operador de captura. Este es un método más sencillo para crear funciones. Ahora definiremos la función de suma anterior usando el operador de captura,

sum = &(&1 + &2) 
IO.puts(sum.(1, 2))

Cuando se ejecuta el programa anterior, genera el siguiente resultado:

3

En la versión abreviada, nuestros parámetros no tienen nombre, pero están disponibles para nosotros como & 1, & 2, & 3, y así sucesivamente.

Funciones de coincidencia de patrones

La coincidencia de patrones no se limita solo a variables y estructuras de datos. Podemos usar la coincidencia de patrones para hacer que nuestras funciones sean polimórficas. Por ejemplo, declararemos una función que puede tomar 1 o 2 entradas (dentro de una tupla) e imprimirlas en la consola,

handle_result = fn
   {var1} -> IO.puts("#{var1} found in a tuple!")
   {var_2, var_3} -> IO.puts("#{var_2} and #{var_3} found!")
end
handle_result.({"Hey people"})
handle_result.({"Hello", "World"})

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hey people found in a tuple!
Hello and World found!

Funciones nombradas

Podemos definir funciones con nombres para poder referirnos a ellas fácilmente más adelante. Las funciones nombradas se definen dentro de un módulo usando la palabra clave def. Las funciones nombradas siempre se definen en un módulo. Para llamar a funciones nombradas, necesitamos hacer referencia a ellas usando su nombre de módulo.

La siguiente es la sintaxis para funciones con nombre:

def function_name(argument_1, argument_2) do
   #code to be executed when function is called
end

Definamos ahora nuestra función denominada suma dentro del módulo de matemáticas.

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

IO.puts(Math.sum(5, 6))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

11

Para las funciones de 1 línea, hay una notación abreviada para definir estas funciones, usando do:. Por ejemplo

defmodule Math do
   def sum(a, b), do: a + b
end
IO.puts(Math.sum(5, 6))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

11

Funciones privadas

Elixir nos brinda la posibilidad de definir funciones privadas a las que se puede acceder desde dentro del módulo en el que están definidas. Para definir una función privada, usedefp en vez de def. Por ejemplo,

defmodule Greeter do
   def hello(name), do: phrase <> name
   defp phrase, do: "Hello "
end

Greeter.hello("world")

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hello world

Pero si solo intentamos llamar explícitamente a la función de frase, usando el Greeter.phrase() función, generará un error.

Argumentos predeterminados

Si queremos un valor predeterminado para un argumento, usamos el argument \\ value sintaxis -

defmodule Greeter do
   def hello(name, country \\ "en") do
      phrase(country) <> name
   end

   defp phrase("en"), do: "Hello, "
   defp phrase("es"), do: "Hola, "
end

Greeter.hello("Ayush", "en")
Greeter.hello("Ayush")
Greeter.hello("Ayush", "es")

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hello, Ayush
Hello, Ayush
Hola, Ayush

La recursividad es un método en el que la solución a un problema depende de las soluciones a instancias más pequeñas del mismo problema. La mayoría de los lenguajes de programación de computadoras admiten la recursividad al permitir que una función se llame a sí misma dentro del texto del programa.

Idealmente, las funciones recursivas tienen una condición final. Esta condición final, también conocida como caso base, deja de volver a ingresar a la función y agregar llamadas a la función a la pila. Aquí es donde se detiene la llamada a la función recursiva. Consideremos el siguiente ejemplo para comprender mejor la función recursiva.

defmodule Math do
   def fact(res, num) do
   if num === 1 do
      res
   else
      new_res = res * num
      fact(new_res, num-1)
      end
   end
end

IO.puts(Math.fact(1,5))

Cuando se ejecuta el programa anterior, genera el siguiente resultado:

120

Entonces, en la función anterior, Math.fact, estamos calculando el factorial de un número. Tenga en cuenta que estamos llamando a la función dentro de sí misma. Entendamos ahora cómo funciona esto.

Le hemos proporcionado 1 y el número cuyo factorial queremos calcular. La función comprueba si el número es 1 o no y devuelve res si es 1(Ending condition). Si no es así, crea una variable new_res y le asigna el valor de res anterior * num actual. Devuelve el valor devuelto por nuestra función llamada fact (new_res, num-1) . Esto se repite hasta que obtenemos num como 1. Una vez que eso sucede, obtenemos el resultado.

Consideremos otro ejemplo, imprimiendo cada elemento de la lista uno por uno. Para hacer esto, utilizaremos elhd y tl funciones de listas y coincidencia de patrones en funciones -

a = ["Hey", 100, 452, :true, "People"]
defmodule ListPrint do
   def print([]) do
   end
   def print([head | tail]) do 
      IO.puts(head)
      print(tail)
   end
end

ListPrint.print(a)

La primera función de impresión se llama cuando tenemos una lista vacía(ending condition). De lo contrario, se llamará a la segunda función de impresión, que dividirá la lista en 2 y asignará el primer elemento de la lista al encabezado y el resto de la lista al final. A continuación, se imprime el encabezado y volvemos a llamar a la función de impresión con el resto de la lista, es decir, cola. Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hey
100
452
true
People

Debido a la inmutabilidad, los bucles en Elixir (como en cualquier lenguaje de programación funcional) se escriben de manera diferente a los lenguajes imperativos. Por ejemplo, en un lenguaje imperativo como C, escribirás:

for(i = 0; i < 10; i++) {
   printf("%d", array[i]);
}

En el ejemplo anterior, estamos mutando tanto la matriz como la variable i. La mutación no es posible en Elixir. En cambio, los lenguajes funcionales se basan en la recursividad: una función se llama de forma recursiva hasta que se alcanza una condición que impide que la acción recursiva continúe. No se modifican datos en este proceso.

Escribamos ahora un ciclo simple usando recursividad que imprima hola n veces.

defmodule Loop do
   def print_multiple_times(msg, n) when n <= 1 do
      IO.puts msg
   end

   def print_multiple_times(msg, n) do
      IO.puts msg
      print_multiple_times(msg, n - 1)
   end
end

Loop.print_multiple_times("Hello", 10)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

Hemos utilizado las técnicas de coincidencia de patrones de funciones y la recursividad para implementar con éxito un bucle. Las definiciones recursivas son difíciles de entender, pero la conversión de bucles en recursividad es fácil.

Elixir nos proporciona el Enum module. Este módulo se usa para las llamadas en bucle más iterativas, ya que es mucho más fácil usarlas que tratar de encontrar definiciones recursivas para las mismas. Los discutiremos en el próximo capítulo. Sus propias definiciones recursivas solo deben usarse cuando no encuentre una solución usando ese módulo. Esas funciones están optimizadas para llamadas de cola y son bastante rápidas.

Un enumerable es un objeto que se puede enumerar. "Enumerado" significa contar los miembros de un conjunto / colección / categoría uno por uno (generalmente en orden, generalmente por nombre).

Elixir proporciona el concepto de enumerables y el módulo Enum para trabajar con ellos. Las funciones del módulo Enum se limitan, como su nombre lo indica, a enumerar valores en estructuras de datos. Un ejemplo de una estructura de datos enumerable es una lista, tupla, mapa, etc. El módulo Enum nos proporciona un poco más de 100 funciones para manejar las enumeraciones. Discutiremos algunas funciones importantes en este capítulo.

Todas estas funciones toman un enumerable como primer elemento y una función como segundo y trabajan en ellas. Las funciones se describen a continuación.

¿todas?

Cuando usamos all? función, toda la colección debe evaluarse como verdadera; de lo contrario, se devolverá falso. Por ejemplo, para comprobar si todos los elementos de la lista son números impares, entonces.

res = Enum.all?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end) 
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

false

Esto se debe a que no todos los elementos de esta lista son extraños.

¿ninguna?

Como sugiere el nombre, esta función devuelve verdadero si cualquier elemento de la colección se evalúa como verdadero. Por ejemplo

res = Enum.any?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end)
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

true

pedazo

Esta función divide nuestra colección en pequeños fragmentos del tamaño proporcionado como segundo argumento. Por ejemplo

res = Enum.chunk([1, 2, 3, 4, 5, 6], 2)
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

[[1, 2], [3, 4], [5, 6]]

cada

Puede ser necesario iterar sobre una colección sin producir un nuevo valor, para este caso usamos el each función -

Enum.each(["Hello", "Every", "one"], fn(s) -> IO.puts(s) end)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hello
Every
one

mapa

Para aplicar nuestra función a cada elemento y producir una nueva colección usamos la función de mapa. Es una de las construcciones más útiles en programación funcional ya que es bastante expresiva y corta. Consideremos un ejemplo para entender esto. Duplicaremos los valores almacenados en una lista y los almacenaremos en una nueva listares -

res = Enum.map([2, 5, 3, 6], fn(a) -> a*2 end)
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

[4, 10, 6, 12]

reducir

los reduceLa función nos ayuda a reducir nuestro enumerable a un solo valor. Para hacer esto, proporcionamos un acumulador opcional (5 en este ejemplo) para pasar a nuestra función; si no se proporciona un acumulador, se utiliza el primer valor:

res = Enum.reduce([1, 2, 3, 4], 5, fn(x, accum) -> x + accum end)
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

15

El acumulador es el valor inicial pasado al fn. A partir de la segunda llamada en adelante, el valor devuelto por la llamada anterior se pasa como acumulado. También podemos usar reducir sin el acumulador -

res = Enum.reduce([1, 2, 3, 4], fn(x, accum) -> x + accum end)
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

10

uniq

La función uniq elimina los duplicados de nuestra colección y devuelve solo el conjunto de elementos de la colección. Por ejemplo

res = Enum.uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

[1, 2, 3, 4]

Evaluación entusiasta

Todas las funciones del módulo Enum están ansiosas. Muchas funciones esperan un enumerable y devuelven una lista. Esto significa que al realizar múltiples operaciones con Enum, cada operación va a generar una lista intermedia hasta llegar al resultado. Consideremos el siguiente ejemplo para entender esto:

odd? = &(odd? = &(rem(&1, 2) != 0) 
res = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

7500000000

El ejemplo anterior tiene una cartera de operaciones. Comenzamos con un rango y luego multiplicamos cada elemento en el rango por 3. Esta primera operación ahora creará y devolverá una lista con 100_000 elementos. Luego mantenemos todos los elementos impares de la lista, generando una nueva lista, ahora con 50_000 elementos, y luego sumamos todas las entradas.

los |> El símbolo utilizado en el fragmento de arriba es el pipe operator: simplemente toma la salida de la expresión en su lado izquierdo y la pasa como el primer argumento a la llamada de función en su lado derecho. Es similar a Unix | operador. Su propósito es resaltar el flujo de datos que está siendo transformado por una serie de funciones.

Sin el pipe operador, el código parece complicado -

Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))

Tenemos muchas otras funciones, sin embargo, aquí solo se han descrito algunas importantes.

Muchas funciones esperan un enumerable y devuelven un listespalda. Significa que, al realizar múltiples operaciones con Enum, cada operación generará una lista intermedia hasta que alcancemos el resultado.

Las transmisiones admiten operaciones perezosas en lugar de operaciones ansiosas por enumeraciones. En breve,streams are lazy, composable enumerables. Lo que esto significa es que Streams no realiza una operación a menos que sea absolutamente necesario. Consideremos un ejemplo para entender esto:

odd? = &(rem(&1, 2) != 0)
res = 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
IO.puts(res)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

7500000000

En el ejemplo dado arriba, 1..100_000 |> Stream.map(&(&1 * 3))devuelve un tipo de datos, un flujo real, que representa el cálculo del mapa en el rango 1..100_000. Aún no ha evaluado esta representación. En lugar de generar listas intermedias, los flujos construyen una serie de cálculos que se invocan solo cuando pasamos el flujo subyacente al módulo Enum. Los flujos son útiles cuando se trabaja con colecciones grandes, posiblemente infinitas.

Las transmisiones y las enumeraciones tienen muchas funciones en común. Los flujos proporcionan principalmente las mismas funciones proporcionadas por el módulo Enum que generó Listas como sus valores de retorno después de realizar cálculos en enumerables de entrada. Algunos de ellos se enumeran en la siguiente tabla:

No Señor. Función y su descripción
1

chunk(enum, n, step, leftover \\ nil)

Transmite el enumerable en fragmentos, que contienen n elementos cada uno, donde cada nuevo fragmento inicia los elementos del paso en el enumerable.

2

concat(enumerables)

Crea una secuencia que enumera cada enumerable en un enumerable.

3

each(enum, fun)

Ejecuta la función dada para cada elemento.

4

filter(enum, fun)

Crea una secuencia que filtra elementos según la función dada en la enumeración.

5

map(enum, fun)

Crea una secuencia que aplicará la función dada en la enumeración.

6

drop(enum, n)

Deja caer perezosamente los siguientes n elementos del enumerable.

Las estructuras son extensiones creadas sobre mapas que proporcionan comprobaciones en tiempo de compilación y valores predeterminados.

Definición de estructuras

Para definir una estructura, se utiliza la construcción defstruct:

defmodule User do
   defstruct name: "John", age: 27
end

La lista de palabras clave utilizada con defstruct define qué campos tendrá la estructura junto con sus valores predeterminados. Las estructuras toman el nombre del módulo en el que están definidas. En el ejemplo anterior, definimos una estructura denominada Usuario. Ahora podemos crear estructuras de usuario usando una sintaxis similar a la que se usa para crear mapas:

new_john = %User{})
ayush = %User{name: "Ayush", age: 20}
megan = %User{name: "Megan"})

El código anterior generará tres estructuras diferentes con valores:

%User{age: 27, name: "John"}
%User{age: 20, name: "Ayush"}
%User{age: 27, name: "Megan"}

Las estructuras proporcionan garantías en tiempo de compilación de que solo los campos (y todos ellos) definidos mediante defstruct podrán existir en una estructura. Por lo tanto, no puede definir sus propios campos una vez que haya creado la estructura en el módulo.

Acceder y actualizar estructuras

Cuando hablamos de mapas, mostramos cómo podemos acceder y actualizar los campos de un mapa. Las mismas técnicas (y la misma sintaxis) se aplican también a las estructuras. Por ejemplo, si queremos actualizar el usuario que creamos en el ejemplo anterior, entonces -

defmodule User do
   defstruct name: "John", age: 27
end
john = %User{}
#john right now is: %User{age: 27, name: "John"}

#To access name and age of John, 
IO.puts(john.name)
IO.puts(john.age)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

John
27

Para actualizar un valor en una estructura, usaremos nuevamente el mismo procedimiento que usamos en el capítulo del mapa,

meg = %{john | name: "Meg"}

Las estructuras también se pueden utilizar en la coincidencia de patrones, tanto para hacer coincidir el valor de claves específicas como para garantizar que el valor coincidente sea una estructura del mismo tipo que el valor coincidente.

Los protocolos son un mecanismo para lograr polimorfismo en Elixir. El envío en un protocolo está disponible para cualquier tipo de datos siempre que implemente el protocolo.

Consideremos un ejemplo de uso de protocolos. Usamos una función llamadato_stringen los capítulos anteriores para convertir de otros tipos al tipo de cadena. Este es en realidad un protocolo. Actúa de acuerdo con la entrada que se da sin producir error. Esto puede parecer que estamos discutiendo funciones de coincidencia de patrones, pero a medida que avanzamos, resulta diferente.

Considere el siguiente ejemplo para comprender mejor el mecanismo del protocolo.

Creemos un protocolo que mostrará si la entrada dada está vacía o no. Llamaremos a este protocoloblank?.

Definición de un protocolo

Podemos definir un protocolo en Elixir de la siguiente manera:

defprotocol Blank do
   def blank?(data)
end

Como puede ver, no necesitamos definir un cuerpo para la función. Si está familiarizado con las interfaces en otros lenguajes de programación, puede pensar en un Protocolo como esencialmente lo mismo.

Así que este Protocolo dice que todo lo que lo implemente debe tener un empty?función, aunque depende del implementador cómo responde la función. Con el protocolo definido, entendamos cómo agregar un par de implementaciones.

Implementar un protocolo

Dado que hemos definido un protocolo, ahora necesitamos decirle cómo manejar las diferentes entradas que podría obtener. Construyamos sobre el ejemplo que habíamos tomado anteriormente. Implementaremos el protocolo en blanco para listas, mapas y cadenas. Esto mostrará si lo que pasamos está en blanco o no.

#Defining the protocol
defprotocol Blank do
   def blank?(data)
end

#Implementing the protocol for lists
defimpl Blank, for: List do
   def blank?([]), do: true
   def blank?(_), do: false
end

#Implementing the protocol for strings
defimpl Blank, for: BitString do
   def blank?(""), do: true
   def blank?(_), do: false
end

#Implementing the protocol for maps
defimpl Blank, for: Map do
   def blank?(map), do: map_size(map) == 0
end

IO.puts(Blank.blank? [])
IO.puts(Blank.blank? [:true, "Hello"])
IO.puts(Blank.blank? "")
IO.puts(Blank.blank? "Hi")

Puede implementar su Protocolo para tantos o tan pocos tipos como desee, lo que tenga sentido para el uso de su Protocolo. Este fue un caso de uso bastante básico de protocolos. Cuando se ejecuta el programa anterior, produce el siguiente resultado:

true
false
true
false

Note - Si usa esto para otros tipos distintos a los que definió el protocolo, producirá un error.

File IO es una parte integral de cualquier lenguaje de programación, ya que permite que el lenguaje interactúe con los archivos en el sistema de archivos. En este capítulo, discutiremos dos módulos: Ruta y Archivo.

El módulo de ruta

los pathmodule es un módulo muy pequeño que puede considerarse como un módulo auxiliar para las operaciones del sistema de archivos. La mayoría de las funciones del módulo Archivo esperan rutas como argumentos. Más comúnmente, esas rutas serán binarios regulares. El módulo Path proporciona facilidades para trabajar con tales rutas. Se prefiere usar funciones del módulo Path en lugar de simplemente manipular binarios, ya que el módulo Path se ocupa de diferentes sistemas operativos de forma transparente. Se debe observar que Elixir convertirá automáticamente barras (/) en barras invertidas (\) en Windows al realizar operaciones con archivos.

Consideremos el siguiente ejemplo para comprender mejor el módulo Path:

IO.puts(Path.join("foo", "bar"))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

foo/bar

Hay muchos métodos que proporciona el módulo de ruta. Puedes echar un vistazo a los diferentes métodos aquí . Estos métodos se utilizan con frecuencia si está realizando muchas operaciones de manipulación de archivos.

El módulo de archivos

El módulo de archivos contiene funciones que nos permiten abrir archivos como dispositivos IO. De forma predeterminada, los archivos se abren en modo binario, lo que requiere que los desarrolladores utilicen elIO.binread y IO.binwritefunciones desde el módulo IO. Creemos un archivo llamadonewfile y escribirle algunos datos.

{:ok, file} = File.read("newfile", [:write]) 
# Pattern matching to store returned stream
IO.binwrite(file, "This will be written to the file")

Si va a abrir el archivo en el que acabamos de escribir, el contenido se mostrará de la siguiente manera:

This will be written to the file

Ahora entendamos cómo usar el módulo de archivos.

Abrir un archivo

Para abrir un archivo, podemos usar cualquiera de las siguientes 2 funciones:

{:ok, file} = File.open("newfile")
file = File.open!("newfile")

Entendamos ahora la diferencia entre File.open función y la File.open!() función.

  • los File.openLa función siempre devuelve una tupla. Si el archivo se abre correctamente, devuelve el primer valor de la tupla como:oky el segundo valor es literal de tipo io_device. Si se produce un error, devolverá una tupla con el primer valor como:error y el segundo valor como motivo.

  • los File.open!() función, por otro lado, devolverá un io_devicesi el archivo se abre correctamente, de lo contrario, generará un error. NOTA: Este es el patrón seguido en todas las funciones del módulo de archivos que vamos a discutir.

También podemos especificar los modos en los que queremos abrir este archivo. Para abrir un archivo como solo lectura y en modo de codificación utf-8, usamos el siguiente código:

file = File.open!("newfile", [:read, :utf8])

Escribir en un archivo

Tenemos dos formas de escribir en archivos. Veamos el primero usando la función de escritura del módulo Archivo.

File.write("newfile", "Hello")

Pero esto no debe usarse si está realizando múltiples escrituras en el mismo archivo. Cada vez que se invoca esta función, se abre un descriptor de archivo y se genera un nuevo proceso para escribir en el archivo. Si está haciendo varias escrituras en un bucle, abra el archivo a través deFile.openy escribir en él usando los métodos del módulo IO. Consideremos un ejemplo para entender lo mismo:

#Open the file in read, write and utf8 modes. 
file = File.open!("newfile_2", [:read, :utf8, :write])

#Write to this "io_device" using standard IO functions
IO.puts(file, "Random text")

Puede utilizar otros métodos de módulo IO como IO.write y IO.binwrite para escribir en archivos abiertos como io_device.

Leer de un archivo

Tenemos dos formas de leer archivos. Veamos el primero usando la función de lectura del módulo Archivo.

IO.puts(File.read("newfile"))

Al ejecutar este código, debería obtener una tupla con el primer elemento como :ok y el segundo como el contenido de newfile

También podemos utilizar el File.read! función para obtener el contenido de los archivos que se nos devuelven.

Cerrar un archivo abierto

Siempre que abra un archivo usando la función File.open, una vez que haya terminado de usarlo, debe cerrarlo usando el File.close función -

File.close(file)

En Elixir, todo el código se ejecuta dentro de los procesos. Los procesos están aislados entre sí, se ejecutan simultáneamente y se comunican mediante el paso de mensajes. Los procesos de Elixir no deben confundirse con los procesos del sistema operativo. Los procesos en Elixir son extremadamente livianos en términos de memoria y CPU (a diferencia de los subprocesos en muchos otros lenguajes de programación). Debido a esto, no es raro tener decenas o incluso cientos de miles de procesos ejecutándose simultáneamente.

En este capítulo, aprenderemos sobre las construcciones básicas para generar nuevos procesos, así como para enviar y recibir mensajes entre diferentes procesos.

La función de generación

La forma más sencilla de crear un nuevo proceso es utilizar el spawnfunción. losspawnacepta una función que se ejecutará en el nuevo proceso. Por ejemplo

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

false

El valor de retorno de la función de generación es un PID. Este es un identificador único para el proceso y, por lo tanto, si ejecuta el código sobre su PID, será diferente. Como puede ver en este ejemplo, el proceso está muerto cuando verificamos si está vivo. Esto se debe a que el proceso se cerrará tan pronto como haya terminado de ejecutar la función dada.

Como ya se mencionó, todos los códigos de Elixir se ejecutan dentro de los procesos. Si ejecuta la función automática, verá el PID de su sesión actual:

pid = self
 
Process.alive?(pid)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

true

Paso de mensajes

Podemos enviar mensajes a un proceso con send y recibirlos con receive. Pasemos un mensaje al proceso actual y recibamoslo en el mismo.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hi people

Enviamos un mensaje al proceso actual usando la función de envío y lo pasamos al PID de self. Luego manejamos el mensaje entrante usando elreceive función.

Cuando se envía un mensaje a un proceso, el mensaje se almacena en el process mailbox. El bloque de recepción pasa por el buzón del proceso actual en busca de un mensaje que coincida con cualquiera de los patrones dados. El bloque de recepción admite guardias y muchas cláusulas, como case.

Si no hay ningún mensaje en el buzón que coincida con ninguno de los patrones, el proceso actual esperará hasta que llegue un mensaje coincidente. También se puede especificar un tiempo de espera. Por ejemplo,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

nothing after 1s

NOTE - Se puede dar un tiempo de espera de 0 cuando ya espera que el mensaje esté en el buzón.

Enlaces

La forma más común de desove en Elixir es en realidad a través de spawn_linkfunción. Antes de ver un ejemplo con spawn_link, comprendamos qué sucede cuando falla un proceso.

spawn fn -> raise "oops" end

Cuando se ejecuta el programa anterior, produce el siguiente error:

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

Se registró un error, pero el proceso de generación aún se está ejecutando. Esto se debe a que los procesos están aislados. Si queremos que la falla en un proceso se propague a otro, necesitamos vincularlos. Esto se puede hacer con elspawn_linkfunción. Consideremos un ejemplo para entender lo mismo:

spawn_link fn -> raise "oops" end

Cuando se ejecuta el programa anterior, produce el siguiente error:

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

Si está ejecutando esto en iexshell entonces el shell maneja este error y no sale. Pero si ejecuta primero haciendo un archivo de script y luego usandoelixir <file-name>.exs, el proceso principal también se desactivará debido a esta falla.

Los procesos y enlaces juegan un papel importante en la construcción de sistemas tolerantes a fallas. En las aplicaciones de Elixir, a menudo vinculamos nuestros procesos a supervisores que detectarán cuando un proceso muere y comenzarán un nuevo proceso en su lugar. Esto solo es posible porque los procesos están aislados y no comparten nada de forma predeterminada. Y dado que los procesos están aislados, no hay forma de que una falla en un proceso bloquee o corrompa el estado de otro. Mientras que otros lenguajes requerirán que capturemos / manejemos excepciones; en Elixir, estamos realmente bien dejando que los procesos fallen porque esperamos que los supervisores reinicien correctamente nuestros sistemas.

Estado

Si está creando una aplicación que requiere estado, por ejemplo, para mantener la configuración de su aplicación, o necesita analizar un archivo y guardarlo en la memoria, ¿dónde lo almacenaría? La funcionalidad del proceso de Elixir puede resultar útil al hacer tales cosas.

Podemos escribir procesos que se repiten infinitamente, mantener el estado y enviar y recibir mensajes. Como ejemplo, escribamos un módulo que inicie nuevos procesos que funcionen como un almacén de clave-valor en un archivo llamadokv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Tenga en cuenta que el start_link La función inicia un nuevo proceso que ejecuta el loopfunción, comenzando con un mapa vacío. losloopLa función luego espera mensajes y realiza la acción apropiada para cada mensaje. En el caso de un:getmensaje, envía un mensaje de vuelta a la persona que llama y las llamadas se repiten para esperar un nuevo mensaje. Mientras que la:put el mensaje realmente invoca loop con una nueva versión del mapa, con la clave y el valor dados almacenados.

Ejecutemos ahora lo siguiente:

iex kv.exs

Ahora deberías estar en tu iexcáscara. Para probar nuestro módulo, intente lo siguiente:

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

"Hello"

En este capítulo, vamos a explorar los sigilos, los mecanismos proporcionados por el lenguaje para trabajar con representaciones textuales. Los sigilos comienzan con el carácter tilde (~) seguido de una letra (que identifica el sigilo) y luego un delimitador; opcionalmente, se pueden agregar modificadores después del delimitador final.

Regex

Las expresiones regulares en Elixir son sigilos. Hemos visto su uso en el capítulo String. Tomemos nuevamente un ejemplo para ver cómo podemos usar expresiones regulares en Elixir.

# A regular expression that matches strings which contain "foo" or
# "bar":
regex = ~r/foo|bar/
IO.puts("foo" =~ regex)
IO.puts("baz" =~ regex)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

true
false

Los sellos admiten 8 delimitadores diferentes:

~r/hello/
~r|hello|
~r"hello"
~r'hello'
~r(hello)
~r[hello]
~r{hello}
~r<hello>

La razón detrás de admitir diferentes delimitadores es que diferentes delimitadores pueden ser más adecuados para diferentes sigilos. Por ejemplo, el uso de paréntesis para expresiones regulares puede ser una elección confusa, ya que pueden mezclarse con los paréntesis dentro de la expresión regular. Sin embargo, los paréntesis pueden ser útiles para otros sigilos, como veremos en la siguiente sección.

Elixir admite expresiones regulares compatibles con Perl y también admite modificadores. Puede leer más sobre el uso de expresiones regulares aquí .

Cadenas, listas de caracteres y listas de palabras

Aparte de las expresiones regulares, Elixir tiene 3 sellos incorporados más. Echemos un vistazo a los sigilos.

Instrumentos de cuerda

El sigilo ~ s se usa para generar cadenas, al igual que las comillas dobles. El sigilo ~ s es útil, por ejemplo, cuando una cadena contiene comillas simples y dobles:

new_string = ~s(this is a string with "double" quotes, not 'single' ones)
IO.puts(new_string)

Este sigilo genera hilos. Cuando se ejecuta el programa anterior, produce el siguiente resultado:

"this is a string with \"double\" quotes, not 'single' ones"

Listas de caracteres

El sigilo ~ c se usa para generar listas de caracteres:

new_char_list = ~c(this is a char list containing 'single quotes')
IO.puts(new_char_list)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

this is a char list containing 'single quotes'

Listas de palabras

El sigilo ~ w se usa para generar listas de palabras (las palabras son cadenas regulares). Dentro del sigilo ~ w, las palabras están separadas por espacios en blanco.

new_word_list = ~w(foo bar bat)
IO.puts(new_word_list)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

foobarbat

El sigilo ~ w también acepta la c, s y a modificadores (para listas de caracteres, cadenas y átomos, respectivamente), que especifican el tipo de datos de los elementos de la lista resultante -

new_atom_list = ~w(foo bar bat)a
IO.puts(new_atom_list)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

[:foo, :bar, :bat]

Interpolación y escape en sigilos

Además de los sigilos en minúsculas, Elixir admite sigilos en mayúsculas para lidiar con caracteres de escape e interpolación. Si bien ~ sy ~ S devolverán cadenas, el primero permite códigos de escape e interpolación, mientras que el segundo no. Consideremos un ejemplo para entender esto:

~s(String with escape codes \x26 #{"inter" <> "polation"})
# "String with escape codes & interpolation"
~S(String without escape codes \x26 without #{interpolation})
# "String without escape codes \\x26 without \#{interpolation}"

Sigilos personalizados

Podemos crear fácilmente nuestros propios sellos personalizados. En este ejemplo, crearemos un sigilo para convertir una cadena a mayúsculas.

defmodule CustomSigil do
   def sigil_u(string, []), do: String.upcase(string)
end

import CustomSigil

IO.puts(~u/tutorials point/)

Cuando ejecutamos el código anterior, produce el siguiente resultado:

TUTORIALS POINT

Primero definimos un módulo llamado CustomSigil y dentro de ese módulo, creamos una función llamada sigil_u. Como no hay un sigilo ~ u existente en el espacio de sigilo existente, lo usaremos. El _u indica que deseamos usar u como el carácter después de la tilde. La definición de la función debe tener dos argumentos, una entrada y una lista.

Las listas por comprensión son azúcar sintáctico para recorrer los enumerables en Elixir. En este capítulo usaremos comprensiones para iteración y generación.

Lo esencial

Cuando miramos el módulo Enum en el capítulo de enumerables, encontramos la función de mapa.

Enum.map(1..3, &(&1 * 2))

En este ejemplo, pasaremos una función como segundo argumento. Cada elemento del rango se pasará a la función y luego se devolverá una nueva lista que contiene los nuevos valores.

Mapear, filtrar y transformar son acciones muy comunes en Elixir, por lo que hay una forma ligeramente diferente de lograr el mismo resultado que en el ejemplo anterior:

for n <- 1..3, do: n * 2

Cuando ejecutamos el código anterior, produce el siguiente resultado:

[2, 4, 6]

El segundo ejemplo es una comprensión, y como probablemente pueda ver, es simplemente azúcar sintáctico para lo que también puede lograr si usa el Enum.mapfunción. Sin embargo, no hay beneficios reales de usar una comprensión sobre una función del módulo Enum en términos de rendimiento.

Las comprensiones no se limitan a las listas, pero se pueden utilizar con todos los enumerables.

Filtrar

Puede pensar en los filtros como una especie de guardia para las comprensiones. Cuando vuelve un valor filtradofalse o nilestá excluido de la lista final. Recorramos un rango y solo nos preocupemos por los números pares. Usaremos elis_even función del módulo Integer para comprobar si un valor es par o no.

import Integer
IO.puts(for x <- 1..10, is_even(x), do: x)

Cuando se ejecuta el código anterior, produce el siguiente resultado:

[2, 4, 6, 8, 10]

También podemos utilizar varios filtros en la misma comprensión. Agregue otro filtro que desee después delis_even filtro separado por una coma.

: en Opción

En los ejemplos anteriores, todas las comprensiones devolvieron listas como resultado. Sin embargo, el resultado de una comprensión se puede insertar en diferentes estructuras de datos pasando el:into opción a la comprensión.

Por ejemplo, un bitstring El generador se puede usar con la opción: into para eliminar fácilmente todos los espacios en una cadena -

IO.puts(for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>)

Cuando se ejecuta el código anterior, produce el siguiente resultado:

helloworld

El código anterior elimina todos los espacios de la cadena usando c != ?\s filter y luego, usando la opción: into, coloca todos los caracteres devueltos en una cadena.

Elixir es un lenguaje escrito dinámicamente, por lo que todos los tipos en Elixir son inferidos por el tiempo de ejecución. No obstante, Elixir viene con typepecs, que es una notación utilizada paradeclaring custom data types and declaring typed function signatures (specifications).

Especificaciones de función (especificaciones)

Por defecto, Elixir proporciona algunos tipos básicos, como integer o pid, y también tipos complejos: por ejemplo, el roundfunción, que redondea un flotante a su número entero más cercano, toma un número como argumento (un número entero o un flotante) y devuelve un número entero. En la documentación relacionada , la firma mecanografiada redonda se escribe como:

round(number) :: integer

La descripción anterior implica que la función de la izquierda toma como argumento lo que se especifica entre paréntesis y devuelve lo que está a la derecha de ::, es decir, Integer. Las especificaciones de la función se escriben con@specdirectiva, colocada justo antes de la definición de función. La función redonda se puede escribir como -

@spec round(number) :: integer
def round(number), do: # Function implementation
...

Typepecs también admite tipos complejos, por ejemplo, si desea devolver una lista de números enteros, puede usar [Integer]

Tipos personalizados

Si bien Elixir proporciona muchos tipos incorporados útiles, es conveniente definir tipos personalizados cuando sea apropiado. Esto se puede hacer al definir módulos a través de la directiva @type. Consideremos un ejemplo para entender lo mismo:

defmodule FunnyCalculator do
   @type number_with_joke :: {number, String.t}

   @spec add(number, number) :: number_with_joke
   def add(x, y), do: {x + y, "You need a calculator to do that?"}

   @spec multiply(number, number) :: number_with_joke
   def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end

{result, comment} = FunnyCalculator.add(10, 20)
IO.puts(result)
IO.puts(comment)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

30
You need a calculator to do that?

NOTE - Los tipos personalizados definidos a través de @type se exportan y están disponibles fuera del módulo en el que están definidos. Si desea mantener un tipo personalizado en privado, puede utilizar el @typep directiva en lugar de @type.

Los comportamientos en Elixir (y Erlang) son una forma de separar y abstraer la parte genérica de un componente (que se convierte en el módulo de comportamiento) de la parte específica (que se convierte en el módulo de devolución de llamada). Los comportamientos proporcionan una forma de:

  • Definir un conjunto de funciones que debe implementar un módulo.
  • Asegúrese de que un módulo implemente todas las funciones de ese conjunto.

Si es necesario, puede pensar en comportamientos como interfaces en lenguajes orientados a objetos como Java: un conjunto de firmas de funciones que un módulo tiene que implementar.

Definiendo un comportamiento

Consideremos un ejemplo para crear nuestro propio comportamiento y luego usemos este comportamiento genérico para crear un módulo. Definiremos un comportamiento que salude a las personas con el saludo y el adiós en diferentes idiomas.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

los @callbackLa directiva se usa para enumerar las funciones que los módulos de adopción necesitarán definir. También especifica el no. de argumentos, su tipo y sus valores de retorno.

Adoptar un comportamiento

Hemos definido con éxito un comportamiento. Ahora lo adoptaremos e implementaremos en múltiples módulos. Creemos dos módulos que implementen este comportamiento en inglés y español.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

defmodule EnglishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hello " <> name)
   def say_bye(name), do: IO.puts("Goodbye, " <> name)
end

defmodule SpanishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hola " <> name)
   def say_bye(name), do: IO.puts("Adios " <> name)
end

EnglishGreet.say_hello("Ayush")
EnglishGreet.say_bye("Ayush")
SpanishGreet.say_hello("Ayush")
SpanishGreet.say_bye("Ayush")

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Hello Ayush
Goodbye, Ayush
Hola Ayush
Adios Ayush

Como ya ha visto, adoptamos un comportamiento utilizando el @behaviourdirectiva en el módulo. Tenemos que definir todas las funciones implementadas en el comportamiento para todos los módulos secundarios . Esto puede considerarse aproximadamente equivalente a las interfaces en los lenguajes de programación orientada a objetos.

Elixir tiene tres mecanismos de error: errores, lanzamientos y salidas. Exploremos cada mecanismo en detalle.

Error

Los errores (o excepciones) se utilizan cuando suceden cosas excepcionales en el código. Se puede recuperar un error de muestra al intentar agregar un número en una cadena:

IO.puts(1 + "Hello")

Cuando se ejecuta el programa anterior, produce el siguiente error:

** (ArithmeticError) bad argument in arithmetic expression
   :erlang.+(1, "Hello")

Este fue un error incorporado de muestra.

Generando errores

Podemos raiseerrores al usar las funciones de aumento. Consideremos un ejemplo para entender lo mismo:

#Runtime Error with just a message
raise "oops"  # ** (RuntimeError) oops

Se pueden generar otros errores con raise / 2 pasando el nombre del error y una lista de argumentos de palabras clave

#Other error type with a message
raise ArgumentError, message: "invalid argument foo"

También puede definir sus propios errores y plantearlos. Considere el siguiente ejemplo:

defmodule MyError do
   defexception message: "default message"
end

raise MyError  # Raises error with default message
raise MyError, message: "custom message"  # Raises error with custom message

Rescatar errores

No queremos que nuestros programas se cierren abruptamente, sino que los errores deben manejarse con cuidado. Para ello utilizamos el manejo de errores. Nosotrosrescue errores usando el try/rescueconstruir. Consideremos el siguiente ejemplo para entender lo mismo:

err = try do
   raise "oops"
rescue
   e in RuntimeError -> e
end

IO.puts(err.message)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

oops

Hemos manejado errores en la declaración de rescate usando la coincidencia de patrones. Si no tenemos ningún uso del error, y solo queremos usarlo con fines de identificación, también podemos usar el formulario -

err = try do
   1 + "Hello"
rescue
   RuntimeError -> "You've got a runtime error!"
   ArithmeticError -> "You've got a Argument error!"
end

IO.puts(err)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

You've got a Argument error!

NOTE- La mayoría de las funciones de la biblioteca estándar de Elixir se implementan dos veces, una devuelve tuplas y la otra genera errores. Por ejemplo, elFile.read y el File.read!funciones. El primero devolvió una tupla si el archivo se leyó correctamente y si se encontró un error, esta tupla se usó para dar la razón del error. El segundo generó un error si se encontró un error.

Si usamos el enfoque de la primera función, entonces necesitamos usar el caso para el patrón que coincide con el error y tomar medidas de acuerdo con eso. En el segundo caso, utilizamos el enfoque de prueba de rescate para el código propenso a errores y manejamos los errores en consecuencia.

Lanza

En Elixir, se puede lanzar un valor y luego ser capturado. Lanzar y atrapar están reservados para situaciones en las que no es posible recuperar un valor a menos que se utilice lanzar y atrapar.

Las instancias son bastante poco comunes en la práctica, excepto cuando se interactúan con bibliotecas. Por ejemplo, supongamos ahora que el módulo Enum no proporcionó ninguna API para encontrar un valor y que necesitábamos encontrar el primer múltiplo de 13 en una lista de números:

val = try do
   Enum.each 20..100, fn(x) ->
      if rem(x, 13) == 0, do: throw(x)
   end
   "Got nothing"
catch
   x -> "Got #{x}"
end

IO.puts(val)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

Got 26

Salida

Cuando un proceso muere por "causas naturales" (por ejemplo, excepciones no controladas), envía una señal de salida. Un proceso también puede morir al enviar explícitamente una señal de salida. Consideremos el siguiente ejemplo:

spawn_link fn -> exit(1) end

En el ejemplo anterior, el proceso vinculado murió al enviar una señal de salida con un valor de 1. Tenga en cuenta que la salida también se puede "capturar" usando try / catch. Por ejemplo

val = try do
   exit "I am exiting"
catch
   :exit, _ -> "not really"
end

IO.puts(val)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

not really

Después

A veces es necesario asegurarse de que un recurso se limpia después de alguna acción que potencialmente puede generar un error. La construcción try / after le permite hacer eso. Por ejemplo, podemos abrir un archivo y usar una cláusula after para cerrarlo, incluso si algo sale mal.

{:ok, file} = File.open "sample", [:utf8, :write]
try do
   IO.write file, "olá"
   raise "oops, something went wrong"
after
   File.close(file)
end

Cuando ejecutemos este programa, nos dará un error. Pero elafter La declaración asegurará que el descriptor de archivo se cierre ante tal evento.

Las macros son una de las funciones más avanzadas y poderosas de Elixir. Como ocurre con todas las funciones avanzadas de cualquier idioma, las macros deben usarse con moderación. Permiten realizar potentes transformaciones de código en tiempo de compilación. Ahora entenderemos brevemente qué son las macros y cómo usarlas.

Citar

Antes de empezar a hablar de macros, veamos primero los componentes internos de Elixir. Un programa Elixir puede representarse mediante sus propias estructuras de datos. El componente básico de un programa Elixir es una tupla con tres elementos. Por ejemplo, la llamada de función sum (1, 2, 3) se representa internamente como -

{:sum, [], [1, 2, 3]}

El primer elemento es el nombre de la función, el segundo es una lista de palabras clave que contiene metadatos y el tercero es la lista de argumentos. Puede obtener esto como salida en el shell iex si escribe lo siguiente:

quote do: sum(1, 2, 3)

Los operadores también se representan como tuplas. Las variables también se representan utilizando estos tripletes, excepto que el último elemento es un átomo, en lugar de una lista. Al citar expresiones más complejas, podemos ver que el código está representado en tales tuplas, que a menudo están anidadas unas dentro de otras en una estructura que se asemeja a un árbol. Muchos lenguajes llamarían a tales representaciones unAbstract Syntax Tree (AST). Elixir llama a estas expresiones citadas.

Quote

Ahora que podemos recuperar la estructura interna de nuestro código, ¿cómo lo modificamos? Para inyectar nuevo código o valores, usamosunquote. Cuando quitamos las comillas de una expresión, se evaluará y se inyectará en el AST. Consideremos un ejemplo (en iex shell) para entender el concepto:

num = 25

quote do: sum(15, num)

quote do: sum(15, unquote(num))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

{:sum, [], [15, {:num, [], Elixir}]}
{:sum, [], [15, 25]}

En el ejemplo de la expresión de comillas, no reemplazó automáticamente num con 25. Necesitamos quitar las comillas de esta variable si queremos modificar el AST.

Macros

Entonces, ahora que estamos familiarizados con las comillas y las sin comillas, podemos explorar la metaprogramación en Elixir usando macros.

En los términos más simples, las macros son funciones especiales diseñadas para devolver una expresión entre comillas que se insertará en el código de nuestra aplicación. Imagine que la macro se reemplaza con la expresión entre comillas en lugar de llamarla como una función. Con las macros tenemos todo lo necesario para extender Elixir y agregar código dinámicamente a nuestras aplicaciones

Implementemos a menos que sea como una macro. Comenzaremos definiendo la macro usando eldefmacromacro. Recuerde que nuestra macro debe devolver una expresión entre comillas.

defmodule OurMacro do
   defmacro unless(expr, do: block) do
      quote do
         if !unquote(expr), do: unquote(block)
      end
   end
end

require OurMacro

OurMacro.unless true, do: IO.puts "True Expression"

OurMacro.unless false, do: IO.puts "False expression"

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

False expression

Lo que está sucediendo aquí está nuestro código está siendo reemplazado por el código devuelto por el citado menos macro. Hemos quitado las comillas de la expresión para evaluarla en el contexto actual y también hemos quitado las comillas del bloque do para ejecutarlo en su contexto. Este ejemplo nos muestra la metaprogramación usando macros en elixir.

Las macros se pueden usar en tareas mucho más complejas, pero deben usarse con moderación. Esto se debe a que la metaprogramación en general se considera una mala práctica y debe usarse solo cuando sea necesario.

Elixir proporciona una excelente interoperabilidad con las bibliotecas de Erlang. Analicemos brevemente algunas bibliotecas.

El módulo binario

El módulo Elixir String integrado maneja binarios codificados en UTF-8. El módulo binario es útil cuando se trata de datos binarios que no están necesariamente codificados en UTF-8. Consideremos un ejemplo para comprender mejor el módulo binario:

# UTF-8
IO.puts(String.to_char_list("Ø"))

# binary
IO.puts(:binary.bin_to_list "Ø")

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

[216]
[195, 152]

El ejemplo anterior muestra la diferencia; el módulo String devuelve puntos de código UTF-8, mientras que: binary trata con bytes de datos sin procesar.

El módulo criptográfico

El módulo de cifrado contiene funciones de hash, firmas digitales, cifrado y más. Este módulo no forma parte de la biblioteca estándar de Erlang, pero se incluye con la distribución de Erlang. Esto significa que debe incluir: crypto en la lista de aplicaciones de su proyecto cada vez que lo use. Veamos un ejemplo usando el módulo criptográfico:

IO.puts(Base.encode16(:crypto.hash(:sha256, "Elixir")))

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB

El módulo Digraph

El módulo de dígrafos contiene funciones para tratar con gráficos dirigidos construidos con vértices y aristas. Después de construir el gráfico, los algoritmos ayudarán a encontrar, por ejemplo, el camino más corto entre dos vértices o bucles en el gráfico. Tenga en cuenta que las funcionesin :digraph alterar la estructura del gráfico indirectamente como un efecto secundario, mientras se devuelven los vértices o bordes añadidos.

digraph = :digraph.new()
coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}]
[v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c))
:digraph.add_edge(digraph, v0, v1)
:digraph.add_edge(digraph, v1, v2)
for point <- :digraph.get_short_path(digraph, v0, v2) do 
   {x, y} = point
   IO.puts("#{x}, #{y}")
end

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

0.0, 0.0
1.0, 0.0
1.0, 1.0

El módulo de matemáticas

El módulo de matemáticas contiene operaciones matemáticas comunes que abarcan trigonometría, funciones exponenciales y logarítmicas. Consideremos el siguiente ejemplo para comprender cómo funciona el módulo de matemáticas:

# Value of pi
IO.puts(:math.pi())

# Logarithm
IO.puts(:math.log(7.694785265142018e23))

# Exponentiation
IO.puts(:math.exp(55.0))

#...

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

3.141592653589793
55.0
7.694785265142018e23

El módulo de cola

La cola es una estructura de datos que implementa colas FIFO (primero en entrar, primero en salir) (de dos extremos) de manera eficiente. El siguiente ejemplo muestra cómo funciona un módulo de cola:

q = :queue.new
q = :queue.in("A", q)
q = :queue.in("B", q)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)

Cuando se ejecuta el programa anterior, produce el siguiente resultado:

A
B