Taller de Elixir #10 – Enumerables y Streams

Enumerables

Elixir proporciona el concepto de enumerables y el módulo Enum para trabajar con ellos. Ya hemos aprendido dos enumerables: listas y mapas.

iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end)
[2, 12]

El módulo Enum proporciona una amplia gama de funciones para transformar, ordenar, agrupar, filtrar y recuperar elementos de enumerables. Es uno de los módulos que los desarrolladores usan con frecuencia en su código Elixir.

Elixir también proporciona rangos:

iex> Enum.map(1..3, fn x -> x * 2 end)
[2, 4, 6]
iex> Enum.reduce(1..3, 0, &+/2)
6

Las funciones en el módulo Enum se limitan a, como su nombre lo indica, enumerar valores en estructuras de datos. Para operaciones específicas, como insertar y actualizar elementos particulares, es posible que deba buscar módulos específicos para el tipo de datos. Por ejemplo, si desea insertar un elemento en una posición dada en una lista, debe usar la función List.insert_at/3 del módulo List, ya que tendría poco sentido insertar un valor en, por ejemplo, un rango.

Decimos que las funciones en el módulo Enum son polimórficas porque pueden trabajar con diversos tipos de datos. En particular, las funciones en el módulo Enum pueden funcionar con cualquier tipo de datos que implemente el protocolo Enumerable.

El operador pipe

En el ejemplo siguiente tiene una pipeline 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.

iex> total_sum = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum
7500000000

El símbolo |> utilizado en el fragmento de arriba es el operador de tubería: toma la salida de la expresión en su lado izquierdo y la pasa como el primer argumento para la llamada a la función en su lado derecho. Es similar al Unix | operador. Su propósito es resaltar los datos que están siendo transformados por una serie de funciones. Para ver cómo puede hacer que el código sea más limpio, eche un vistazo al ejemplo anterior reescrito sin usar el operador |>:

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

Puedes encontrar más sobre el operador de tubería en su documentación.

Streams

Como alternativa a Enum, Elixir proporciona el módulo Stream que admite operaciones diferidas:

iex> 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
7500000000

Los flujos son enumerables perezosos y componibles.

En el ejemplo anterior, 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:

iex> 1..100_000 |> Stream.map(&(&1 * 3))
<[enum: 1..100000, funs: [#Function<34.16982430/1 in Stream.map/2>]]>

Además, son componibles porque podemos canalizar muchas operaciones de flujo:

1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?)
<[enum: 1..100000, funs: [...]]>

En lugar de generar listas intermedias, las secuencias crean una serie de cálculos que se invocan solo cuando pasamos la secuencia subyacente al módulo Enum. Las secuencias son útiles cuando se trabaja con colecciones grandes, posiblemente infinitas.

iex> stream = Stream.cycle([1, 2, 3])
<15.16982430/2 in Stream.unfold/2>
iex> Enum.take(stream, 10)
[1, 2, 3, 1, 2, 3, 1, 2, 3, 1]

Por otro lado, Stream.unfold/2 puede usarse para generar valores a partir de un valor inicial dado:

iex> stream = Stream.unfold("hełło", &String.next_codepoint/1)
<39.75994740/2 in Stream.unfold/2>
iex> Enum.take(stream, 3)
["h", "e", "ł"]

Otra función interesante es Stream.resource/3, que se puede utilizar para envolver los recursos, garantizando que se abran justo antes de la enumeración y se cierren después, incluso en caso de fallas. Por ejemplo, File.stream!/1 se basa en Stream.resource/3 para transmitir archivos:

iex> stream = File.stream!("path/to/file")
%File.Stream{
  line_or_bytes: :line,
  modes: [:raw, :read_ahead, :binary],
  path: "path/to/file",
  raw: true
}
iex> Enum.take(stream, 10)

El ejemplo anterior buscará las primeras 10 líneas del archivo que ha seleccionado. Esto significa que las transmisiones pueden ser muy útiles para manejar archivos grandes o incluso recursos lentos como los recursos de red.

La cantidad de funcionalidad en los módulos Enum y Stream puede ser desalentadora al principio, pero se familiarizará con ellos caso por caso. En particular, enfóquese primero en el módulo Enum y solo muévase a Stream para los escenarios particulares donde se requiere pereza, ya sea para manejar recursos lentos o grandes colecciones, posiblemente infinitas.

Taller de Elixir #8 – Módulos y funciones

En Elixir agrupamos varias funciones en módulos. Ya hemos usado muchos módulos diferentes en los capítulos anteriores, como el módulo de cadena:

iex> String.length("hello")
5

Para crear nuestros propios módulos en Elixir, utilizamos la macro defmodule. Usamos la macro def para definir funciones en ese módulo:

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

iex> Math.sum(1, 2)
3

Compilación

Ya es hora de que aprendamos cómo compilar el código de Elixir y también cómo ejecutar los scripts de Elixir.

La mayoría de las veces es conveniente escribir módulos en archivos para que puedan compilarse y reutilizarse. Supongamos que tenemos un archivo llamado math.ex con los siguientes contenidos:

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

Este archivo se puede compilar usando elixirc:

$ elixirc math.ex

Esto generará un archivo llamado Elixir.Math.beam que contiene el código de bytes para el módulo definido. Si iniciamos iex nuevamente, la definición de nuestro 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):

