Never

«Nunca» es un adverbio que indica que un evento no ocurre en ningún tiempo pasado o futuro. En el eje del tiempo constituye una imposibilidad lógica; la nada se extiende en todas direcciones. Para siempre.

… por eso es especialmente preocupante encontrarse este comentario en el código:

// esto nunca ocurrirá

Cualquier libro de texto sobre compiladores te dirá que un comentario como ese no puede ni afectará al comportamiento del código compilado. La Ley de Murphy dice lo contrario.

¿Cómo nos mantiene Swift a salvo de todo este caos impredecible que es la programación? Aunque no lo creas, la respuesta es: nada y crashing.


Se ha propuesto Never como sustituto del atributo @noreturn en SE-0102: “Eliminar el atributo @noreturn e introducir un tipo vacío Never”, por Joe Groff.

Antes de Swift 3, las funciones que detenían la ejecución, como fatalError(_:file:line:), abort() y exit(_:) se anotaban con el atributo @noreturn, el cual le indicaba al compilador que no existía un punto de retorno al ámbito del que hizo la llamada.

// Swift < 3.0
@noreturn func fatalError(_ message: () -> String = String(),
                               file: StaticString = #file,
                               line: UInt = #line)

Después del cambio, fatalError y sus similares se declararon para devolver el tipo Never:

// Swift >= 3.0
func fatalError(_ message: @autoclosure () -> String = String(),
                     file: StaticString = #file,
                     line: UInt = #line) -> Never

Podría pensarse que reemplazar la funcionalidad de una anotación con un tipo puede ser algo muy complejo, ¿verdad? ¡Al contrario! Never es de los tipos más simples de la librería estándar de Swift:

enum Never {}

Tipos vacíos

Never es un tipo vacío, lo que significa que no tiene valores. O dicho de otra forma, no puede construirse.

Enums sin casos son el ejemplo más común de tipos vacíos en Swift. Al contrario que Structs y clases, los enum no tienen inicializador. Y al contrario que los protocolos, son tipos concretos que pueden tener propiedades, métodos, restricciones genéricas y tipos anidados. Por todo ello se usan enum vacíos a lo largo de la librería estándar de Swift para cosas como la funcionalidad de namespaces y sobre tipos.

Pero Never no es así. No tiene ningún adorno especial. Su especial virtud es la de ser lo que es (o mejor dicho, lo que no).

Considera una función que devuelva un tipo vacío. Como los tipos vacíos no tienen ningún valor, la función no puede retornar de manera normal (¿qué retornaría?). En cambio, la función debe o parar su ejecución o ejecutarse indefinidamente.

Eliminando estados imposibles en tipos genéricos

Never es interesante teóricamente, desde luego. Pero, ¿qué aplicación práctica tiene? No mucha. O, al menos, no antes de que aceptaran SE-0215: Conformar Never a Equatable y Hashable.

En esta propuesta, Matt Diephouse explica la motivación que hay detrás de hacer que este oscuro tipo conforme Equatable y otros protocolos:

Never es muy útil a la hora de representar código imposible. La gran mayoría está familiarizada con este tipo como el tipo que devuelven funciones como fatalError, pero también es útil a la hora de trabajar con clases genéricas. Por ejemplo, un tipo Result podría usar Never como su Value para representar algo que siempre falla o usar Never como su Error para representar algo que nunca lo hace.

Swift no tiene un tipo estándar Result, pero la mayoría de ellos son similares a éste:

enum Result<Value, Error: Swift.Error> {
    case success(Value)
    case failure(Error)
}

Los tipos como Result se usan para encapsular valores y errores producidos por funciones que se ejecutan de manera asíncrona (funciones síncronas pueden usar throws para comunicar errores).

Por ejemplo, una función que hace una petición HTTP asíncrona podría usar un tipo Result para almacenar la respuesta o el error:

func fetch(_ request: Request, completion: (Result<Response, Error>) -> Void) {
    // ...
}

Cuando llamas a esa función, harías un switch sobre result para responder a .success o .failure de forma aislada:

fetch(request) { result in
    switch result {
    case .success(let value):
        print("Success: \(value)")
    case .failure(let error):
        print("Failure: \(error)")
    }
}

Ahora considera una función que devuelva siempre con éxito un resultado en su completion handler:

func alwaysSucceeds(_ completion: (Result<String, Never>) -> Void) {
    completion(.success("yes!"))
}

Al especificar Never como el tipo Error del resultado estamos usando el sistema de tipado para indicar que el fallo no es una opción. Y es genial que Swift sea lo suficientemente listo para saber que no hay que controlar el caso .failure del switch para que sea exhaustivo:

alwaysSucceeds { (result) in
    switch result {
    case .success(let string):
        print(string)
    }
}

Se puede ver este efecto aplicado al extremo en la implementación de Never conformando a Comparable:

extension Never: Comparable {
  public static func < (lhs: Never, rhs: Never) -> Bool {
    switch (lhs, rhs) {}
  }
}

Como Never es un tipo vacío no hay ningún posible valor de él, por lo que al hacer un switch sobre lhs y rhs, Swift entiende que no hay ningún caso faltante por controlar. Y como todos los casos (ninguno en este caso) devuelven Bool, el método compila sin problemas.

¡Genial!


Never como tipo de fondo

A modo de corolario, la propuesta original de Swift Evolution sobre Never insinúa la utilidad teórica del tipo con mejoras adicionales:

Un tipo vacío puede verse como un subtipo de cualquier otro tipo: Si evaluar una expresión no produce nunca un valor, no importa de qué tipo sea dicha expresión. Si el compilador soportase esto, permitiría cosas muy útiles.

Unwrap o muere

El operador de unwrap forzado(!) es una de las partes más controvérsicas de Swift. En el mejor de los casos, es un mal necesario. En el peor, es un code smell que sugiere descuido. Y sin información adicional puede ser realmente complicado diferenciar ambos casos.

Por ejemplo, considera el siguiente código que asume que un array no está vacío:

let array: [Int]
let firstIem = array.first!

Para evitar ! aquí, podríamos usar un guard con asignación condicional:

let array: [Int]
guard let firstItem = array.first else {
    fatalError("array cannot be empty")
}

Si, en el futuro, se implementa Never como un tipo de fondo, podría usarse en el lado derecho del operador de coalescencia de una expresión:

//Swift futuro? 🔮
let firstItem = array.first ?? fatalError("array cannot be empty")

Si te urge la necesidad de adoptar este patrón, puedes sobrecargar el operador ??:

func ?? <T>(lhs: T?, rhs: @autoclosure () -> Never) -> T {
    switch lhs {
    case let value?:
        return value
    case nil:
        rhs()
    }
}

Sin embargo…

En la base lógica de SE-0217: Introduciendo el operador !! “Unwrap o Muere” a la Librería Estándar de Swift, Joe Groff señala que “[…] sobrecargar [?? usando Never] tiene un impacto inaceptable en el rendimiento de la comprobación de tipos…”. Por tanto, no se recomienda que lo hagas en tu código.

Throw como expresión

De igual forma, si se cambia throw para que sea una expresión que devuelve Never, podría usarse throw en el lado derecho de ??:

// Swift futuro? 🔮
let firstItem = array.first ?? throw Error.empty

Throws tipados

Mirando incluso más allá: si la palabra clave throw soportase restricciones de tipo en la declaración de funciones, el tipo Never podría usarse para indicar que una función no hará throw (igual que el ejemplo anterior sobre Result):

// Swift futuro? 🔮
func neverThrows() throws<Never> {
    // ...
}

neverThrows() // `try` innecesario porque se garantiza el éxito (quizá)

Afirmar que algo nunca ocurrirá es como invitar al Universo a probar lo contrario. Mientras que la lógica modal o doxástica permite un salvoconducto para guardar las apariencias (“era cierto en aquel momento, ¡por eso lo creí!”), la lógica temporal parece estar a un nivel superior a la hora de sostener una afirmación.

Por suerte para nosotros, Swift sostiene esa afirmación gracias al más improbable de los tipos: Never.

Escrito por Mattt
Mattt

Mattt (@mattt) is a writer and developer in Portland, Oregon. He is the founder of NSHipster and Flight School, and the creator of several open source libraries, including AFNetworking and Alamofire.

Traducido por Juan F. Sagasti
Juan F. Sagasti

Juan F. Sagasti es ingeniero de software y cofundador en Theam. Colabora como traductor principal en español de NSHipster. Vive con su gata Lily en una bonita isla del Atlántico llamada Gran Canaria.

Artículo siguiente

El Machine learning (Aprendizaje Automático) ha sido el núcleo del procesamiento del lenguaje natural en las plataformas de Apple durante mucho tiempo, pero ha sido recientemente cuando los desarrolladores externos han tenido acceso para su aprovechamiento.