Programación y programación orientada a protocolos. genéricos

Ver en TensorFlow.org Ver fuente en GitHub

Este tutorial repasará la programación orientada a protocolos y diferentes ejemplos de cómo se pueden usar con genéricos en ejemplos del día a día.

Protocolos

La herencia es una forma poderosa de organizar el código en lenguajes de programación que le permite compartir código entre múltiples componentes del programa.

En Swift, hay diferentes formas de expresar la herencia. Es posible que ya esté familiarizado con una de esas formas, de otros lenguajes: la herencia de clases. Sin embargo, Swift tiene otra forma: los protocolos.

En este tutorial, exploraremos los protocolos: una alternativa a la subclasificación que le permite lograr objetivos similares a través de diferentes compensaciones. En Swift, los protocolos contienen varios miembros abstractos. Las clases, estructuras y enumeraciones pueden ajustarse a múltiples protocolos y la relación de conformidad puede establecerse retroactivamente. Todo eso permite algunos diseños que no son fácilmente expresables en Swift usando subclases. Veremos los modismos que respaldan el uso de protocolos (extensiones y restricciones de protocolo), así como las limitaciones de los protocolos.

¡Tipos de valor de Swift 💖!

Además de las clases que tienen semántica de referencia, Swift admite enumeraciones y estructuras que se pasan por valor. Las enumeraciones y las estructuras admiten muchas funciones proporcionadas por las clases. ¡Vamos a ver!

En primer lugar, veamos cómo las enumeraciones son similares a las clases:

enum Color: String {
    case red = "red"
    case green = "green"
    case blue = "blue"
    // A computed property. Note that enums cannot contain stored properties.
    var hint: String {
        switch self {
            case .red:
                return "Roses are this color."
            case .green:
                return "Grass is this color."
            case .blue:
                return "The ocean is this color."
        }
    }

    // An initializer like for classes.
    init?(color: String) {
        switch color {
        case "red":
            self = .red
        case "green":
            self = .green
        case "blue":
            self = .blue
        default:
            return nil
        }
    }
}

// Can extend the enum as well!
extension Color {
    // A function.
    func hintFunc() -> String {
        return self.hint
    }
}

let c = Color.red
print("Give me a hint for c: \(c.hintFunc())")

let invalidColor = Color(color: "orange")
print("is invalidColor nil: \(invalidColor == nil)")
Give me a hint for c: Roses are this color.
is invalidColor nil: true

Ahora, veamos las estructuras. Tenga en cuenta que no podemos heredar estructuras, sino que podemos usar protocolos:

struct FastCar {
    // Can have variables and constants as stored properties.
    var color: Color
    let horsePower: Int
    // Can have computed properties.
    var watts: Float {
       return Float(horsePower) * 745.7
    }
    // Can have lazy variables like in classes!
    lazy var titleCaseColorString: String = {
        let colorString = color.rawValue
        return colorString.prefix(1).uppercased() + 
               colorString.lowercased().dropFirst()
    }()
    // A function.
    func description() -> String {
        return "This is a \(color) car with \(horsePower) horse power!"
    }
    // Can create a variety of initializers.
    init(color: Color, horsePower: Int) {
        self.color = color
        self.horsePower = horsePower
    }
    // Can define extra initializers other than the default one.
    init?(color: String, horsePower: Int) {
        guard let enumColor = Color(color: color) else {
            return nil
        }
        self.color = enumColor
        self.horsePower = horsePower
    }
}

var car = FastCar(color: .red, horsePower: 250)
print(car.description())
print("Horse power in watts: \(car.watts)")
print(car.titleCaseColorString)
This is a red car with 250 horse power!
Horse power in watts: 186425.0
Red

Finalmente, veamos cómo se pasan por tipos de valor a diferencia de las clases:

// Notice we have no problem modifying a constant class with 
// variable properties.
class A {
  var a = "a"
}

func foo(_ a: A) {
  a.a = "foo"
}
let a = A()
print(a.a)
foo(a)
print(a.a)

/* 
Uncomment the following code to see how an error is thrown.
Structs are implicitly passed by value, so we cannot modify it.
> "error: cannot assign to property: 'car' is a 'let' constant"
*/

// func modify(car: FastCar, toColor color: Color) -> Void {
//   car.color = color
// }