iex> Math.sum(1, 2)
3

Los proyectos de elixir generalmente se organizan en tres directorios:

  • ebin – contiene el bytecode compilado
  • lib: contiene código de elixir (generalmente archivos .ex)
  • prueba: contiene pruebas (generalmente archivos .exs)

Cuando trabaje en proyectos reales, la herramienta de compilación llamada mix será responsable de compilar y configurar las rutas adecuadas para usted. Con fines de aprendizaje, Elixir también es compatible con un modo de script que es más flexible y no genera ningún artefacto compilado.

Modo Scripted

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

Por ejemplo, podemos crear un archivo llamado math.exs:

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

IO.puts Math.sum(1, 2)

Y ejecutarlo como:

$ elixir math.exs

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.

Funciones nombradas

Dentro de un módulo, podemos definir funciones con def/2 y funciones privadas con defp/2. Una función definida con def/2 puede invocarse desde otros módulos, mientras que una función privada solo puede invocarse localmente.

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

  defp do_sum(a, b) do
    a + b
  end
end

IO.puts Math.sum(1, 2)    #=> 3
IO.puts Math.do_sum(1, 2) #=> ** (UndefinedFunctionError)

Las declaraciones de funciones también admiten guards y múltiples cláusulas. Si una función tiene varias cláusulas, Elixir probará cada cláusula hasta que encuentre una que coincida. Aquí tenemos una implementación de una función que verifica si el número dado es cero o no:

defmodule Math do
  def zero?(0) do
    true
  end

  def zero?(x) when is_integer(x) do
    false
  end
endSiiiii......imágenes cuanto menos, impactantes

IO.puts Math.zero?(0)         #=> true
IO.puts Math.zero?(1)         #=> false
IO.puts Math.zero?([1, 2, 3]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0)       #=> ** (FunctionClauseError)

¿El signo de interrogación final en cero? significa que esta función devuelve un valor booleano. Ver convenciones de nomenclatura.

Dar un argumento que no coincide con ninguna de las cláusulas genera un error.

Similar a construcciones como if, las funciones con nombre admiten la sintaxis de bloque do: y do/end, como aprendimos, do/end es una sintaxis conveniente para el formato de lista de palabras clave. Por ejemplo, podemos editar math.exs para que se vea así:

defmodule Math do
  def zero?(0), do: true
  def zero?(x) when is_integer(x), do: false
end

Y proporcionará el mismo comportamiento. Puede usar do: para líneas simples, pero siempre use do/end para funciones que abarcan varias líneas.

