1. Por que se necesitan algoritmos genericos
Cuando aprendes programacion, parece que los «algoritmos» son algo del mundo de las olimpiadas y de personas que, por apuesta, ordenan un array en 0.3 nanosegundos. Pero en un proyecto real, un algoritmo suele ser una pequena utilidad que hace que tu codigo sea mas simple y mas honesto: quitar duplicados, encerrar un valor entre limites, encontrar el maximo. Elegimos unique, clamp, maxOf porque muestran perfectamente como los constraints convierten el codigo generico de «acepto/devuelvo» en «hago un trabajo util».
La idea principal de la leccion es esta: no intentamos escribir «una funcion universal para todos los casos», sino disenamos un pequeno contrato. Un contrato no es un documento legal, sino un conjunto de promesas en la firma: que tipos encajan, que pasa en los extremos (array vacio, limites invalidos) y que garantiza devolver la funcion.
Para no perderse, ten en mente esta tabla simple:
| Algoritmo | Que hace | Que necesita del tipo | Por que |
|---|---|---|---|
|
elimina duplicados | Hashable (o Equatable, pero mas lento) | las comprobaciones rapidas mediante Set requieren hash |
|
limita un valor con limites | Comparable | se necesita comparar </> |
|
encuentra el maximo | Comparable | tambien requiere comparacion |
2. Mini modelo de libro para los ejemplos
Antes de escribir algoritmos, necesitamos un objeto comprensible sobre el que practicar. Ahora no estamos construyendo una arquitectura ni discutiendo almacenamiento: solo nos importa un conjunto de tipos que se parezcan a los que realmente viven en un proyecto. Por eso haremos un mini modelo Book con id, title, year y tags.
Abajo tienes el esqueleto inicial. Supondremos que trabajamos en un solo archivo (por ejemplo, main.swift). El codigo es corto, pero ya se parece a algo «real».
import Foundation
struct BookID: Hashable {
let value: Int
}
struct Book {
let id: BookID
let title: String
let year: Int
let tags: [String]
}
Fijate en un detalle pequeno pero importante: BookID es un tipo aparte, no simplemente Int. Eso mejora la legibilidad y reduce la probabilidad de confundir el «id del libro» con el «ano de publicacion», porque el compilador no lo permitira. Y ademas, BookID: Hashable nos sera util en un par de minutos.
3. unique: eliminamos duplicados
Si alguna vez has trabajado con entrada de usuario, ya sabes que la gente puede introducir «lo mismo» de formas distintas. Incluso si la entrada es perfecta, los duplicados aparecen facilmente al unir listas, al parsear, al hacer «anadimos mas etiquetas», al «pulsar Enter dos veces por error». Por eso unique no es un capricho, sino algo muy practico.
Pero aqui aparece una decision de diseno: queremos simplemente «elementos unicos en cualquier orden», o queremos «elementos unicos conservando el orden de la primera aparicion»? En aplicaciones CLI, la segunda opcion suele ser mas agradable: el usuario introdujo ["swift", "cli", "swift"], y tu devuelves ["swift", "cli"], sin cambiar el orden.
unique ingenuo con Equatable
Empecemos por la version mas «humana»: recorrer el array y, si el elemento aun no esta en el resultado, anadirlo. Para esto solo hace falta Equatable, porque usamos ==. En Swift, la igualdad es el contrato del protocolo Equatable, expresado mediante el operador ==.
import Foundation
func uniqueSlow<T: Equatable>(_ items: [T]) -> [T] {
var result: [T] = []
for item in items {
if !result.contains(item) { // O(n) dentro del ciclo => O(n²) en total
result.append(item)
}
}
return result
}
print(uniqueSlow([1, 2, 2, 3])) // [1, 2, 3]
Esta version es buena porque funciona con casi cualquier tipo (muchos tipos implementan Equatable facilmente) y es muy comprensible. Pero el precio es una complejidad potencial de O(n²) en arrays grandes.
unique rapido con Hashable y Set
Ahora hagamos una version que usa Set para comprobar rapidamente si ya hemos visto ese valor. Para esto el tipo debe ser Hashable, porque Set guarda elementos por hash (y las claves de Dictionary tambien).
Una aclaracion importante: seguimos queriendo conservar el orden, asi que Set solo se usara «para comprobar», y el resultado seguira siendo un array normal.
import Foundation
func unique<T: Hashable>(_ items: [T]) -> [T] {
var seen = Set<T>()
var result: [T] = []
for item in items {
if seen.insert(item).inserted { // inserted == true si el elemento no existia
result.append(item)
}
}
return result
}
print(unique(["swift", "cli", "swift"])) // ["swift", "cli"]
Aqui lo bonito es que el contrato de la funcion es honesto: si quieres velocidad, dale a la funcion un tipo que pueda servir como clave de un conjunto (Hashable). Y eso no es un «capricho del autor de la funcion», sino una consecuencia directa de la operacion interna.
Ejemplo: etiquetas unicas de un libro
Ahora lo unimos con Book. Imagina que las etiquetas llegan de distintas fuentes y pueden repetirse. Queremos normalizarlas.
import Foundation
func normalizedTags(for book: Book) -> [String] {
unique(book.tags.map { $0.lowercased() })
}
let book = Book(
id: BookID(value: 1),
title: "Swift Basics",
year: 2024,
tags: ["Swift", "cli", "swift", "Basics"]
)
print(normalizedTags(for: book)) // ["swift", "cli", "basics"]
Si, ahora hemos hecho lowercased() y despues unique. Es como lavar las verduras y luego cortarlas: dos operaciones seguidas, pero cada una se ocupa de lo suyo. Y eso es un estilo muy saludable.
4. clamp: limitamos un valor con limites
clamp es una funcion que toma un valor y unos limites, y garantiza que el resultado no saldra de ellos. Es super util para todo lo «de usuario»: porcentajes, ano, valoracion, limite, cantidad de resultados. En un mundo ideal, el usuario solo introduce numeros correctos. En el real, el usuario introduce -100, 999999 y a veces 42, porque «bueno, y que».
Y aqui importa el estilo de pensamiento: en lugar de escribir en diez sitios if x < min … if x > max …, aislamos la regla en una sola funcion. El concepto de «hacer clamp de un valor dentro de limites» aparece en distintas partes del ecosistema Swift.
Implementacion basica de clamp para Comparable
Aqui todo es sencillo: necesitamos poder comparar. Es decir, T: Comparable.
import Foundation
func clamp<T: Comparable>(_ value: T, lower: T, upper: T) -> T {
if value < lower { return lower }
if value > upper { return upper }
return value
}
print(clamp(10, lower: 0, upper: 5)) // 5
print(clamp(-3, lower: 0, upper: 5)) // 0
print(clamp(3, lower: 0, upper: 5)) // 3
Este es uno de esos casos en los que la funcion generica parece casi «matematica», y eso es un cumplido.
Que hacer si lower > upper
Aqui empieza la vida adulta de una API. Que hacer si alguien llama a clamp(value, lower: 10, upper: 5)? Hay tres enfoques populares: considerarlo un error de programador, intercambiar automaticamente los limites, o devolver algo «como salga». Nosotros elegiremos una opcion simple y robusta para un proyecto didactico: si los limites estan intercambiados, los «arreglaremos» con cuidado dentro de la funcion para que sea tolerante.
import Foundation
func clampSafe<T: Comparable>(_ value: T, lower: T, upper: T) -> T {
let minBound = min(lower, upper)
let maxBound = max(lower, upper)
return clamp(value, lower: minBound, upper: maxBound)
}
print(clampSafe(7, lower: 10, upper: 5)) // 7 (los limites se corrigieron a 5...10)
En codigo mas estricto, a veces es mas logico fallar con precondition, pero ahora estamos practicando precisamente «utilidades para datos», no politica de errores.
Ejemplo: normalizacion del ano
En una aplicacion de biblioteca, el ano de publicacion es un clasico. Supongamos que queremos limitar el ano a un intervalo: de 1450 (aproximadamente «despues de Gutenberg») hasta el ano actual. Obtendremos el ano actual mediante Calendar, para no dejarlo fijo en el codigo.
import Foundation
func normalizedYear(_ year: Int) -> Int {
let currentYear = Calendar.current.component(.year, from: Date())
return clampSafe(year, lower: 1450, upper: currentYear)
}
print(normalizedYear(1200)) // 1450
print(normalizedYear(3000)) // (ano actual en el momento de la ejecucion)
Este es un ejemplo limpio de «no confiamos en la entrada, pero tampoco montamos un drama».
5. maxOf: encontramos el maximo y devolvemos Optional
El maximo parece elemental: «el numero mas grande». Pero en cuanto buscas el maximo en un array, aparece la pregunta: y si el array esta vacio? Y eso no es un problema teorico. Un array vacio es un estado normal de los datos: «aun no hay libros en la biblioteca», «no se ha encontrado nada para la consulta», «el usuario elimino todos los registros».
Por eso, en maxOf (para un array) el elemento de diseno mas importante es la devolucion de Optional: o existe un maximo, o no existe.
maxOf para dos valores
Primero, una version parecida a la de un «manual»: el maximo de dos valores.
import Foundation
func maxOf<T: Comparable>(_ a: T, _ b: T) -> T {
a > b ? a : b
}
print(maxOf(10, 3)) // 10
print(maxOf("b", "a")) // "b"
Aqui todo es lineal: si hay dos valores, el maximo existe con seguridad.
maxOf para un array: devolvemos T?
Ahora la version importante: el maximo de un array.
import Foundation
func maxOf<T: Comparable>(_ items: [T]) -> T? {
guard let first = items.first else { return nil }
var best = first
for item in items {
if item > best { best = item }
}
return best
}
print(maxOf([3, 1, 10, 2]) as Any) // Optional(10)
print(maxOf([Int]()) as Any) // nil
Usamos deliberadamente guard let y devolvemos nil en un array vacio. Eso hace que el contrato de la funcion sea honesto: «si el maximo existe, lo devolvere; si no, dire que no existe».
6. Los mismos algoritmos en sus propios tipos
Ahora llega el momento en que muchos sienten de verdad el poder de los protocolos por primera vez: escribes una vez el algoritmo y luego «conectas» tus propios tipos a el. Lo importante es elegir correctamente que protocolo hace falta: Hashable para unique, Comparable para clamp y maxOf.
unique para BookID
Ya hicimos BookID: Hashable, asi que podemos deduplicar ids «listos para usar».
import Foundation
let ids = [BookID(value: 1), BookID(value: 1), BookID(value: 2)]
let uniqueIDs = unique(ids)
print(uniqueIDs.map { $0.value }) // [1, 2]
Aqui es agradable que unique ni siquiera sabe que es BookID. Y eso es correcto: un algoritmo no deberia depender del dominio del problema.
clamp para el tipo Year
A veces apetece no guardar el ano como Int, sino crear un tipo aparte Year, para no confundirlo con el «numero de paginas» o el «limite de resultados». Para clamp necesitamos Comparable, asi que hay que implementar la comparacion.
import Foundation
struct Year: Comparable {
let value: Int
static func < (lhs: Year, rhs: Year) -> Bool {
lhs.value < rhs.value
}
}
let y = clampSafe(Year(value: 3000), lower: Year(value: 1450), upper: Year(value: 2026))
print(y.value) // 2026
Hemos implementado solo el operador <, y Swift obtiene == automaticamente, porque Year es una struct con un unico campo let (y, en general, la sintesis de igualdad para estructuras es algo tipico). Al final, Year se ha vuelto comparable y, con ello, compatible con nuestros algoritmos genericos.
maxOf para libros: elegimos el sentido de «maximo»
Con los libros es mas interesante: que significa «el maximo de un libro»? El mas nuevo? El mas largo? El mas querido? En proyectos reales, a menudo es mejor no hacer Book: Comparable, porque el «orden natural» puede ser discutible. Pero para un ejemplo didactico elegiremos un criterio claro: «el libro mas nuevo» es el maximo por year.
import Foundation
struct ComparableBook: Comparable {
let title: String
let year: Int
static func < (lhs: ComparableBook, rhs: ComparableBook) -> Bool {
lhs.year < rhs.year
}
}
let books = [
ComparableBook(title: "Old", year: 2001),
ComparableBook(title: "New", year: 2024)
]
let newest = maxOf(books)
print(newest?.title as Any) // Optional("New")
Idea importante: eres tu quien decide que significa «comparar» tu tipo. El compilador no es telepata. Y eso es bueno: menos magia, mas control.
7. Ejemplo: procesamiento de entrada para CLI
Ahora vamos a reunir un pequeno flujo de datos parecido a un escenario real de CLI: el usuario introduce un libro, nosotros normalizamos el ano, limpiamos las etiquetas, y luego podemos, por ejemplo, encontrar «el libro mas nuevo» en la biblioteca. No es arquitectura ni parser: solo una demostracion honesta de como las utilidades hacen que el codigo sea mas corto.
import Foundation
func addBook(
to library: inout [Book],
id: Int,
title: String,
yearInput: Int,
tagsInput: [String]
) {
let year = normalizedYear(yearInput)
let tags = unique(tagsInput.map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() })
let book = Book(
id: BookID(value: id),
title: title,
year: year,
tags: tags
)
library.append(book)
}
var library: [Book] = []
addBook(to: &library, id: 1, title: "Swift", yearInput: 3000, tagsInput: [" Swift ", "cli", "swift"])
addBook(to: &library, id: 2, title: "Algorithms", yearInput: 2020, tagsInput: ["algo", "cli"])
let newestYear = maxOf(library.map { $0.year })
print(newestYear as Any) // Optional(ano actual en el momento de la ejecucion, ya que 3000 fue limitado)
Aqui se ven de inmediato tres efectos.
En primer lugar, clamp convierte una «entrada terrible» en un valor normal, sin esparcir comprobaciones por el codigo. En segundo lugar, unique garantiza que las etiquetas seran unicas y normalizadas. En tercer lugar, maxOf muestra por que el resultado de un array es un Optional: si la biblioteca estuviera vacia, el maximo del ano seria nil, y eso es normal.
8. Errores tipicos
Error n. 1: hacer unique con Set y perder accidentalmente el orden, para luego sorprenderse de que la salida salta.
Una trampa muy comun: escribir Array(Set(items)) y creer que el problema esta resuelto. Si, los duplicados desapareceran, pero el orden sera «como salga», porque Set no esta obligado a conservar el orden de insercion. Si para el usuario el orden importa (y en CLI a menudo importa), guarda un array result aparte y usa Set solo para comprobaciones.
Error n. 2: exigir Hashable «por si acaso», cuando bastaria con Equatable.
A veces apetece escribir func unique<T: Hashable>(...) en todas partes, porque «asi es mas rapido». Pero eso hace tu funcion menos universal: no todos los tipos son faciles de convertir en Hashable, sobre todo si contienen numeros en coma flotante o campos complejos. Sigue el principio del contrato minimo suficiente: si solo usas ==, basta con Equatable. Y exige Hashable solo cuando realmente uses Set / claves de diccionario.
Error n. 3: olvidar el caso de «array vacio» en maxOf y devolver T en lugar de T?.
Los principiantes a menudo escriben var best = items[0] y sufren un fallo en un array vacio. Pero un array vacio no es un error del usuario ni un «estado imposible»: es un caso limite normal de los datos. Devuelve Optional y trata nil con la misma calma con la que tratas «el usuario no ha introducido nada».
Error n. 4: usar clamp sin acordar que hacer con los limites intercambiados.
Si en el proyecto no hay una regla unica, un desarrollador «arreglara» los limites, otro fallara, y otro devolvera lower esperando lo mejor. Al final, el error parecera «a veces funciona, a veces no». Elige un comportamiento y mantenlo. Para un proyecto didactico, la opcion segura es normalizar los limites mediante min/max y seguir adelante.
Error n. 5: hacer Comparable para un modelo de dominio sin un significado claro de comparacion.
Hacer que Book sea Comparable «para que funcione maxOf» es tentador, pero peligroso. Un libro no tiene un unico orden natural: por ano, por titulo, por valoracion, por autor. Si aun asi haces Comparable, prepara una explicacion de por que ese orden es exactamente el «natural» para tu aplicacion, o dentro de un mes ni tu mismo te lo creerias.
GO TO FULL VERSION