section | layout | title |
---|---|---|
getting-started |
getting-started |
Binarios, `strings` y listas de caracteres |
En "Tipos de datos básicos" aprendimos un poco sobre strings
y usamos la
función is_binary/1
para hacer verificaciones:
iex> string = "hello"
"hello"
iex> is_binary(string)
true
En este capítulo vamos a ganar claridad respecto a qué son exactamente los
binarios, cómo se relacionan con los strings
y que significan los valores
entre comillas simples como 'este'
en Elixir. Aunque los strings
con uno de
los tipos de datos más comunes en lenguajes de programación, son sutilmente
complejos y es bastante común que no se comprenda esta complejidad. Para las
sutilezas de los strings
en Elixir tenemos que aprender sobre
Unicode y codificación de caracteres,
en particular la codificación UTF-8.
Para facilitar la comunicación entre computadoras a través de distintos lenguajes humanos, se requiere un estándar que permita que los cereos y unos en una máquina configurada para usar francés, por ejemplo, signifiquen lo mismo cuando son transmitidos a otra que está configurada en un lenguaje distinto, por ejemplo alemán. El Estándar Unicode actúa como un registro oficial de prácticamente todos los caracteres que conocemos. Esto incluye caracteres pertenecientes a textos clásicos e históricos, emojis y también caracteres de formateo y control.
El estándar Unicode organiza todo el repertorio de caracteres en mapas de código, donde a cada caracter se le asigna un índice numérico. Este índice se conoce como Punto de Código.
En Elixir puedes usar ?
frente a un caracter para revelar su punto de código:
iex> ?a
97
iex> ?ł
322
Ten presente que la mayoría de mapeos Unicode se referirán al punto de código
por su valor hexadecimal, por ejemplo, 97
es 0061
en haxadecimal. Podemos
representar caracteres Unicode en Elixir mediante un string con el formato
\uXXXX
, donde las X
se deben reemplazar por el punto de código del caracter
en hexadecimal.
iex> "\u0061" == "a"
true
iex> 0x0061 = 97 = ?a
97
La representación hexadecimal también te será útil para buscar información
sobre un punto de código en particular, por ejemplo en el siguiente URL
https://codepoints.net/U+0061 (nótese el
código hexadecimal en negritas), puedes encontrar la ficha de datos con toda la
información del caracter a
.
Ahora que sabemos de qué se trata el estándar Unicode y qué son los puntos de código, podemos hablar de codificaciones. Mientras que el punto de código es lo que guardamos (qué), una codificación es la manera (cómo) en que lo guardamos. Las codificaciones son implementaciones. En otras palabras, necesitamos un mecanismo para convertir los puntos de código en bytes, de manera que puedan ser almacenados en memoria, escritos a disco, etc.
Elixir usa UTF-8 para codificar string, lo que significa que los puntos de código son codificados como series de bytes de 8 bits. UTF-8 usa una codificación variable en tamaño, entre uno y cuatro bytes, para almacenar cada punto de código. Es capaz de codificar todos los puntos de código de Unicode. Veamos un ejemplo:
iex> string = "sólo"
"sólo"
iex> String.length(string)
4
iex> byte_size(string)
5
Aunque el string del ejemplo tiene 4 caracteres, usa 5 bytes para almacenarlo. Esto es debido a que se requieren 2 bytes para almacenar la "ó" acentuada.
Nota: si estás en Windows, existe la posibilidad de que tu terminal no esté configurada para usar UTF-8. Puedes cambiar la codificacion de tu sesión de terminal corriendo el comando
chcp 65001
antes de correr el REPLiex
(iex.bat
).
Además de definir caracteres, UTF-8 provee la noción de grafemas. Un grafema es una unidad mínima e indivisible de la escritura de una lengua, sin embargo puede consistir de múltiples caracteres que se perciben como un sólo grafema. Por ejemplo, el emoji de mujer bombero se representa mediante la combinación de tres caracteres: el emoji de mujer (👩), un caracter unificador no visible, y el emoji del carro de bomberos (🚒):
iex> String.codepoints("👩🚒")
["👩", "", "🚒"]
iex> String.graphemes("👩🚒")
["👩🚒"]
Elixir es capaz de reconocer que se debe representar como un sólo grafema y por lo tanto su tamaño es 1:
iex> String.length("👩🚒")
1
Nota: si no puedes ver los emojis de los ejemplos en tu terminal, debes verificar que tu terminal soporte emojis y que estés usando una tipografía que los pueda reproducir.
Los documentos codificados en UTF-8 se encuentran en todos lados. Esta página está codificada en UTF-8. La información de codificación es entregada a tu navegador y de esa manera el navegador sabe como reproducir los bytes, caracteres y grafemas apropiadamente.
Si quieres ver qué bytes se usan en un string, un truco es concatenar el byte
nulo <<0>>
al string:
iex> "hełło" <> <<0>>
<<104, 101, 197, 130, 197, 130, 111, 0>>
Alternativamente, puedes ver la representación binaria usando la función IO.inspect/2:
iex> IO.inspect("hełło", binaries: :as_binaries)
<<104, 101, 197, 130, 197, 130, 111>>
Hablemos de cadenas de bits y aprendamos qué hace el constructor <<>>
.
Aunque hemos cubierto los puntos de código en la codificación UTF-8, aún
debemos adentrarnos en la manera en que se almacenan los bytes codificados,
para ello introduciremos el concepto de cadenas de bits. Una cadena de bits
es un tipo de dato fundamental en Elixir. Se denota con la sintaxis <<>>
, y
es una secuencia continua de bits en memoria.
Puedes ver más información sobre el constructor binario de cadenas de
caracteres <<>>
en el manual de
referencia.
Por omisión, 8 bits, que son 1 byte, se usan para almacenar cada número en una
cadena de bits. Pero puedes especificar el número de bits a través del
modificador ::n
para indicar el tamaño en n
bits. También puedes usar la
declaración ::size(n)
:
iex> <<42>> == <<42::8>>
true
iex> <<3::4>>
<<3::size(4)>>
Por ejemplo, el número decimal 3
, cuando se representa con 4 bits en base 2
será 0011
, que será equivalente a guardar cada valor por separado usando un
sólo bit:
iex> <<0::1, 0::1, 1::1, 1::1>> == <<3::4>>
true
Cualquier valor que exceda lo que se puede almacenar por el número de bits predefinidos, será truncado:
iex> <<1>> == <<257>>
true
Aquí, 257
en base 2 será representado como 100000001
, pero dado que sólo
hemos reservado 8 bits para almacenarlo (por omisión), el bit de la izquierda
es ignorado y el valor almacenado es 00000001
, que es 1
.
Un binario es una cadena de bits donde el número de bits es divisible por
8. Esto significa que todo binario es una cadena de bits, pero que no
cualquier cadena de bits es un binario. Podemos usar las funciones
is_bitstring/1
y is_binary/1
para verificarlo:
iex> is_bitstring(<<3::4>>)
true
iex> is_binary(<<3::4>>)
false
iex> is_bitstring(<<0, 255, 42>>)
true
iex> is_binary(<<0, 255, 42>>)
true
iex> is_binary(<<42::16>>)
true
Podemos hacer pattern matching sobre binarios y cadenas de bits:
iex> <<0, 1, x>> = <<0, 1, 2>>
<<0, 1, 2>>
iex> x
2
iex> <<0, 1, x>> = <<0, 1, 2, 3>>
** (MatchError) no match of right hand side value: <<0, 1, 2, 3>>
Observa que a menos que usemos los modificadores ::
, cada entrada en el
patrón binario hará match con un byte (exactamente 8 bits). Si queremos hacer
match sobre un binario de un tamaño desconocido, podemos usar el modificador
binary
al final del patrón:
iex> <<0, 1, x::binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> x
<<2, 3>>
Hay un par de modificadores que son útiles al hacer pattern matching sobre
binarios. El modificador binary-size(n)
hará match con n
bytes en un
binario:
iex> <<head::binary-size(2), rest::binary>> = <<0, 1, 2, 3>>
<<0, 1, 2, 3>>
iex> head
<<0, 1>>
iex> rest
<<2, 3>>
Un string es un binario codificado en UTF-8, donde el punto de código de cada caracter es codificado usando entre 1 y 4 bytes. Por lo tanto todo string es un binario, pero debido a las reglas de codificación del estándar UTF-8, no todo binario es un string válido.
iex> is_binary("hello")
true
iex> is_binary(<<239, 191, 19>>)
true
iex> String.valid?(<<239, 191, 19>>)
false
El operador de concatenación de string es, de hecho, un operador de concatenación binaria:
iex> "a" <> "ha"
"aha"
iex> <<0, 1>> <> <<2, 3>>
<<0, 1, 2, 3>>
Dado que los string son binarios, podemos hacer pattern matching sobre string:
iex> <<head, rest::binary>> = "banana"
"banana"
iex> head == ?b
true
iex> rest
"anana"
Sin embargo, recuerda que al hacer pattern matching, estás trabajando sobre bytes, por lo tanto un string como "über", con caracteres multibyte, no hará match en el caracter, sino en el primer byte del caracter:
iex> "ü" <> <<0>>
<<195, 188, 0>>
iex> <<x, rest::binary>> = "über"
"über"
iex> x == ?ü
false
iex> rest
<<188, 98, 101, 114>>
En el ejemplo anterior, x
hizo match sólo con el primer byte del caracter
multibyte ü
.
Por lo tanto, cuando se realiza pattern matching en string, es importante
usar el modificador utf8
:
iex> <<x::utf8, rest::binary>> = "über"
"über"
iex> x == ?ü
true
iex> rest
"ber"
Nuestra gira por las cadenas de bits, binarios y string está casi completa, pero nos queda un tipo de datos por explicar: la lista de caracteres.
Una lista de caracteres es una lista de enteros, donde todos los enteros son puntos de código válidos. En la práctica, es improbable que te los cruces, sólo en escenarios específicos, como por ejemplo al interactuar con viejas bibliotecas de Erlang que no aceptan tipos de datos binarios como argumentos.
Mientras que las comillas dobles crean tipos de datos string, las comillas simples crean listas de caracteres literales:
iex> 'hello'
'hello'
iex> [?h, ?e, ?l, ?l, ?o]
'hello'
Lo que hay que observar y recordar es que "hello"
no es lo mismo que
'hello'
. Hablando en general, se deben usar siempre comillas dobles para
representar datos de tipo string en Elixir. De todas maneras aprendamos como
funcionan las listas de caracteres.
En vez de contener bytes, una lista de caracteres contiene puntos de código enteros. Sin embargo, la lista de caracteres se imprimirá entre comillas simples si todos los puntos de código se encuentran dentro del rango ASCII, sino es así, se imprimirán como una lista de enteros.
iex 'hello'
'hello'
iex> 'hełło'
[104, 101, 322, 322, 111]
iex> is_list('hełło')
true
Intrepretar enteros como puntos de código puede llevar a comportamientos
inesperados. Por ejemplo, si estás almacenando una lista de enteros que se
encuentra dentro del rango de 0 a 127, iex
interpreta por omisión esta lista
como una lista de caracteres, y por lo tanto mostrará los caracteres ASCII
correspondientes.
iex> heartbeats_per_minute = [99, 97, 116]
'cat'
Puedes convertir una lista de caracteres en un string y viceversa con las
funciones to_string/1
y to_charlist/1
:
iex> to_charlist("hełło")
[104, 101, 322, 322, 111]
iex> to_string('hełło')
"hełło"
iex> to_string(:hello)
"hello"
iex> to_string(1)
"1"
Observa que estas funciones son polimórficas. No sólo convierten cadenas de caracteres en string, también operan sobre int, atom y otros tipos de datos.
La concatenación de string se realiza con el operador <>
, pero las cadenas
de caracteres, al ser listas, usan el operador de concatenación de listas ++
:
iex> 'this ' <> 'fails'
** (ArgumentError) expected binary argument in <> operator but got: 'this '
(elixir) lib/kernel.ex:1821: Kernel.wrap_concatenation/3
(elixir) lib/kernel.ex:1808: Kernel.extract_concatenations/2
(elixir) expanding macro: Kernel.<>/2
iex:1: (file)
iex> 'this ' ++ 'works'
'this works'
iex> "he" ++ "llo"
** (ArgumentError) argument error
:erlang.++("he", "llo")
iex> "he" <> "llo"
"hello"
Con los binarios, string, y listas de caracteres aprendidos, es hora de hablar de las estructuras de datos de tipo clave-valor.