Captura de funciones

Hemos estado usando el nombre/aridad de notación para referirnos a funciones. Sucede que esta notación se puede utilizar para recuperar una función con nombre como un tipo de función. Inicie iex, ejecutando el archivo math.exs definido anteriormente:

$ iex math.exs
iex> Math.zero?(0)
true
iex> fun = &Math.zero?/1
&Math.zero?/1
iex> is_function(fun)
true
iex> fun.(0)
true

Recuerde que Elixir hace una distinción entre funciones anónimas y funciones con nombre, donde las primeras deben invocarse con un punto (.) Entre el nombre de la variable y los paréntesis. El operador de captura cierra esta brecha al permitir que las funciones con nombre se asignen a variables y se pasen como argumentos de la misma manera que asignamos, invocamos y pasamos funciones anónimas.

Las funciones locales o importadas, como is_function/1, se pueden capturar sin el módulo:

iex> &is_function/1
&:erlang.is_function/1
iex> (&is_function/1).(fun)
true

hay que tener en cuenta que la sintaxis de captura también se puede utilizar como acceso directo para crear funciones:

iex> fun = &(&1 + 1)
<6.71889879/1 in :erl_eval.expr/5>
iex> fun.(1)
2

iex> fun2 = &"Good #{&1}"
<6.127694169/1 in :erl_eval.expr/5>
iex)> fun2.("morning")
"Good morning"

El &1 representa el primer argumento pasado a la función. &(&1 + 1) arriba es exactamente lo mismo que fn x -> x + 1 final. La sintaxis anterior es útil para definiciones breves de funciones.

Si desea capturar una función desde un módulo, puede hacer &Module.function():

iex> fun = &List.flatten(&1, &2)
&List.flatten/2
iex> fun.([1, [[2], 3]], [4, 5])
[1, 2, 3, 4, 5]

&List.flatten(&1, &2) es lo mismo que escribir fn(list, tail) -> List.flatten (list, tail) final, que en este caso es equivalente a &List.flatten/2. Puede leer más sobre el operador de captura y en la documentación Kernel.SpecialForms.

Argumentos por defecto

Las funciones con nombre en Elixir también admiten argumentos predeterminados:

defmodule Concat do
  def join(a, b, sep \\ " ") do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world

Se permite que cualquier expresión sirva como valor predeterminado, pero no se evaluará durante la definición de la función. Cada vez que se invoca la función y se debe utilizar cualquiera de sus valores predeterminados, se evaluará la expresión para ese valor predeterminado:

defmodule DefaultTest do
  def dowork(x \\ "hello") do
    x
  end
end
iex> DefaultTest.dowork
"hello"
iex> DefaultTest.dowork 123
123
iex> DefaultTest.dowork
"hello"

Si una función con valores predeterminados tiene varias cláusulas, es necesario crear un encabezado de función (sin un cuerpo real) para declarar valores predeterminados:

defmodule Concat do
  def join(a, b \\ nil, sep \\ " ")

  def join(a, b, _sep) when is_nil(b) do
    a
  end

  def join(a, b, sep) do
    a <> sep <> b
  end
end

IO.puts Concat.join("Hello", "world")      #=> Hello world
IO.puts Concat.join("Hello", "world", "_") #=> Hello_world
IO.puts Concat.join("Hello")               #=> Hello

El guión bajo inicial en _sep significa que la variable se ignorará en esta función. Ver convenciones de nomenclatura.

Cuando se usan valores predeterminados, se debe tener cuidado para evitar la superposición de definiciones de funciones. Considere el siguiente ejemplo:

defmodule Concat do
  def join(a, b) do
    IO.puts "***First join"
    a <> b
  end

  def join(a, b, sep \\ " ") do
    IO.puts "***Second join"
    a <> sep <> b
  end
end

Si guardamos el código anterior en un archivo llamado “concat.ex” y lo compilamos, Elixir emitirá la siguiente advertencia:

warning: this clause cannot match because a previous clause at line 2 always matches

El compilador nos dice que invocar la función de combinación con dos argumentos siempre elegirá la primera definición de combinación, mientras que la segunda solo se invocará cuando se pasen tres argumentos:

$ iex concat.ex
iex> Concat.join "Hello", "world"
***First join
"Helloworld"
iex> Concat.join "Hello", "world", "_"
***Second join
"Hello_world"

Taller de Elixir #6 – Case, cond e if

Las estructuras de flujo de control de flujo son case, cond e if.

Case

Case nos permite comparar un valor con muchos patrones hasta que encontremos uno coincidente:

iex> case {1, 2, 3} do
...>   {4, 5, 6} ->
...>     "This clause won't match"
...>   {1, x, 3} ->
...>     "This clause will match and bind x to 2 in this clause"
...>   _ ->
...>     "This clause would match any value"
...> end
"This clause will match and bind x to 2 in this clause"

Si desea comparar patrones con una variable existente, debe usar el operador ^:

iex> x = 1
1
iex> case 10 do
...>   ^x -> "Won't match"
...>   _ -> "Will match"
...> end
"Will match"

Las cláusulas también permiten que se especifiquen condiciones adicionales a través de guards:

iex> case {1, 2, 3} do
...>   {1, x, 3} when x > 0 ->
...>     "Will match"
...>   _ ->
...>     "Would match, if guard condition were not satisfied"
...> end
"Will match"

La primera cláusula anterior solo coincidirá cuando x sea positivo.

Tenga en cuenta que los errores en los guardias no tienen fugas, sino que simplemente hacen que el guardia falle:

iex> hd(1)
** (ArgumentError) argument error
iex> case 1 do
...>   x when hd(x) -> "Won't match"
...>   x -> "Got #{x}"
...> end
"Got 1"

Si ninguna de las cláusulas coincide, se genera un error:

iex> case :ok do
...>   :error -> "Won't match"
...> end
** (CaseClauseError) no case clause matching: :ok

Consulte la documentación completa de los guardias para obtener más información sobre los guardias, cómo se usan y qué expresiones están permitidas en ellos.

Tenga en cuenta que las funciones anónimas también pueden tener múltiples cláusulas y guardias:

iex> f = fn
...>   x, y when x > 0 -> x + y
...>   x, y -> x * y
...> end
<12.71889879/2 in :erl_eval.expr/5>
iex> f.(1, 3)
4
iex> f.(-1, 3)
-3

El número de argumentos en cada cláusula de función anónima debe ser el mismo, de lo contrario se genera un error.

iex> f2 = fn
...>   x, y when x > 0 -> x + y
...>   x, y, z -> x * y + z
...> end
** (CompileError) iex:1: cannot mix clauses with different arities in anonymous functions

Cond

Case es útil cuando necesita hacer coincidir valores diferentes. Sin embargo, en muchas circunstancias, queremos verificar diferentes condiciones y encontrar la primera que no se evalúa como nula o falsa. En tales casos, uno puede usar cond:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This will not be true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   1 + 1 == 2 ->
...>     "But this will"
...> end
"But this will"

Esto es equivalente a las cláusulas else if en muchos idiomas imperativos (aunque aquí se usan con menos frecuencia).

Si todas las condiciones devuelven nulo o falso, se genera un error (CondClauseError). Por esta razón, puede ser necesario agregar una condición final, igual a verdadera, que siempre coincidirá con:

iex> cond do
...>   2 + 2 == 5 ->
...>     "This is never true"
...>   2 * 2 == 3 ->
...>     "Nor this"
...>   true ->
...>     "This is always true (equivalent to else)"
...> end
"This is always true (equivalent to else)"

Finalmente, note cond considera que cualquier valor además de nil y false es verdadero:

iex> cond do
...>   hd([1, 2, 3]) ->
...>     "1 is considered as true"
...> end
"1 is considered as true"

if y unless

