Swift y programación funcional: Contramap

Swift y programación funcional: Contramap

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)) }
    }
}

Ejemplo 1: Validación de Formularios

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.

Definiendo Predicados de Validación

Creamos predicados específicos para validar cada campo del formulario:

Aplicación de Predicados

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)

Ejemplo 2: Sistema de Búsqueda y Filtrado para una Biblioteca de Libros

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.

Creación de Predicados Específicos

Definimos predicados para cada uno de estos criterios de filtrado.

Combinando Predicados para Búsqueda Avanzada