// car = FastCar(color: .red, horsePower: 250)
// print(car.description())
// modify(car: &car, toColor: .blue)
// print(car.description())
a
foo

Usemos protocolos

Comencemos por crear protocolos para diferentes autos:

protocol Car {
    var color: Color { get set }
    var price: Int { get }
    func turnOn()
    mutating func drive()
}

protocol Electric {
    mutating func recharge()
    // percentage of the battery level, 0-100%.
    var batteryLevel: Int { get set }
}

protocol Gas {
    mutating func refill()
    // # of liters the car is holding, varies b/w models.
    var gasLevelLiters: Int { get set }
}

En un mundo orientado a objetos (sin herencia múltiple), es posible que haya creado clases abstractas Electric y Gas , luego haya usado la herencia de clases para hacer que ambas hereden de Car y luego tener un modelo de automóvil específico como clase base. Sin embargo, ¡aquí ambos son protocolos completamente separados con acoplamiento cero ! Esto hace que todo el sistema sea más flexible a la hora de diseñarlo.

Definamos un Tesla:

struct TeslaModelS: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

var tesla = TeslaModelS(color: .red, price: 110000, batteryLevel: 100)

Esto especifica una nueva estructura TeslaModelS que se ajusta a los protocolos Car y Electric .

Ahora definamos un automóvil a gasolina:

struct Mustang: Car, Gas{
    var color: Color
    let price: Int
    var gasLevelLiters: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Time to drive!")
        gasLevelLiters -= 1
    }

    mutating func refill() {
        print("Filling the tank...")
        gasLevelLiters = 25
    }
}

var mustang = Mustang(color: .red, price: 30000, gasLevelLiters: 25)

Ampliar protocolos con comportamientos predeterminados

Lo que puede notar de los ejemplos es que tenemos cierta redundancia. Cada vez que recargamos un automóvil eléctrico, debemos establecer el nivel de porcentaje de la batería en 100. Dado que todos los automóviles eléctricos tienen una capacidad máxima del 100 %, pero los automóviles de gasolina varían según la capacidad del tanque de gasolina, podemos establecer el nivel predeterminado en 100 para los automóviles eléctricos. .

Aquí es donde las extensiones en Swift pueden ser útiles:

extension Electric {
    mutating func recharge() {
        print("Recharging the battery...")
        batteryLevel = 100
    }
}

Así que ahora, cualquier auto eléctrico nuevo que creemos establecerá la batería a 100 cuando lo recarguemos. Por lo tanto, acabamos de poder decorar clases, estructuras y enumeraciones con un comportamiento único y predeterminado.

Cómic de protocolo

¡Gracias a Ray Wenderlich por el cómic!

Sin embargo, una cosa a tener en cuenta es lo siguiente. En nuestra primera implementación, definimos foo() como una implementación predeterminada en A , pero no lo exigimos en el protocolo. Entonces, cuando llamamos a.foo() , obtenemos " A default " impreso.

protocol Default {}

extension Default {
    func foo() { print("A default")}
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
A default

Sin embargo, si hacemos que foo() sea obligatorio en A , obtenemos " Inst ":

protocol Default {
    func foo()
}

extension Default {
    func foo() { 
        print("A default")
    }
}

struct DefaultStruct: Default {
    func foo() {
        print("Inst")
    }
}

let a: Default = DefaultStruct()
a.foo()
Inst

Esto ocurre debido a una diferencia entre el envío estático en el primer ejemplo y el envío estático en el segundo en los protocolos de Swift. Para obtener más información, consulte esta publicación de Medium .

Anular el comportamiento predeterminado

Sin embargo, si queremos, aún podemos anular el comportamiento predeterminado. Una cosa importante a tener en cuenta es que esto no es compatible con el envío dinámico .

Digamos que tenemos una versión anterior de un automóvil eléctrico, por lo que la salud de la batería se ha reducido al 90%:

struct OldElectric: Car, Electric {
    var color: Color // Needs to be a var since `Car` has a getter and setter.
    let price: Int
    var batteryLevel: Int

    func turnOn() {
        print("Starting all systems!")
    }

    mutating func drive() {
        print("Self driving engaged!")
        batteryLevel -= 8
    }