Además de case y cond, también tenemos las macros if/2 y unless/2, que son útiles cuando necesita verificar una sola condición:

iex> if true do
...>   "This works!"
...> end
"This works!"
iex> unless true do
...>   "This will never be seen"
...> end
nil

Si la condición if/2 devuelve false o nil, el cuerpo dado entre do/end no se ejecuta y en su lugar devuelve nil. Lo contrario sucede con unless/2.

También admiten bloques más:

iex> if nil do
...>   "This won't be seen"
...> else
...>   "This will"
...> end
"This will"

Bloques do/end

Como hemos visto en las cuatro estructuras de control: case, cond, if y unless. En todas estaban envueltas en bloques do/end. Sucede que también podríamos escribir si es así:

iex> if true, do: 1 + 2
3

Los bloques do/end son una conveniencia sintáctica construida sobre las palabras clave. Es por eso que los bloques do/end no requieren una coma entre el argumento anterior y el bloque. Son útiles exactamente porque eliminan la verbosidad al escribir bloques de código. Estos son equivalentes:

iex> if true do
...>   a = 1 + 2
...>   a + 10
...> end
13
iex> if true, do: (
...>   a = 1 + 2
...>   a + 10
...> )
13

Las listas de palabras clave juegan un papel importante en el lenguaje y son bastante comunes en muchas funciones y macros.

Taller de Elixir #3 – Tipos de datos

Los tipos básicos en elixir son enteros, flotantes, booleanos, átomos, cadenas, listas y tuplas.

iex> 1          # integer
iex> 0x1F       # integer
iex> 1.0        # float
iex> true       # boolean
iex> :atom      # atom / symbol
iex> "elixir"   # string
iex> [1, 2, 3]  # list
iex> {1, 2, 3}  # tuple

Aritmética básica

Las operaciones básicas son comunes:

iex> 1 + 2
3
iex> 2 - 1
1
iex> 5 * 5
25
iex> 10 / 2
5.0

Para que al dividir devuelva un entero en vez de un flotante se usa la función div/1 y para el modulo usamos rem/1.

iex> div(10, 2)
5
iex> div 10, 2
5
iex> rem 10, 3
1

Elixir le permite quitar los paréntesis al invocar funciones con nombre. Esta característica proporciona una sintaxis más limpia.

También admite anotaciones de acceso directo para ingresar números binarios, octales y hexadecimales:

iex> 0b1010
10
iex> 0o777
511
iex> 0x1F
31

Los números flotantes requieren un punto seguido de al menos un dígito y también admiten e para notación científica:

iex> 1.0
1.0
iex> 1.0e-10
1.0e-10

Las flotantes son de doble precisión de 64 bits.

La función “round” devuelve el número entero más cercano a un flotante y la función “trunc” para obtener la parte entera de un flotante.

iex> round(3.58)
4
iex> trunc(3.58)
3

Booleanos

Tenemos las palas claves true y false:

iex> true
true
iex> true == false
false

También tenemos la función “is_boolean”:

iex> is_boolean(true)
true
iex> is_boolean(1)
false

Atoms

Un átomo es una constante cuyo valor es su propio nombre. Algunos lenguajes llaman a estas símbolos. A menudo son útiles para enumerar valores distintos, como:

iex> :apple
:apple
iex> :orange
:orange
iex> :watermelon
:watermelon

Los átomos son iguales si sus nombres son iguales:

iex> :apple == :apple
true
iex> :apple == :orange
false

A menudo se utilizan para expresar el estado de una operación, mediante el uso de valores como :ok y :error.

Los booleanos verdadero y falso también son átomos:

iex> true == :true
true
iex> is_atom(false)
true
iex> is_boolean(:false)
true

Elixir te permite omitir el líder: para los átomos falso, verdadero y nulo.

Finalmente, Elixir tiene una construcción llamada alias que exploraremos más adelante. Los alias comienzan en mayúsculas y también son átomos:

iex> is_atom(Hello)
true

