Regex literales: expresiones regulares con tipos
Swift 5.7 incorporó las expresiones regulares al lenguaje con una idea que ningún otro lenguaje mayoritario había llevado tan lejos: que el patrón se analice en tiempo de compilación y que sus capturas aparezcan en el tipo del resultado. Esta lección recorre el literal de barras y su forma extendida, explica cómo las capturas con nombre se convierten en etiquetas de tupla, presenta RegexBuilder como la salida cuando el patrón deja de ser legible, y detalla la semántica de coincidencia por grafemas que distingue a Swift de sus predecesores.
Una expresión regular es, en casi todos los lenguajes, una cadena que el compilador no mira. Si te equivocas al escribir el patrón lo descubres al ejecutarlo; si capturas tres grupos y lees el cuarto, lo descubres al ejecutarlo; si el grupo estaba dentro de una alternativa y a veces no existe, también lo descubres al ejecutarlo. Swift rompió con esa tradición al hacer del patrón parte del programa: el literal se analiza durante la compilación, los errores de sintaxis son errores de compilación y el número, el orden y la opcionalidad de las capturas viven en el tipo del valor devuelto. La consecuencia es que una familia entera de fallos deja de existir.
- Escribir patrones con el literal de barras y con su forma extendida, y saber cuándo hace falta cada uno.
- Leer el tipo de salida de un patrón y acceder a las capturas por posición y por nombre.
- Construir patrones complejos con
RegexBuilder, incluidas capturas que transforman a tipos propios. - Elegir la semántica de coincidencia y las opciones que evitan sorpresas con texto real.
El literal, y lo que el compilador sabe de él
Hay dos literales. El de barras simples sirve para patrones cortos que no contienen barras. El extendido, que envuelve el patrón entre almohadilla y barra, permite barras sin escapar y activa un modo donde los espacios y los saltos de línea se ignoran, lo que hace posible escribir un patrón largo en varias líneas con comentarios. En ambos casos el compilador analiza el patrón y sintetiza un tipo de salida que describe las capturas.
let simple = /\d+/
// Regex<Substring>
let fecha = /(\d{4})-(\d{2})-(\d{2})/
// Regex<(Substring, Substring, Substring, Substring)>
let extendido = #/
(?<anio> \d{4} ) -
(?<mes> \d{2} ) -
(?<dia> \d{2} )
/#
// Regex<(Substring, anio: Substring, mes: Substring, dia: Substring)>
if let m = "2026-07-31".wholeMatch(of: extendido) {
print(m.anio, m.mes, m.dia) // 2026 07 31
}
El primer elemento de la tupla es siempre la coincidencia completa, y a partir de ahí van los grupos en orden de apertura. Un grupo con nombre, escrito con la sintaxis de interrogación y ángulos, se convierte en una etiqueta de tupla, así que el acceso deja de ser un número y pasa a ser un identificador que el compilador verifica. Si renombras el grupo y olvidas actualizar el uso, no compila; si el grupo está dentro de una alternativa y puede no aparecer, su tipo en la tupla es opcional y el compilador te obliga a desenvolverlo.
Si el patrón viene de un archivo de configuración o del usuario, no hay literal posible: se usa el inicializador que lanza, y el resultado tiene una salida anónima. Pierdes el tipado de capturas y accedes por índice con comprobación en ejecución, que es exactamente el modelo de siempre. La ventaja del literal desaparece justo donde desaparece la información en tiempo de compilación, ni antes ni después.
Buscar, extraer, reemplazar
El patrón por sí solo no hace nada; lo que hace el trabajo son los algoritmos que lo consumen, y conviene distinguirlos porque anclan de forma distinta. La coincidencia completa exige que el patrón cubra toda la cadena, la de prefijo exige que empiece en el principio, la primera busca en cualquier posición, y la enumeración recorre todas las apariciones sin solapamiento.
let linea = "temp=21.5C hum=48%"
let par = /(\w+)=([\d.]+)/
for m in linea.matches(of: par) {
print(m.1, m.2) // temp 21.5 y luego hum 48
}
print("21.5".wholeMatch(of: /[\d.]+/) != nil) // true
print(linea.firstMatch(of: par)?.1 ?? "") // temp
// Reemplazo con acceso a la coincidencia
let anonimo = linea.replacing(par) { m in "\(m.1)=oculto" }
// Trocear por patron en lugar de por separador fijo
let trozos = "a1b22c333".split(separator: /\d+/) // [a, b, c]
flowchart TD L[Literal de barras] --> C[Analisis en tiempo de compilacion] C --> T[Tipo Regex con tupla de salida] B[RegexBuilder con componentes] --> T R[Cadena en tiempo de ejecucion] --> A[Regex con salida anonima] T --> M[Capturas verificadas por el compilador] A --> V[Capturas por indice sin verificar]
RegexBuilder, cuando el patrón deja de leerse
Un patrón de tres tokens cabe en un literal y se lee bien. Uno de treinta, con alternativas anidadas y clases de caracteres, se convierte en una línea que nadie quiere tocar. RegexBuilder ofrece el mismo poder expresivo como un constructor de resultados: componentes con nombre, anidamiento con sangrado, y sobre todo capturas que transforman el texto en el tipo que de verdad querías.
import RegexBuilder
let campo = Reference(Substring.self)
let patron = Regex {
Anchor.startOfLine
Capture(as: campo) { OneOrMore(.word) }
"="
TryCapture {
OneOrMore(.digit)
Optionally { "."; OneOrMore(.digit) }
} transform: { Double($0) }
ChoiceOf { "C"; "F"; "%" }
Anchor.endOfLine
}
// Regex<(Substring, Substring, Double)>
if let m = "temp=21.5C".wholeMatch(of: patron) {
print(m[campo], m.2 + 1) // temp 22.5
}
La captura que transforma es la pieza importante y no tiene equivalente en las expresiones regulares clásicas. La variante que puede fallar descarta la coincidencia si la conversión devuelve nulo, de modo que el motor sigue buscando en lugar de entregarte un texto que luego habría que convertir y comprobar aparte. El resultado es que el analizador devuelve valores del dominio, no cadenas: un Double, una fecha, un identificador tipado. La validación y la extracción dejan de ser dos pasos.
RegexBuilder acepta estrategias de análisis como componentes, así que puedes incrustar el analizador de fechas o de moneda que ya usa la aplicación dentro del patrón y recibir el valor ya construido y sensible al idioma. Es la manera de no reimplementar con dígitos y guiones algo que Foundation sabe hacer bien.
Semántica de coincidencia y opciones
Aquí aparece la diferencia más profunda con los motores clásicos, y la que produce resultados inesperados si vienes de ellos. Por defecto, el motor de Swift trabaja con semántica de grafemas: un punto coincide con un cluster completo, y una comparación de literales usa equivalencia canónica igual que el operador de igualdad. Es coherente con el resto del modelo de String, y es lo que quieres cuando el patrón describe texto humano. No es lo que quieres cuando el patrón describe un formato binario o un protocolo, y para eso existe la semántica de escalares.
let emoji = "👍🏽" // pulgar mas modificador de tono
print(emoji.wholeMatch(of: /./) != nil) // true, un grafema
print(emoji.wholeMatch(of: /../) != nil) // false
let porEscalar = /../.matchingSemantics(.unicodeScalar)
print(emoji.wholeMatch(of: porEscalar) != nil) // true, dos escalares
let flexible = /hola mundo/
.ignoresCase()
.dotMatchesNewlines()
.repetitionBehavior(.reluctant)
Conviene además saber qué ancla cada cosa, porque los anclajes de línea y los de cadena no son intercambiables y el modo por defecto trata la entrada como una sola unidad. Si vas a recorrer un archivo entero con un patrón que describe líneas, o activas el modo en que los anclajes reconocen los saltos de línea, o troceas antes y aplicas el patrón a cada línea por separado, que suele ser más claro y más rápido.
let registro = "info: ok\nerror: fallo\ninfo: listo"
let porLinea = /^(\w+): (.+)$/.anchorsMatchLineEndings()
for m in registro.matches(of: porLinea) {
print(m.1, m.2)
}
Las opciones se aplican como métodos sobre el patrón y devuelven otro patrón, lo que las hace componibles y locales: puedes activar la insensibilidad a mayúsculas para un subpatrón concreto dentro de un constructor sin contaminar el resto. Y el control del comportamiento de repetición merece atención especial, porque cambiar de codicioso a perezoso no solo cambia qué texto captura un cuantificador, también cambia cuánto retrocede el motor. En patrones con alternativas anidadas y cuantificadores anidados, esa diferencia es la que separa un análisis instantáneo de uno que parece colgado.
Conviene ver por qué esta característica no es una comodidad sino un cambio de categoría. Una expresión regular escrita como cadena es un programa dentro de otro programa, con su propia sintaxis, su propio motor y su propia semántica, del que el compilador anfitrión no sabe absolutamente nada. Todo lo que sabemos sobre por qué el código con tipos es más fácil de mantener queda suspendido en cuanto abres unas comillas: dentro de ellas no hay verificación, no hay autocompletado, no hay renombrado seguro y no hay diagnósticos útiles. Swift atacó exactamente esa frontera. Al integrar el analizador del patrón en el compilador, convirtió un error de ejecución en un error de compilación; al derivar el tipo de salida del patrón, convirtió el número y la opcionalidad de las capturas en información que el compilador propaga y verifica; y al ofrecer un constructor con los mismos poderes, permitió que un patrón crezca sin volverse ilegible y que sus capturas produzcan valores del dominio en lugar de texto. El patrón dejó de ser un dato opaco para ser código de primera clase. Reconocerás el movimiento porque es el mismo que el lenguaje viene haciendo desde su primera versión: los opcionales cogieron una convención que vivía en la documentación, la ausencia de valor, y la escribieron en el tipo; los errores lanzados cogieron el canal de fallo y lo hicieron visible en la firma; los case paths cogieron el acto de apuntar a un caso y lo volvieron un valor manipulable. En los cuatro casos el patrón de diseño es idéntico: coger algo que existía solo en la cabeza del programador o dentro de una cadena, y darle representación en el sistema de tipos para que el compilador pueda ayudarte con ello. Cuando evalúes una biblioteca o diseñes una API propia, esta es la pregunta que más rendimiento da: qué información importante estoy escondiendo en un dato opaco, y qué pasaría si la subiera al tipo.
- Escribe con literal extendido un patrón para direcciones de correo simplificadas con capturas con nombre para usuario y dominio, y accede a ambas por su etiqueta.
- Toma un patrón tuyo con cinco grupos, léelo por índice y después reescríbelo con nombres. Cambia deliberadamente un nombre y anota el mensaje del compilador.
- Construye con
RegexBuilderun analizador de una línea de registro que devuelva la marca temporal como fecha, el nivel como enumeración propia y el mensaje como texto, usando capturas que transforman. - Prueba un patrón con punto sobre un emoji con modificador de tono, primero con la semántica por defecto y después con la de escalares. Explica los dos resultados con lo que sabes del modelo de grafemas.
- Escribe un patrón con cuantificadores anidados que tarde visiblemente sobre una entrada larga, mide, cambia el comportamiento de repetición y vuelve a medir. Documenta la diferencia.