    mutating func reCharge() {
        print("Recharging the battery...")
        batteryLevel = 90
    }
}

Usos estándar de la biblioteca de protocolos

Ahora que tenemos una idea de cómo funcionan los protocolos en Swift, veamos algunos ejemplos típicos del uso de los protocolos de la biblioteca estándar.

Ampliar la biblioteca estándar

Veamos cómo podemos agregar funcionalidad adicional a los tipos que ya existen en Swift. Dado que los tipos en Swift no están integrados, pero son parte de la biblioteca estándar como estructuras, esto es fácil de hacer.

Intentemos hacer una búsqueda binaria en una matriz de elementos, mientras nos aseguramos de verificar que la matriz esté ordenada:

extension Collection where Element: Comparable {
    // Verify that a `Collection` is sorted.
    func isSorted(_ order: (Element, Element) -> Bool) -> Bool {
        var i = index(startIndex, offsetBy: 1)

        while i < endIndex {
            // The longer way of calling a binary function like `<(_:_:)`, 
            // `<=(_:_:)`, `==(_:_:)`, etc.
            guard order(self[index(i, offsetBy: -1)], self[i]) else {
                return false
            }
            i = index(after: i)
        }
        return true
    }

    // Perform binary search on a `Collection`, verifying it is sorted.
    func binarySearch(_ element: Element) -> Index? {
        guard self.isSorted(<=) else {
            return nil
        }

        var low = startIndex
        var high = endIndex

        while low <= high {
            let mid = index(low, offsetBy: distance(from: low, to: high)/2)

            if self[mid] == element {
                return mid
            } else if self[mid] < element {
                low = index(after: mid)
            } else {
                high = index(mid, offsetBy: -1)
            }
        }

        return nil
    }
}

print([2, 2, 5, 7, 11, 13, 17].binarySearch(5)!)
print(["a", "b", "c", "d"].binarySearch("b")!)
print([1.1, 2.2, 3.3, 4.4, 5.5].binarySearch(3.3)!)
2
1
2

Hacemos esto extendiendo el protocolo de Collection que define "una secuencia cuyos elementos se pueden recorrer varias veces, de forma no destructiva, y se puede acceder a ellos mediante un subíndice indexado". Dado que las matrices se pueden indexar utilizando la notación de corchetes, este es el protocolo que queremos ampliar.

De manera similar, solo queremos agregar esta función de utilidad a las matrices cuyos elementos se pueden comparar. Esta es la razón por la que tenemos where Element: Comparable .

La cláusula where es parte del sistema de tipos de Swift, que cubriremos pronto, pero en resumen nos permite agregar requisitos adicionales a la extensión que estamos escribiendo, como requerir que el tipo implemente un protocolo, requerir que dos tipos sean el mismo, o para requerir que una clase tenga una superclase particular.

Element es el tipo asociado de los elementos en un tipo conforme a la Collection . El Element se define dentro del protocolo Sequence , pero dado que Collection hereda de Sequence , hereda el tipo asociado Element .

Comparable es un protocolo que define "un tipo que se puede comparar mediante los operadores relacionales < , <= , >= y > ". . Dado que estamos realizando una búsqueda binaria en una Collection ordenada, esto, por supuesto, tiene que ser cierto o, de lo contrario, no sabemos si repetir/iterar hacia la izquierda o hacia la derecha en la búsqueda binaria.

Como nota al margen sobre la implementación, para obtener más información sobre la función index(_:offsetBy:) que se usó, consulte la siguiente documentación .

Genéricos + protocolos = 💥

Los genéricos y los protocolos pueden ser una herramienta poderosa si se usan correctamente para evitar la duplicación de código.

En primer lugar, revise otro tutorial, A Swift Tour , que cubre brevemente los genéricos al final del libro de Colab.

Suponiendo que tenga una idea general sobre los genéricos, echemos un vistazo rápido a algunos usos avanzados.

Cuando un solo tipo tiene varios requisitos, como un tipo que se ajusta a varios protocolos, tiene varias opciones a su disposición:

typealias ComparableReal = Comparable & FloatingPoint

func foo1<T: ComparableReal>(a: T, b: T) -> Bool {
    return a > b
}

func foo2<T: Comparable & FloatingPoint>(a: T, b: T) -> Bool {
    return a > b
}

func foo3<T>(a: T, b: T) -> Bool where T: ComparableReal {
    return a > b
}

func foo4<T>(a: T, b: T) -> Bool where T: Comparable & FloatingPoint {
    return a > b
}

func foo5<T: FloatingPoint>(a: T, b: T) -> Bool where T: Comparable {
    return a > b
}

print(foo1(a: 1, b: 2))
print(foo2(a: 1, b: 2))
print(foo3(a: 1, b: 2))
print(foo4(a: 1, b: 2))
print(foo5(a: 1, b: 2))
false
false
false
false
false

Observe el uso de typealias en la parte superior. Esto agrega un alias con nombre de un tipo existente en su programa. Después de declarar un alias de tipo, el nombre con alias se puede usar en lugar del tipo existente en todas partes de su programa. Los alias de tipo no crean nuevos tipos; simplemente permiten que un nombre se refiera a un tipo existente.

Ahora, veamos cómo podemos usar protocolos y genéricos juntos.

Imaginemos que somos una tienda de computadoras con los siguientes requisitos en cualquier computadora portátil que vendemos para determinar cómo las organizamos en la parte trasera de la tienda:

enum Box {
    case small
    case medium
    case large
}

enum Mass {
    case light
    case medium
    case heavy
}

// Note: `CustomStringConvertible` protocol lets us pretty-print a `Laptop`.
struct Laptop: CustomStringConvertible {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

Sin embargo, tenemos un nuevo requisito de agrupar nuestras Laptop por masa ya que los estantes tienen restricciones de peso.

func filtering(_ laptops: [Laptop], by mass: Mass) -> [Laptop] {
    return laptops.filter { $0.mass == mass }
}

let laptops: [Laptop] = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

let filteredLaptops = filtering(laptops, by: .light)
print(filteredLaptops)
[(a small light), (d large light)]

Sin embargo, ¿qué pasaría si quisiéramos filtrar por algo que no sea Mass ?

Una opción es hacer lo siguiente:

// Define a protocol which will act as our comparator.
protocol DeviceFilterPredicate {
    associatedtype Device
    func shouldKeep(_ item: Device) -> Bool
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter: DeviceFilterPredicate {
    typealias Device = Laptop
    var box: Box 

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.box == box
    }
}

struct MassFilter: DeviceFilterPredicate {
    typealias Device = Laptop  
    var mass: Mass