Strings

Las cadenas están delimitadas por comillas dobles, y están codificadas en UTF-8:

iex> "hellö #{:world}"
"hellö world"

Las cadenas pueden tener saltos de línea en ellas. Puedes presentarlos usando secuencias de escape:

ex> "hello
...> world"
"hello\nworld"
iex> "hello\nworld"
"hello\nworld"

Se puede imprimir una cadena usando la función IO.puts/1 desde el módulo IO:

iex> IO.puts "hello\nworld"
hello
world
:ok

Podemos ver que la función IO.puts/1 devuelve el átomo :ok después de imprimir.

Las cadenas están representadas internamente por secuencias contiguas de bytes conocidas como binarios:

iex> is_binary("hellö")
true

Para saber el número de bytes en una cadena:

iex> byte_size("hellö")
6

Observamos que el número de bytes en esa cadena es 6, a pesar de que tiene 5 caracteres. Esto se debe a que el carácter “ö” toma 2 bytes para ser representado en UTF-8. Podemos obtener la longitud real de la cadena, en función del número de caracteres, utilizando la función String.length/1:

iex> String.length("hellö")
5

El String module  contiene un montón de funciones para operar:

iex> String.upcase("hellö")
"HELLÖ"

Funciones anónimas

Estas funciones nos permiten almacenar y pasar código ejecutable como si fuera un entero o una cadena. Están delimitados por las palabras clave fn y end:

iex> add = fn a, b -> a + b end
<12.71889879/2 in :erl_eval.expr/5>
iex> add.(1, 2)
3
iex> is_function(add)
true

Las funciones anónimas también se identifican por la cantidad de argumentos que reciben. Podemos verificar si una función usando is_function / 2:

iex> is_function(add, 2)
true

iex> is_function(add, 1)
false

Definamos una nueva función anónima que usa la función agregar anónimo que hemos definido previamente:

iex> double = fn a -> add.(a, a) end
<6.71889879/1 in :erl_eval.expr/5>
iex> double.(2)
4

Una variable asignada dentro de una función no afecta a su entorno:

iex> x = 42
42
iex> (fn -> x = 0 end).()
0
iex> x
42

Listas

Se usan corchetes para especificar una lista de valores. Los valores pueden ser de cualquier tipo:

iex> [1, 2, true, 3]
[1, 2, true, 3]
iex> length [1, 2, 3]
3

Se pueden concatenar o restar dos listas utilizando los operadores ++ / 2 y – / 2 respectivamente:

iex> [1, 2, 3] ++ [4, 5, 6]
[1, 2, 3, 4, 5, 6]
iex> [1, true, 2, false, 3, true] -- [true, false]
[1, 2, 3, true]

Los operadores de listas nunca modifican la lista existente. Concatenar o eliminar elementos de una lista devuelve una nueva lista. Decimos que las estructuras de datos de Elixir son inmutables. Una ventaja de la inmutabilidad es que conduce a un código más claro. Puede pasar libremente los datos con la garantía de que nadie los mutará en la memoria, solo los transformará.

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

