← Volver al listado de tecnologías

Capítulo 4: Strings - Inmutables y Compartidos

Por: Artiko
luastringspattern-matchingutf-8text-processing

Capítulo 4: Strings - Inmutables y Compartidos

“Todo lo complicado es innecesario, y todo lo necesario es simple.” — Filosofía Lua

Las strings en Lua son uno de los tipos de datos más usados. Son inmutables, internadas (interned), y tienen un sistema de pattern matching único que es más simple que regex pero sorprendentemente poderoso.

Inmutabilidad: Strings Nunca Cambian

En Lua, una vez que creas una string, nunca cambia:

-- >>> s = "hello"
-- >>> s = s .. " world"  -- Esto NO modifica la string original
-- >>> print(s)
-- hello world

Lo que realmente sucede:

  1. Se crea la string "hello"
  2. Se crea una NUEVA string "hello world"
  3. La variable s ahora apunta a la nueva string
  4. La string original "hello" queda para el garbage collector (a menos que otra variable la referencie)

Implicaciones

-- ❌ MALO: Concatenación en loop
local result = ""
for i = 1, 10000 do
    result = result .. "x"  -- Crea 10,000 strings nuevas!
end

-- ✅ BUENO: Usar table.concat
local parts = {}
for i = 1, 10000 do
    table.insert(parts, "x")
end
local result = table.concat(parts)  -- Una sola concatenación

PERFORMANCE TIP:

table.concat es órdenes de magnitud más rápido que concatenación repetida:

  • Concatenación en loop: O(n²)
  • table.concat: O(n)

String Interning: Todas las Strings Son Únicas

Lua mantiene una tabla interna de todas las strings. Si dos strings tienen el mismo contenido, son la misma string en memoria:

-- >>> s1 = "hello"
-- >>> s2 = "hello"
-- >>> print(s1 == s2)  -- Comparación de contenido
-- true
-- >>> print(s1 == s2)  -- ¡También son la MISMA string en memoria!
-- true

Esto significa que:

  1. Comparar strings es rápido: Solo compara punteros, no contenido
  2. Strings ocupan menos memoria: Cada string única se almacena solo una vez
  3. Crear strings “duplicadas” es gratis: Si ya existe, se reutiliza

Deep Dive: Cómo Funciona

-- Internamente, Lua mantiene algo así:
string_table = {
    ["hello"] = <puntero a memoria>,
    ["world"] = <puntero a memoria>,
    ["foo"] = <puntero a memoria>
}

-- Cuando haces:
local s = "hello"
-- Lua busca en string_table. Si existe, devuelve el puntero. Si no, crea la string.

NOTA: No hay strings mutables

A diferencia de Python (que tiene str y bytearray) o JavaScript (que tiene strings y arrays de chars), Lua solo tiene strings inmutables.

Operaciones Básicas

Concatenación: Operador ..

-- >>> s1 = "Hello"
-- >>> s2 = "World"
-- >>> s3 = s1 .. " " .. s2
-- >>> print(s3)
-- Hello World

Convertir números automáticamente:

-- >>> age = 30
-- >>> msg = "You are " .. age .. " years old"
-- >>> print(msg)
-- You are 30 years old

Longitud: Operador #

