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 fatal
, 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 fatal Error(_ message: () -> String = String(),
file: Static String = #file,
line: UInt = #line)
Después del cambio, fatal
y sus similares se declararon para devolver el tipo Never
:
// Swift >= 3.0
func fatal Error(_ message: @autoclosure () -> String = String(),
file: Static String = #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 comofatal
, pero también es útil a la hora de trabajar con clases genéricas. Por ejemplo, un tipoError Result
podría usarNever
como suValue
para representar algo que siempre falla o usarNever
como suError
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 always Succeeds(_ 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:
always Succeeds { (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 first Iem = array.first!
Para evitar !
aquí, podríamos usar un guard
con asignación condicional:
let array: [Int]
guard let first Item = array.first else {
fatal Error("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 first Item = array.first ?? fatal Error("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 first Item = 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 never Throws() throws<Never> {
// ...
}
never Throws() // `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
.