iex> list = [1, 2, 3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

Obtener la cabeza o la cola de una lista vacía arroja un error:

iex> hd []
** (ArgumentError) argument error

A veces creará una lista y devolverá un valor entre comillas simples. Por ejemplo:

iex> [11, 12, 13]
'\v\f\r'
iex> [104, 101, 108, 108, 111]
'hello'

Cuando Elixir ve una lista de números ASCII imprimibles, Elixir lo imprimirá como una lista de caracteres (literalmente, una lista de caracteres). Las listas de caracteres son bastante comunes al interactuar con el código Erlang existente. Siempre que vea un valor en IEx y no esté seguro de cuál es, puede usar el i/1 para recuperar información al respecto:

iex> i 'hello'
Term
  'hello'
Data type
  List
Description
  ...
Raw representation
  [104, 101, 108, 108, 111]
Reference modules
  List
Implemented protocols
  ...

Las representaciones con comillas simples y dobles no son equivalentes en Elixir, ya que están representadas por diferentes tipos:

iex> 'hello' == "hello"
false

Las comillas simples son charlists, las comillas dobles son cadenas.

Tuplas

Elixir usa llaves para definir tuplas. Al igual que las listas, las tuplas pueden contener cualquier valor:

iex> {:ok, "hello"}
{:ok, "hello"}
iex> tuple_size {:ok, "hello"}
2

Las tuplas almacenan elementos contiguos en la memoria. Esto significa que acceder a un elemento de tupla por índice u obtener el tamaño de la tupla es una operación rápida. Los índices comienzan desde cero:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"
iex> tuple_size(tuple)
2

También es posible poner un elemento en un índice particular en una tupla con put_elem/3:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> put_elem(tuple, 1, "world")
{:ok, "world"}
iex> tuple
{:ok, "hello"}

Observamos que put_elem/3 devolve una nueva tupla. La tupla original almacenada en la variable tupla no se modificó. Al igual que las listas, las tuplas también son inmutables. Cada operación en una tupla devuelve una nueva tupla, nunca cambia la dada.

¿Listas o tuplas?

Las listas se almacenan en la memoria como listas vinculadas, lo que significa que cada elemento de una lista mantiene su valor y apunta al siguiente elemento hasta llegar al final de la lista. Esto significa que acceder a la longitud de una lista es una operación lineal: debemos recorrer toda la lista para determinar su tamaño.

Del mismo modo, el rendimiento de la concatenación de listas depende de la longitud de la lista de la izquierda:

iex> list = [1, 2, 3]

iex> [0] ++ list
[0, 1, 2, 3]

iex> list ++ [4]
[1, 2, 3, 4]

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

iex> tuple = {:a, :b, :c, :d}
iex> put_elem(tuple, 2, :e)
{:a, :b, :e, :d}

Hay que tener en cuenta que esto solo se aplica a la tupla en sí, no a su contenido. Por ejemplo, cuando actualiza una tupla, todas las entradas se comparten entre la tupla antigua y la nueva, excepto la entrada que ha sido reemplazada. En otras palabras, las tuplas y las listas en Elixir son capaces de compartir sus contenidos. Esto reduce la cantidad de asignación de memoria que necesita realizar el idioma y solo es posible gracias a la semántica inmutable del idioma.

Esas características de rendimiento dictan el uso de esas estructuras de datos. Un caso de uso muy común para las tuplas es usarlas para devolver información adicional de una función. Por ejemplo, File.read/1 es una función que se puede usar para leer el contenido del archivo. Devuelve una tupla:

iex> File.read("path/to/existing/file")
{:ok, "... contents ..."}
iex> File.read("path/to/unknown/file")
{:error, :enoent}

Si la ruta dada a File.read/1 existe, devuelve una tupla con el átomo :ok como el primer elemento y el contenido del archivo como el segundo. De lo contrario, devuelve una tupla con :error y la descripción del error.

La mayoría de las veces, Elixir te guiará para hacer lo correcto. Por ejemplo, hay una función elem/2 para acceder a un elemento de tupla, pero no hay un equivalente incorporado para las listas:

iex> tuple = {:ok, "hello"}
{:ok, "hello"}
iex> elem(tuple, 1)
"hello"

Hasta ahora hemos utilizado 4 funciones de conteo: byte_size/1 (para el número de bytes en una cadena), tuple_size/1 (para el tamaño de la tupla), length/1 (para la longitud de la lista) y String.length/1 ( para el número de grafemas en una cadena). Usamos byte_size para obtener el número de bytes en una cadena, una operación barata. Recuperar el número de caracteres Unicode, por otro lado, usa String.length, y puede ser costoso ya que depende de un recorrido de toda la cadena.

Elixir también proporciona puertos, referencias y PID como tipos de datos (generalmente utilizados en la comunicación de procesos).