-- >>> s = "hello"
-- >>> print(#s)
-- 5

Cuidado con UTF-8:

-- >>> s = "señor"  -- 5 caracteres
-- >>> print(#s)
-- 6  -- ¡Cuenta bytes, no caracteres!

Para contar caracteres UTF-8:

-- >>> s = "señor"
-- >>> print(utf8.len(s))
-- 5

Acceso a Caracteres: string.sub

-- >>> s = "hello"
-- >>> print(string.sub(s, 1, 1))  -- Primer carácter
-- h
-- >>> print(string.sub(s, 2, 4))  -- Del 2 al 4
-- ell
-- >>> print(string.sub(s, -1))   -- Último carácter
-- o

Lua NO tiene s[i] para acceder a caracteres individuales.

Comparación

Strings se comparan lexicográficamente:

-- >>> print("apple" < "banana")
-- true
-- >>> print("Z" < "a")  -- Mayúsculas vienen antes
-- true

La Biblioteca string

Lua tiene una rica biblioteca para manipular strings.

Mayúsculas/Minúsculas

-- >>> s = "Hello World"
-- >>> print(string.upper(s))
-- HELLO WORLD
-- >>> print(string.lower(s))
-- hello world

Repetición

-- >>> print(string.rep("ha", 3))
-- hahaha
-- >>> print(string.rep("-", 20))
-- --------------------

Reverso

-- >>> print(string.reverse("hello"))
-- olleh

Buscar y Reemplazar

-- Buscar
-- >>> s = "hello world"
-- >>> start, finish = string.find(s, "world")
-- >>> print(start, finish)
-- 7    11

-- Reemplazar
-- >>> s = "hello world"
-- >>> result = string.gsub(s, "world", "Lua")
-- >>> print(result)
-- hello Lua

-- Reemplazar N veces
-- >>> s = "foo foo foo"
-- >>> result = string.gsub(s, "foo", "bar", 2)
-- >>> print(result)
-- bar bar foo

Formato: string.format

Similar a printf de C:

-- >>> name = "Alice"
-- >>> age = 30
-- >>> print(string.format("My name is %s and I'm %d years old", name, age))
-- My name is Alice and I'm 30 years old

-- >>> pi = 3.14159265
-- >>> print(string.format("Pi: %.2f", pi))
-- Pi: 3.14

-- >>> num = 42
-- >>> print(string.format("Hex: 0x%x, Oct: %o", num, num))
-- Hex: 0x2a, Oct: 52

Especificadores comunes:

Pattern Matching: NO es Regex

Lua tiene su propio sistema de patrones. NO es compatible con regex, pero es más simple y suficientemente poderoso.

Diferencias con Regex

RegexLua Pattern
.. (cualquier carácter)
\d%d (dígito)
\w%w (alfanumérico)
\s%s (espacio)
** (0 o más)
++ (1 o más)
?- (0 o más, no greedy)
[abc][abc] (clase de caracteres)
(...)(...) (captura)

Clases de Caracteres

Versión negada (mayúscula):

Ejemplos

-- Encontrar un número
-- >>> s = "Age: 30"
-- >>> num = string.match(s, "%d+")
-- >>> print(num)
-- 30

-- Encontrar una palabra
-- >>> s = "Hello World"
-- >>> word = string.match(s, "%a+")
-- >>> print(word)
-- Hello

-- Capturar grupos
-- >>> s = "Name: Alice, Age: 30"
-- >>> name, age = string.match(s, "Name: (%a+), Age: (%d+)")
-- >>> print(name, age)
-- Alice    30

-- Encontrar todos los matches
-- >>> s = "foo bar baz"
-- >>> for word in string.gmatch(s, "%a+") do
-- >>>     print(word)
-- >>> end
-- foo
-- bar
-- baz

Reemplazar con Patterns

-- Reemplazar todos los números con 'X'
-- >>> s = "Room 101, Floor 5"
-- >>> result = string.gsub(s, "%d+", "X")
-- >>> print(result)
-- Room X, Floor X

-- Usar capturas en el reemplazo
-- >>> s = "John Doe"
-- >>> result = string.gsub(s, "(%a+) (%a+)", "%2, %1")
-- >>> print(result)
-- Doe, John

Anclajes

-- Verificar que empiece con "Hello"
-- >>> s = "Hello World"
-- >>> if string.match(s, "^Hello") then
-- >>>     print("Starts with Hello")
-- >>> end
-- Starts with Hello

-- Verificar que termine con ".txt"
-- >>> filename = "document.txt"
-- >>> if string.match(filename, "%.txt$") then
-- >>>     print("Es un archivo de texto")
-- >>> end
-- Es un archivo de texto

UTF-8 en Lua 5.3+

Desde Lua 5.3, hay soporte built-in para UTF-8.

Biblioteca utf8

-- Longitud en caracteres (no bytes)
-- >>> s = "señor"
-- >>> print(utf8.len(s))
-- 5

-- Iterar sobre caracteres UTF-8
-- >>> s = "héllo"
-- >>> for pos, code in utf8.codes(s) do
-- >>>     print(pos, utf8.char(code))
-- >>> end
-- 1    h
-- 2    é
-- 4    l
-- 5    l
-- 6    o

-- Convertir code point a carácter
-- >>> print(utf8.char(0x1F600))  -- Emoji 😀
-- 😀

Validar UTF-8

-- >>> s = "válido"
-- >>> if utf8.len(s) then
-- >>>     print("UTF-8 válido")
-- >>> end
-- UTF-8 válido

Caso Práctico: Parser de Markdown Simple

Implementemos un parser que convierta markdown básico a HTML:

local MarkdownParser = {}

function MarkdownParser.parse(markdown)
    local html = markdown

    -- Headers
    html = string.gsub(html, "^### (.+)$", "<h3>%1</h3>", 1)
    html = string.gsub(html, "^## (.+)$", "<h2>%1</h2>", 1)
    html = string.gsub(html, "^# (.+)$", "<h1>%1</h1>", 1)

    -- Bold
    html = string.gsub(html, "%*%*(.-)%*%*", "<strong>%1</strong>")

    -- Italic
    html = string.gsub(html, "%*(.-)%*", "<em>%1</em>")

    -- Code inline
    html = string.gsub(html, "`(.-)`", "<code>%1</code>")

    -- Links [text](url)
    html = string.gsub(html, "%[(.-)%]%((.-)%)", '<a href="%2">%1</a>')

    return html
end

function MarkdownParser.parse_multiline(markdown)
    local lines = {}
    for line in markdown:gmatch("[^\r\n]+") do
        table.insert(lines, MarkdownParser.parse(line))
    end
    return table.concat(lines, "\n")
end

Uso:

-- >>> md = [[
-- >>> # Welcome to Lua
-- >>> This is **bold** and this is *italic*.
-- >>> Check out [Lua](https://lua.org) for more info.
-- >>> Use `print()` to output text.
-- >>> ]]
-- >>> html = MarkdownParser.parse_multiline(md)
-- >>> print(html)
-- <h1>Welcome to Lua</h1>
-- This is <strong>bold</strong> and this is <em>italic</em>.
-- Check out <a href="https://lua.org">Lua</a> for more info.
-- Use <code>print()</code> to output text.

Mejora: Bloques de Código

function MarkdownParser.parse_code_blocks(markdown)
    local in_code_block = false
    local lines = {}
    local code_lines = {}

    for line in markdown:gmatch("[^\r\n]+") do
        if line:match("^```") then
            if in_code_block then
                -- Cerrar bloque de código
                table.insert(lines, "<pre><code>")
                table.insert(lines, table.concat(code_lines, "\n"))
                table.insert(lines, "</code></pre>")
                code_lines = {}
                in_code_block = false
            else
                -- Abrir bloque de código
                in_code_block = true
            end
        else
            if in_code_block then
                table.insert(code_lines, line)
            else
                table.insert(lines, MarkdownParser.parse(line))
            end
        end
    end

    return table.concat(lines, "\n")
end

Builder de Strings Eficiente

Cuando construyes strings dinámicamente, usa este patrón:

local StringBuilder = {}
StringBuilder.__index = StringBuilder

function StringBuilder.new()
    return setmetatable({_parts = {}}, StringBuilder)
end

function StringBuilder:append(str)
    table.insert(self._parts, str)
    return self  -- Para chainear
end

function StringBuilder:append_line(str)
    return self:append(str):append("\n")
end

function StringBuilder:to_string()
    return table.concat(self._parts)
end

function StringBuilder:__tostring()
    return self:to_string()
end

Uso:

-- >>> sb = StringBuilder.new()
-- >>> sb:append("Hello ")
-- >>>   :append("World")
-- >>>   :append_line("!")
-- >>>   :append("Lua is awesome")
-- >>> print(sb)
-- Hello World!
-- Lua is awesome

Caso Práctico: Template Engine

local Template = {}

function Template.render(template, data)
    -- Reemplazar {{variable}} con valores de data
    return (string.gsub(template, "{{%s*(.-)%s*}}", function(key)
        return tostring(data[key] or "")
    end))
end

Uso:

-- >>> template = "Hello {{name}}, you are {{age}} years old!"
-- >>> data = {name = "Alice", age = 30}
-- >>> print(Template.render(template, data))
-- Hello Alice, you are 30 years old!

Versión avanzada con expresiones:

function Template.render_advanced(template, data)
    -- Permitir expresiones simples: {{user.name}}
    return (string.gsub(template, "{{%s*(.-)%s*}}", function(expr)
        -- Evaluar la expresión en el contexto de data
        local func, err = load("return " .. expr, "template", "t", data)
        if not func then
            return "{{" .. expr .. "}}"  -- Si falla, dejar como está
        end
        local ok, result = pcall(func)
        return ok and tostring(result) or ""
    end))
end

Resumen del Capítulo

Strings en Lua:

Próximo: Capítulo 5: Múltiples Valores y Destructuring


Ejercicios

  1. Contar Palabras: Escribe una función que cuente palabras en una string.
function word_count(s)
    -- Tu código aquí
end

-- >>> print(word_count("Hello world from Lua"))
-- 4
  1. Slug Generator: Convierte una string a formato slug (URL-friendly).
function slugify(s)
    -- Tu código aquí
end

-- >>> print(slugify("Hello World! ¿Cómo estás?"))
-- hello-world-como-estas
  1. CSV Parser: Parse una línea CSV considerando comillas.
function parse_csv_line(line)
    -- Tu código aquí
end

-- >>> print(table.concat(parse_csv_line('a,b,"c,d",e'), " | "))
-- a | b | c,d | e