dados de letras con mensaje "TEACH"

Tipos genéricos en protocolos con Swift

Publicado por

Hoy voy a tocar un tema que me encanta. Me he decidido por hacer esta entrada porque creo que entender cómo funcionan los tipos genéricos en protocolos con Swift es algo realmente útil cuando sabes cómo y cuándo aplicarlo. Podéis encontrar toda la información detallada sobre el uso de genéricos en la documentación oficial de Swift sobre Generics. Os cuento primero lo que haremos:

¡Vamos a jugar a los dados!

Así es. Vamos a crear una aplicación para generar dados. Pero cómo bien estarás pensando…. ¡Hay muchos tipos de dados! ¿Verdad? Pues esa es la gracia de todo. Y por eso vamos a necesitar hacer uso de los tipos genéricos en Swift. Queremos que nuestra aplicación pueda trabajar con dados independientemente de qué tipo de dado se trate. Ya sea de 6 caras o de 15, sean números, letras o frases, o incluso de imágenes o iconos.

¡Fabriquemos un dado!

Espera espera… Antes de fabricar un dado, deberíamos definir qué es exactamente un dado.

Si pensamos en un dado, podemos decir de forma simple (y bruta) que es un dispositivo que al lanzarlo da como resultado una opción al azar de entre una serie de opciones (Aquí tenéis la definición de la RAE por si la mía os ha chirriado mucho…). Pues está definición ya la podemos convertir a código de una forma muy simple. Para ello vamos a crear un protocolo en el que le vamos a definir que tiene que tener cualquier objeto para ser considerado un dado.

protocol Dice {
    associatedtype SideType
    
    var sides: [SideType] { get }
    
    func roll() -> SideType?
}

¿Que hemos hecho aquí? Muy fácil. Hemos creado un protocol llamado Dice (dado en ingles), al cual le hemos dicho que tendrá un tipo asociado (associatedtype) que llamaremos SideType. Básicamente, hasta aquí, lo que hemos hecho es decirle que nuestro protocolo trabajará con un tipo de dato que ya le diremos mas adelante, pero que por el momento lo llamaremos SideType.

A continuación hemos creado una propiedad llamada sides que no es más que un listado de valores del tipo SideType (recordemos que aún no le hemos dicho de que dato se tratará, simplemente le estamos diciendo que del tipo de dato que sea, aquí habrá un array). Y le decimos también que habrá un método llamado roll() que devolverá un elemento (opcional) de ese mismo tipo de dato. Este método representa nuestro acto de tirar el dado y puede devolver opcional porque (matemáticamente) si él dado no tuviera ninguna cara (el array sides está vacío) no devolvería nada por más que tiremos el dado.

Perfecto, pero… con esto solo hemos definido que es para nosotros un dado…. ¡Creemos uno!

Nuestro primer dado

Ahora ya sabemos que es un dado y que necesitamos para crear uno… Pues vamos a ello. Vamos a crear un dado «típico». 6 caras con valores del 1 al 6 en cada cara (el del parchís o la oca de toda la vida).

struct Dice6Sides: Dice {
    let sides: [Int] = [1,2,3,4,5,6]

    func roll() -> Int? {
        return sides.randomElement()
    }
}

¡Pues aquí lo tenemos! Este es nuestro primer dado. Veamos-lo en detalle:

Hemos creado un struct que implementa nuestro protocolo Dice. Luego le hemos creado una propiedad llamada sides (como bien pedía nuestro protocolo) pero esta vez le hemos dicho que el tipo de dato será Int. ¿Y que ha pasado? Pues que ese valor SideType que antes le habíamos dicho al protocol que ya le informaríamos de que tipo de dato se trata, Swift lo está cogiendo inferido de la definición que acabamos de hacer. Es decir, al nosotros decirle que vamos a tener un array llamado igual que el del protocol pero con un tipo de dato especifico, Swift va a entender que todo lo que hemos dicho que sería SideType en el protocol ahora va a ser Int. Y por eso, lo siguiente que nos pedirá para cumplir la implementación del protocolo Dice será añadir un método llamado roll() que devuelva un Int opcional. ¡Es fantasticooo!!

¡Tiremos el dado!

Bien! Ahora ya tenemos un dado (mejor dicho el molde). Ahora ya podemos empezar a jugar con ellos. Vamos a crear una instancia de nuestro dado Dice6Sides y lo lanzaremos un par de veces para ver que tal funciona…

let myFirstDice = Dice6Sides()
print(myFirstDice.roll()) // Imprime un random entre 1 y 6
print(myFirstDice.roll()) // Imprime un random entre 1 y 6

¿¡Genial, no!? Pero… si estamos hablando del uso de tipos genéricos en protocolos con Swift es precisamente para poder crear cualquier tipo de dado…

¡A por más dados!

Hemos creado un definición de «dado» y un primer molde para un dado en concreto. ¡Bien! ¡Pero queremos más! Vamos a crear distintos moldes para distintos tipos de dados. Empezaremos por uno de letras.

struct CharacterDice: Dice {
    var sides: [Character] = ["A", "B", "C", "D", "E", "F"]
    