    func shouldKeep(_ item: Laptop) -> Bool {
        return item.mass == mass
    }
}

// Make sure our filter conforms to `DeviceFilterPredicate` and that we are 
// filtering `Laptop`s.
func filtering<F: DeviceFilterPredicate>(
    _ laptops: [Laptop], 
    by filter: F
) -> [Laptop] where Laptop == F.Device {
    return laptops.filter { filter.shouldKeep($0) }
}

// Let's test the function out!
print(filtering(laptops, by: BoxFilter(box: .large)))
print(filtering(laptops, by: MassFilter(mass: .heavy)))
[(b large medium), (d large light)]
[(c medium heavy)]

¡Impresionante! Ahora podemos filtrar en función de cualquier restricción de computadora portátil. Sin embargo, solo podemos filtrar Laptop .

¿Qué hay de poder filtrar cualquier cosa que esté en una caja y tenga masa? Tal vez este almacén de portátiles también se utilice para servidores que tienen una base de clientes diferente:

// Define 2 new protocols so we can filter anything in a box and which has mass.
protocol Weighable {
    var mass: Mass { get }
}

protocol Boxed {
    var box: Box { get }
}

// Define the new Laptop and Server struct which have mass and a box.
struct Laptop: CustomStringConvertible, Boxed, Weighable {
    var name: String
    var box: Box
    var mass: Mass

    var description: String {
        return "(\(self.name) \(self.box) \(self.mass))"
    }
}

struct Server: CustomStringConvertible, Boxed, Weighable {
    var isWorking: Bool
    var name: String
    let box: Box
    let mass: Mass

    var description: String {
        if isWorking {
            return "(working \(self.name) \(self.box) \(self.mass))"
        } else {
            return "(notWorking \(self.name) \(self.box) \(self.mass))"
        }
    }
}

// Define the structs we will use for passing into our filtering function.
struct BoxFilter<T: Boxed>: DeviceFilterPredicate {
    var box: Box 

