La programación funcional ha introducido varios conceptos poderosos que pueden mejorar significativamente la claridad, concisión y expresividad del código. Entre estos, los predicados y la función contramap ofrecen una manera elegante de construir lógica de validación y filtrado reutilizable y componible. En este artículo, exploraremos cómo estos conceptos pueden aplicarse en Swift para resolver problemas complejos de manera eficiente. Predicados y contramap: Una Introducción
Un Predicate<A>
es una estructura que encapsula una condición que los elementos de tipo A deben cumplir. Esta condición se representa mediante un closure que toma un elemento de tipo A y devuelve un valor booleano, indicando si el elemento cumple o no con la condición especificada.
La función contramap, por otro lado, permite transformar un Predicate<B>
en un Predicate<A>
, dada una función que convierte de A a B. Esto es particularmente útil cuando queremos aplicar un predicado existente a un tipo de datos diferente, sin necesidad de duplicar o reescribir la lógica del predicado.
struct Predicate<A> {
let evaluate: (A) -> Bool
func contramap<B>(_ transform: @escaping (B) -> A) -> Predicate<B> {
Predicate<B> { b in self.evaluate(transform(b)) }
}
}
Imaginemos una aplicación con un formulario de registro de usuario. Necesitamos validar diferentes campos del formulario, como el nombre, el correo electrónico y la edad, para asegurarnos de que cumplen con ciertos criterios antes de permitir que el usuario envíe el formulario.
Creamos predicados específicos para validar cada campo del formulario:
- isValidName: Verifica que el nombre tenga al menos 2 caracteres.
- isValidEmail: Verifica que el correo electrónico siga un patrón específico.
- isAdult: Verifica que el usuario sea mayor de edad.
Usamos contramap para adaptar estos predicados a un formulario completo, permitiendo una validación compuesta que evalúa todos los campos a la vez.
struct Predicate<A> {
let evaluate: (A) -> Bool
func contramap<B>(_ transform: @escaping (B) -> A) -> Predicate<B> {
Predicate<B> { b in evaluate(transform(b)) }
}
func and(_ other: Predicate<A>) -> Predicate<A> {
Predicate<A> { a in self.evaluate(a) && other.evaluate(a) }
}
func or(_ other: Predicate<A>) -> Predicate<A> {
Predicate<A> { a in self.evaluate(a) || other.evaluate(a) }
}
}
struct UserForm {
var name: String
var email: String
var age: Int
}
// Aquí definimos los predicates para cada una de las propiedades internas del UserForm
let isValidName = Predicate<String> { $0.count >= 2 }
let isValidEmail = Predicate<String> { email in
let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email)
}
let isAdult = Predicate<Int> { $0 >= 18 }
// Aquí finalmente definimos el Predicate para evaluar el UserForm
let isValidUserForm = Predicate<UserForm> { form in
isValidName.evaluate(form.name) &&
isValidEmail.evaluate(form.email) &&
isAdult.evaluate(form.age)
}
// Otra opción sería hacerlo usando los and y or que hemos creado en el predicate
let isValidUserForm = isValidName.contramap { $0.name }
.and(isValidEmail.contramap { $0.email })
.and(isAdult.contramap { $0.age })
let form = UserForm(name: "Jane Doe", email: "[email protected]", age: 20)
let formIsValid = isValidUserForm.evaluate(form)
// Esto imprimirá true si el formulario es válido según nuestros criterios.
print(formIsValid)
En una aplicación de biblioteca de libros, queremos permitir a los usuarios filtrar libros por autor, año de publicación y género.
Definimos predicados para cada uno de estos criterios de filtrado.
Los usuarios pueden combinar estos predicados para crear consultas de búsqueda específicas.
struct Book {
var title: String
var author: String
var year: Int
var genre: String
}
let byAuthor = { (author: String) in Predicate<Book> { $0.author == author } }
let byYear = { (year: Int) in Predicate<Book> { $0.year == year } }
let byGenre = { (genre: String) in Predicate<Book> { $0.genre == genre } }
let searchQuery = byAuthor("Isaac Asimov")
.and(byYear(2000))
.and(byGenre("Science Fiction"))
let library: [Book] = [
Book(title: "Foundation", author: "Isaac Asimov", year: 1951, genre: "Science Fiction"),
Book(title: "I, Robot", author: "Isaac Asimov", year: 1950, genre: "Science Fiction"),
Book(title: "2001: A Space Odyssey", author: "Arthur C. Clarke", year: 1968, genre: "Science Fiction"),
Book(title: "The Martian", author: "Andy Weir", year: 2000, genre: "Science Fiction")
]
let filteredBooks = library.filter(searchQuery.evaluate)
El uso de Predicate<A>
y contramap en Swift ilustra la potencia y flexibilidad de la programación funcional para construir soluciones elegantes a problemas complejos. Estos conceptos no solo mejoran la reusabilidad y la composibilidad del código, sino que también promueven una mayor claridad y expresividad, permitiendo a los desarrolladores implementar lógicas de filtrado y validación complejas de manera concisa y mantenible.
Los dos casos de uso mostrados son casos típicos que pueden llegar a tener bastante complejidad técnica es ciertos tipos de aplicaciones y son casos perfectos para transformar a programación funcional.