    func roll() -> Character? {
        return sides.randomElement()
    }
}

¿Fácil, verdad? Sin embargo aquí ya podemos darnos cuenta de un detalle… ¡la función roll() es idéntica en ambos dados! Mmm…. Eso ya debería molestarnos un poco. ¡Pero sigamos!

Crearemos también un dado con palabras. Algo así como esos dados del kamasutra… pero a nuestro estilo.

struct HotDice: Dice {
    var sides: [String] = [ "coding", "gaming", "drinking", "sleeping" ]
    
    func roll() -> String? {
        return sides.randomElement()
    }
}
  

¡Perfecto! Ahora ya podemos jugar con nuestros nuevos dados:

let myCharDice = CharacterDice()
let myHotDice = HotDice()

print(myCharDice.roll())
print(myHotDice.roll())

Sin embargo, y como ya nos ha pasado con el CharacterDice, la función roll() vuelve a repetirse. Esto no suele ser una muy buena práctica. ¡Tenemos que poner solución!

Evitando duplicar el código

Sí solo estamos modificando los valores del dado, pero el acto de lanzarlo siempre va a hacer lo mismo (coger una de las caras al azar), no tiene sentido que en cada dado le digamos que tiene que hacer al lanzar. Por ello, podemos extender nuestro protocolo Dice para añadirle esa funcionalidad por defecto.

// protocol Dice { ... }

extension Dice {
    func roll() -> SideType? {
        return sides.randomElement()
    }
}

Añadiendo esto después de definir el protocol, lo que le estamos haciendo es proveerlo de lógica por defecto. Ahora, todo el que implemente este protocolo tendrá por defecto un método roll() que devolverá uno de los elementos del array sides al azar.

Aún así, nosotros podemos, en un dado concreto, sobrescribir ese método para añadirle una lógica distinta en caso de que quisiéramos.

¡Hagamos un poco de trampas!

Vamos a hacer un dado trucado que tenderá a devolver con mayor probabilidad el valor que le especifiquemos. Además, podremos escoger también esa probabilidad a nuestro gusto en el momento de instanciarlo (aunque por defecto será de 50%). Eso sí, es importante que para los demás parezca un dado normal y corriente… ¡O se nos verá el plumero!

struct CheatedDice {
    let sides: [Int] = [1,2,3,4,5,6]
    
    private let cheatedValue: Int
    private let cheatedProbabilityPercent: Int
    
    init( cheatedValue: Int, cheatedProbabilityPercent: Int = 50 ){
        self.cheatedValue = cheatedValue
        self.cheatedProbabilityPercent = cheatedProbabilityPercent
    }
    
    func roll() -> Int? {
        let cheatRandom = Int.random(in: 0...100) // Obtenemos un random del 0 al 100
        
        if cheatRandom > cheatedProbabilityPercent { // Si sacamos un valor más alto que la probabilidad
            return sides.randomElement() // retornamos un valor aleatorio real
        } else { // Pero si el cehatRandom es mas pequeño o igual al cheatedProbabilityPercent
            return cheatedValue // Retornamos el valor 'trucado'
        }
    }
}

Cómo puedes ver en el ejemplo, este dado parece algo más complejo. Pero si lo vemos desde fuera parece un dado normal, ya que sus propiedades publicas (sides y roll()) siguen siendo las mismas y solo podríamos darnos cuenta que es un dado modificado si nos fijamos en su construcción, ya que ahora hay que pasarle algún parámetros: el numero que queremos «trucar» y el porcentaje de probabilidad para el mismo (opcional).

Es una implementación simplificada que sirve para ilustrar el ejemplo. Realmente con está implementación podríamos hacer que él dado devolviera valores que ni existen en sus caras. Además, he simplificado la lógica de probabilidades para no complicar el ejemplo demasiado.

Veamos como jugaríamos con este dado:

let myCheatedDice = CheatedDice(cheatedValue: 3)
let myHeavyCheatedDice = CheatedDice(cheatedValue: 5, cheatedProbabilityPercent: 80)

// lanzamos 4 veces cada dado para ver los resultados
print(myCheatedDice.roll())
print(myCheatedDice.roll())
print(myCheatedDice.roll())
print(myCheatedDice.roll())

print(myHeavyCheatedDice.roll())
print(myHeavyCheatedDice.roll())
print(myHeavyCheatedDice.roll())
print(myHeavyCheatedDice.roll())

¡Quiero más!

Por el momento terminamos aquí con el uso de tipos genéricos en protocolos con Swift. Con esto ya tenemos suficiente para aprender, jugar y divertirnos un buen rato. Aún así, más delante hare otro post, siguiendo con la temática de los dados, para aprender cómo utilizar estos protocolos genéricos en parámetros, por ejemplo. Así podremos crear un croupier que nos lance los dados que le digamos, e incluso que nos almacene un histórico con todos los resultados que hemos ido obteniendo y demás.

Podrás encontrar los ejemplos de este post en mi GitHub. Ver ejemplos

¡Si te ha gustado, y quieres más contenidos de este tipo…

Like y comparte, plis! 🙂

Deja un comentario