    func shouldKeep(_ item: T) -> Bool {
        return item.box == box
    }
}

struct MassFilter<T: Weighable>: DeviceFilterPredicate {
    var mass: Mass

    func shouldKeep(_ item: T) -> Bool {
        return item.mass == mass
    }
}

// Define the new filter function.
func filtering<F: DeviceFilterPredicate, T>(
    _ elements: [T], 
    by filter: F
) -> [T] where T == F.Device {
    return elements.filter { filter.shouldKeep($0) }
}


// Let's test the function out!
let servers = [
    Server(isWorking: true, name: "serverA", box: .small, mass: .heavy),
    Server(isWorking: false, name: "serverB", box: .medium, mass: .medium),
    Server(isWorking: true, name: "serverC", box: .large, mass: .light),
    Server(isWorking: false, name: "serverD", box: .medium, mass: .light),
    Server(isWorking: true, name: "serverE", box: .small, mass: .heavy)
]

let products = [
    Laptop(name: "a", box: .small, mass: .light),
    Laptop(name: "b", box: .large, mass: .medium),
    Laptop(name: "c", box: .medium, mass: .heavy),
    Laptop(name: "d", box: .large, mass: .light)
]

print(filtering(servers, by: BoxFilter(box: .small)))
print(filtering(servers, by: MassFilter(mass: .medium)))

print(filtering(products, by: BoxFilter(box: .small)))
print(filtering(products, by: MassFilter(mass: .medium)))
[(working serverA small heavy), (working serverE small heavy)]
[(notWorking serverB medium medium)]
[(a small light)]
[(b large medium)]

¡Ahora hemos podido filtrar una matriz no solo por cualquier propiedad de una struct específica, sino también por cualquier estructura que tenga esa propiedad!

Consejos para un buen diseño de API

Esta sección se tomó de la charla WWDC 2019: Modern Swift API Design .

Ahora que comprende cómo se comportan los protocolos, es mejor repasar cuándo debe usar los protocolos. Tan poderosos como pueden ser los protocolos, no siempre es la mejor idea sumergirse y comenzar de inmediato con los protocolos.

  • Comience con casos de uso concretos:
    • Primero explore el caso de uso con tipos concretos y comprenda qué código desea compartir y encontrar que se está repitiendo. Luego, elimine ese código compartido con los genéricos. Podría significar crear nuevos protocolos. Descubrir una necesidad de código genérico.
  • Considere la posibilidad de componer nuevos protocolos a partir de protocolos existentes definidos en la biblioteca estándar. Consulte la siguiente documentación de Apple para ver un buen ejemplo de esto.
  • En lugar de un protocolo genérico, considere definir un tipo genérico.

Ejemplo: definir un tipo de vector personalizado

Digamos que queremos definir un protocolo GeometricVector en números de coma flotante para usar en alguna aplicación de geometría que estamos creando que define 3 operaciones vectoriales importantes:

protocol GeometricVector {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

Digamos que queremos almacenar las dimensiones del vector, con lo que el protocolo SIMD nos puede ayudar, así que haremos que nuestro nuevo tipo refine el protocolo SIMD . Los vectores SIMD se pueden considerar como vectores de tamaño fijo que son muy rápidos cuando los usa para realizar operaciones con vectores:

protocol GeometricVector: SIMD {
    associatedtype Scalar: FloatingPoint
    static func dot(_ a: Self, _ b: Self) -> Scalar
    var length: Scalar { get }
    func distance(to other: Self) -> Scalar
}

Ahora, definamos las implementaciones predeterminadas de las operaciones anteriores:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

Y luego necesitamos agregar una conformidad a cada uno de los tipos que queremos agregar estas habilidades:

extension SIMD2: GeometricVector where Scalar: FloatingPoint { }
extension SIMD3: GeometricVector where Scalar: FloatingPoint { }
extension SIMD4: GeometricVector where Scalar: FloatingPoint { }
extension SIMD8: GeometricVector where Scalar: FloatingPoint { }
extension SIMD16: GeometricVector where Scalar: FloatingPoint { }
extension SIMD32: GeometricVector where Scalar: FloatingPoint { }
extension SIMD64: GeometricVector where Scalar: FloatingPoint { }

Este proceso de tres pasos de definir el protocolo, darle una implementación predeterminada y luego agregar una conformidad a múltiples tipos es bastante repetitivo.

¿Era necesario el protocolo?

El hecho de que ninguno de los tipos de SIMD tenga implementaciones únicas es una señal de advertencia. Entonces, en este caso, el protocolo realmente no nos está dando nada.

Definirlo en una extensión de SIMD

Si escribimos los 3 operadores en una extensión del protocolo SIMD , esto puede resolver el problema de forma más sucinta:

extension SIMD where Scalar: FloatingPoint {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a * b).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

Usando menos líneas de código, agregamos todas las implementaciones predeterminadas a todos los tipos de SIMD .

A veces puede verse tentado a crear esta jerarquía de tipos, pero recuerde que no siempre es necesario. Esto también significa que el tamaño binario de su programa compilado será más pequeño y su código será más rápido de compilar.

Sin embargo, este enfoque de extensión es ideal para cuando tiene algunos métodos que desea agregar. Sin embargo, se encuentra con un problema de escalabilidad cuando está diseñando una API más grande.

¿Es un? ¿Tiene un?

Anteriormente dijimos que GeometricVector refinaría SIMD . Pero, ¿es esta una relación es-una? El problema es que SIMD define operaciones que nos permiten agregar un escalar 1 a un vector, pero no tiene sentido definir tal operación en el contexto de la geometría.

Por lo tanto, tal vez sería mejor una relación "tiene un" envolviendo SIMD en un nuevo tipo genérico que pueda manejar cualquier número de punto flotante:

// NOTE: `Storage` is the underlying type that is storing the values, 
// just like in a `SIMD` vector.
struct GeometricVector<Storage: SIMD> where Storage.Scalar: FloatingPoint {
    typealias Scalar = Storage.Scalar
    var value: Storage
    init(_ value: Storage) { self.value = value }
}

Entonces podemos tener cuidado y solo definir las operaciones que tienen sentido solo en el contexto de la geometría:

extension GeometricVector {
    static func + (a: Self, b: Self) -> Self {
        Self(a.value + b.value)
    }

    static func - (a: Self, b: Self) -> Self {
        Self(a.value - b.value)
    }
    static func * (a: Self, b: Scalar) -> Self {
        Self(a.value * b)
    }
}

Y aún podemos usar extensiones genéricas para obtener los 3 operadores anteriores que queríamos implementar y que se ven casi exactamente igual que antes:

extension GeometricVector {
    static func dot(_ a: Self, _ b: Self) -> Scalar {
        (a.value * b.value).sum()
    }

    var length: Scalar {
        Self.dot(self, self).squareRoot()
    }

    func distance(to other: Self) -> Scalar {
        (self - other).length
    }
}

En general, hemos podido refinar el comportamiento de nuestras tres operaciones a un tipo simplemente usando una estructura. Con los protocolos, enfrentamos el problema de escribir conformidades repetitivas en todos los vectores SIMD , y tampoco pudimos evitar que ciertos operadores como Scalar + Vector estuvieran disponibles (que en este caso no queríamos). Como tal, recuerde que los protocolos no son una solución completa y final. Pero a veces las soluciones más tradicionales pueden resultar más poderosas.

Más recursos de programación orientados a protocolos

Aquí hay recursos adicionales sobre los temas discutidos:

  • WWDC 2015: Programación orientada a protocolos en Swift : esto se presentó usando Swift 2, por lo que muchas cosas han cambiado desde entonces (por ejemplo, el nombre de los protocolos que usaron en la presentación), pero este sigue siendo un buen recurso para la teoría y los usos detrás de él. .
  • Introducción a la programación orientada a protocolos en Swift 3 : esto se escribió en Swift 3, por lo que es posible que se deba modificar parte del código para que se compile correctamente, pero es otro gran recurso.
  • WWDC 2019: Modern Swift API Design : repasa las diferencias entre los tipos de valor y referencia, un caso de uso de cuando los protocolos pueden resultar ser la peor opción en el diseño de API (igual que la sección "Consejos para un buen diseño de API" anterior), clave búsqueda de miembros de rutas y contenedores de propiedades.
  • Genéricos : documentación propia de Swift para Swift 5 sobre genéricos.