De vez en cuando, cuando los programadores se quejan de errores nulos / excepciones, alguien pregunta qué hacemos sin nulos.
Tengo una idea básica de la genialidad de los tipos de opciones, pero no tengo el conocimiento o la habilidad de los idiomas para expresarlo mejor. ¿Cuál es una gran explicación de lo siguiente escrita de una manera accesible para el programador promedio al que podríamos apuntar a esa persona?
- La indeseabilidad de que las referencias / punteros sean anulables por defecto
- Cómo funcionan los tipos de opciones, incluidas las estrategias para facilitar la comprobación de casos nulos, como
- coincidencia de patrones y
- comprensiones monádicas
- Solución alternativa como mensaje comiendo nulo
- (otros aspectos me perdí)
programming-languages
functional-programming
null
nullpointerexception
non-nullable
Roman A. Taycher
fuente
fuente
Respuestas:
Creo que el resumen sucinto de por qué nulo es indeseable es que los estados sin sentido no deberían ser representables .
Supongamos que estoy modelando una puerta. Puede estar en uno de tres estados: abierto, cerrado pero desbloqueado, y cerrado y bloqueado. Ahora podría modelarlo siguiendo las líneas de
y está claro cómo mapear mis tres estados en estas dos variables booleanas. Pero esto deja un cuarto estado no deseado disponibles:
isShut==false && isLocked==true
. Debido a que los tipos que he seleccionado como mi representación admiten este estado, debo hacer un esfuerzo mental para asegurar que la clase nunca entre en este estado (tal vez codificando explícitamente una invariante). Por el contrario, si estuviera usando un lenguaje con tipos de datos algebraicos o enumeraciones verificadas que me permite definirentonces podría definir
Y no hay más preocupaciones. El sistema de tipos garantizará que solo haya tres estados posibles para una instancia
class Door
. Esto es en lo que son buenos los sistemas de tipos, descartando explícitamente toda una clase de errores en tiempo de compilación.El problema con
null
es que cada tipo de referencia obtiene este estado adicional en su espacio que normalmente no es deseado. Unastring
variable podría ser cualquier secuencia de caracteres, o podría ser este loconull
valor extra que no se asigna a mi dominio problemático. UnTriangle
objeto tiene tresPoint
s, que ellos mismos tienenX
yY
valores, pero desafortunadamente elPoint
s o elTriangle
mismo podría ser este loco valor nulo que no tiene sentido para el dominio de gráficos en el que estoy trabajando. Etc.Cuando tiene la intención de modelar un valor posiblemente inexistente, entonces debe optar por él explícitamente. Si la forma en que pretendo modelar a las personas es que cada
Person
una tiene aFirstName
y aLastName
, pero solo algunas personas tienenMiddleName
s, entonces me gustaría decir algo comodonde
string
aquí se supone que es un tipo no anulable. Entonces no hay invariantes difíciles de establecer y no hayNullReferenceException
s inesperados al tratar de calcular la longitud del nombre de alguien. El sistema de tipos asegura que cualquier código que se ocupe de lasMiddleName
cuentas tiene la posibilidad de serloNone
, mientras que cualquier código que se ocupe deFirstName
eso puede asumir con seguridad que hay un valor allí.Entonces, por ejemplo, usando el tipo anterior, podríamos crear esta función tonta:
sin preocupaciones Por el contrario, en un lenguaje con referencias anulables para tipos como string, luego suponiendo
terminas creando cosas como
que explota si el objeto Person entrante no tiene la invariante de que todo sea no nulo, o
o tal vez
asumiendo que eso
p
asegura que primero / último estén allí, pero el medio puede ser nulo, o tal vez haga controles que arrojen diferentes tipos de excepciones, o quién sabe qué. Todas estas locas opciones de implementación y cosas en las que pensar surgen porque existe este estúpido valor representable que no quieres ni necesitas.Nulo típicamente agrega complejidad innecesaria. La complejidad es el enemigo de todo software, y debe esforzarse por reducir la complejidad siempre que sea razonable.
(Tenga en cuenta que hay incluso más complejidad para incluso estos ejemplos simples. Incluso si un
FirstName
no puede sernull
, unstring
puede representar""
(la cadena vacía), que probablemente tampoco sea un nombre de persona que pretendemos modelar. Como tal, incluso con cadenas anulables, aún podría ser el caso de que estamos "representando valores sin sentido". Una vez más, puede optar por combatir esto ya sea a través de invariantes y código condicional en tiempo de ejecución, o mediante el uso del sistema de tipos (por ejemplo, para tener unNonEmptyString
tipo). esto último es quizás desaconsejado (los tipos "buenos" a menudo se "cierran" en un conjunto de operaciones comunes, y p. ej.NonEmptyString
no se cierra.SubString(0,0)
), pero demuestra más puntos en el espacio de diseño. Al final del día, en cualquier sistema de tipo dado, hay cierta complejidad de la que será muy bueno deshacerse, y otra complejidad que es intrínsecamente más difícil de eliminar. La clave para este tema es que en casi todos los sistemas de tipos, el cambio de "referencias anulables por defecto" a "referencias no anulables por defecto" es casi siempre un cambio simple que hace que el sistema de tipos sea mucho mejor para combatir la complejidad y descartando ciertos tipos de errores y estados sin sentido. Por lo tanto, es una locura que tantos idiomas sigan repitiendo este error una y otra vez).fuente
Lo bueno de los tipos de opciones no es que sean opcionales. Es que todos los otros tipos no lo son .
A veces , necesitamos poder representar un tipo de estado "nulo". A veces tenemos que representar una opción "sin valor", así como los otros valores posibles que puede tomar una variable. Entonces, un lenguaje que no permite esto va a estar un poco paralizado.
Pero a menudo , no lo necesitamos, y permitir ese estado "nulo" solo conduce a la ambigüedad y la confusión: cada vez que accedo a una variable de tipo de referencia en .NET, tengo que considerar que puede ser nulo .
A menudo, en realidad nunca será nulo, porque el programador estructura el código para que nunca suceda. Pero el compilador no puede verificar eso, y cada vez que lo ve, tiene que preguntarse "¿puede esto ser nulo? ¿Necesito verificar si es nulo aquí?"
Idealmente, en los muchos casos donde nulo no tiene sentido, no debería permitirse .
Eso es difícil de lograr en .NET, donde casi todo puede ser nulo. Debe confiar en el autor del código al que llama para que sea 100% disciplinado y coherente y haya documentado claramente lo que puede y no puede ser nulo, o debe ser paranoico y verificar todo .
Sin embargo, si los tipos no son anulables por defecto , entonces no necesita verificar si son nulos o no. Sabes que nunca pueden ser nulos, porque el verificador de compilador / tipo lo hace cumplir por ti.
Y a continuación, sólo tenemos una puerta trasera para los raros casos en que hacemos necesidad de manejar un estado nulo. Entonces se puede usar un tipo de "opción". Luego permitimos nulo en los casos en que hemos tomado una decisión consciente de que necesitamos poder representar el caso "sin valor", y en cualquier otro caso, sabemos que el valor nunca será nulo.
Como otros han mencionado, en C # o Java, por ejemplo, nulo puede significar una de dos cosas:
El segundo significado tiene que ser preservado, pero el primero debe ser eliminado por completo. E incluso el segundo significado no debería ser el predeterminado. Es algo en lo que podemos optar si y cuando lo necesitamos . Pero cuando no necesitamos que algo sea opcional, queremos que el verificador de tipo garantice que nunca será nulo.
fuente
Todas las respuestas hasta ahora se centran en por qué
null
es algo malo y cómo es útil si un lenguaje puede garantizar que ciertos valores nunca serán nulos.Luego continúan sugiriendo que sería una buena idea si aplica la no anulabilidad para todos los valores, lo que se puede hacer si agrega un concepto como
Option
oMaybe
para representar tipos que pueden no tener siempre un valor definido. Este es el enfoque adoptado por Haskell.¡Todo es bueno! Pero no excluye el uso de tipos explícitamente nulos / no nulos para lograr el mismo efecto. ¿Por qué, entonces, la opción sigue siendo algo bueno? Después de todo, Scala soporta valores con valores nulos (es tiene que, para que pueda trabajar con bibliotecas de Java), pero los soportes
Options
así.P. ¿Cuáles son los beneficios más allá de poder eliminar por completo los valores nulos de un idioma?
A. composición
Si realiza una traducción ingenua de código nulo
al código de opciones
no hay mucha diferencia! Pero también es un forma terrible de usar Opciones ... Este enfoque es mucho más limpio:
O incluso:
Cuando comienzas a lidiar con la Lista de opciones, se pone aún mejor. Imagina que la lista
people
es en sí misma opcional:¿Como funciona esto?
El código correspondiente con controles nulos (o incluso elvis?: Operadores) sería dolorosamente largo. El verdadero truco aquí es la operación flatMap, que permite la comprensión anidada de Opciones y colecciones de una manera que los valores anulables nunca pueden alcanzar.
fuente
flatMap
se llamaría(>>=)
, el operador de "vinculación" para las mónadas. Así es, a Haskellers le gusta tanto hacerflatMap
ping a las cosas que lo ponemos en el logotipo de nuestro idioma.Option<T>
nunca, nunca sería nula. Lamentablemente, Scala está ehh, todavía está vinculado a Java :-) (Por otro lado, si Scala no jugó bien con Java, ¿quién lo usaría? Oo)Dado que la gente parece estar perdiendo:
null
es ambiguo.La fecha de nacimiento de Alice es
null
. Qué significa eso?La fecha de la muerte de Bob es
null
. Qué significa eso?Una interpretación "razonable" podría ser que la fecha de nacimiento de Alice existe pero es desconocida, mientras que la fecha de muerte de Bob no existe (Bob todavía está vivo). Pero, ¿por qué llegamos a diferentes respuestas?
Otro problema:
null
es un caso extremo.null = null
?nan = nan
?inf = inf
?+0 = -0
?+0/0 = -0/0
?Las respuestas son generalmente "sí", "no", "sí", "sí", "no", "sí" respectivamente. Los "matemáticos" locos llaman a NaN "nulidad" y dicen que se compara igual a sí mismo. SQL trata los valores nulos como no iguales a nada (por lo que se comportan como NaN). Uno se pregunta qué sucede cuando intenta almacenar ± ∞, ± 0 y NaN en la misma columna de la base de datos (hay 2 53 NaN, la mitad de los cuales son "negativos").
Para empeorar las cosas, las bases de datos difieren en la forma en que tratan NULL, y la mayoría de ellas no son consistentes (vea Manejo de NULL en SQLite para obtener una descripción general). Es bastante horrible
Y ahora para la historia obligatoria:
Recientemente diseñé una tabla de base de datos (sqlite3) con cinco columnas
a NOT NULL, b, id_a, id_b NOT NULL, timestamp
. Debido a que es un esquema genérico diseñado para resolver un problema genérico para aplicaciones bastante arbitrarias, existen dos restricciones de unicidad:id_a
solo existe para la compatibilidad con el diseño de una aplicación existente (en parte porque no he encontrado una solución mejor) y no se usa en la nueva aplicación. Debido a la forma NULL funciona en SQL, puedo insertar(1, 2, NULL, 3, t)
y(1, 2, NULL, 4, t)
sin violar la primera restricción de unicidad (porque(1, 2, NULL) != (1, 2, NULL)
).Esto funciona específicamente debido a cómo funciona NULL en una restricción de unicidad en la mayoría de las bases de datos (presumiblemente, por lo que es más fácil modelar situaciones del "mundo real", por ejemplo, no hay dos personas que puedan tener el mismo Número de Seguro Social, pero no todas las personas tienen uno).
FWIW, sin invocar primero un comportamiento indefinido, las referencias de C ++ no pueden "señalar" nulas, y no es posible construir una clase con variables miembro de referencia no inicializadas (si se produce una excepción, la construcción falla).
Nota al margen: Ocasionalmente, es posible que desee punteros mutuamente excluyentes (es decir, solo uno de ellos puede ser no NULL), por ejemplo, en un iOS hipotético
type DialogState = NotShown | ShowingActionSheet UIActionSheet | ShowingAlertView UIAlertView | Dismissed
. En cambio, me veo obligado a hacer cosas comoassert((bool)actionSheet + (bool)alertView == 1)
.fuente
assert(actionSheet ^ alertView)
? ¿O no puede su idioma XOR bools?La indeseabilidad de tener referencias / punteros puede ser anulada por defecto.
No creo que este sea el problema principal con los nulos, el problema principal con los nulos es que pueden significar dos cosas:
Los lenguajes que admiten tipos de opciones también suelen prohibir o desalentar el uso de variables no inicializadas.
Cómo funcionan los tipos de opciones, incluidas las estrategias para facilitar la comprobación de casos nulos, como la coincidencia de patrones.
Para que sean efectivos, los tipos de opciones deben ser compatibles directamente en el idioma. De lo contrario, se necesita mucho código de placa de caldera para simularlos. La coincidencia de patrones y la inferencia de tipos son dos características clave del lenguaje que facilitan el trabajo con los tipos de opciones. Por ejemplo:
En F #:
Sin embargo, en un lenguaje como Java sin soporte directo para los tipos de Opción, tendríamos algo como:
Solución alternativa como mensaje comiendo nulo
El "mensaje comiendo cero" de Objective-C no es tanto una solución como un intento de aligerar el dolor de cabeza de la comprobación nula. Básicamente, en lugar de lanzar una excepción de tiempo de ejecución al intentar invocar un método en un objeto nulo, la expresión en su lugar se evalúa como nula. Suspendiendo la incredulidad, es como si cada método de instancia comenzara con
if (this == null) return null;
. Pero luego hay pérdida de información: no sabe si el método devuelve nulo porque es un valor de retorno válido o porque el objeto es realmente nulo. Es muy parecido a la deglución de excepciones, y no hace ningún progreso al abordar los problemas con nulo descrito anteriormente.fuente
La Asamblea nos trajo direcciones también conocidas como punteros sin tipo. C los asignó directamente como punteros mecanografiados, pero introdujo el nulo de Algol como un valor de puntero único, compatible con todos los punteros mecanografiados. El gran problema con nulo en C es que dado que cada puntero puede ser nulo, uno nunca puede usar un puntero de forma segura sin una verificación manual.
En lenguajes de nivel superior, tener nulo es incómodo ya que realmente transmite dos nociones distintas:
Tener variables indefinidas es bastante inútil y cede ante comportamientos indefinidos cada vez que ocurren. Supongo que todos estarán de acuerdo en que evitar las cosas indefinidas debe evitarse a toda costa.
El segundo caso es la opcionalidad y se proporciona mejor explícitamente, por ejemplo, con un tipo de opción .
Digamos que estamos en una empresa de transporte y necesitamos crear una aplicación para ayudar a crear un horario para nuestros conductores. Para cada conductor, almacenamos algunas informaciones como: las licencias de conducir que tienen y el número de teléfono para llamar en caso de emergencia.
En C podríamos tener:
Como observa, en cualquier procesamiento sobre nuestra lista de controladores tendremos que verificar si hay punteros nulos. El compilador no lo ayudará, la seguridad del programa depende de sus hombros.
En OCaml, el mismo código se vería así:
Digamos ahora que queremos imprimir los nombres de todos los conductores junto con sus números de licencia de camión.
C ª:
En OCaml eso sería:
Como puede ver en este ejemplo trivial, no hay nada complicado en la versión segura:
Mientras que en C, podrías haber olvidado un cheque nulo y boom ...
Nota: estos ejemplos de código no se compilaron, pero espero que tenga las ideas.
fuente
NULL
que en "referencia que puede no señalar a nada" se inventó para algún lenguaje Algol (Wikipedia está de acuerdo, ver en.wikipedia.org/wiki/Null_pointer#Null_pointer ). Pero, por supuesto, es probable que los programadores de ensamblaje inicialicen sus punteros a una dirección no válida (léase: Nulo = 0).Microsoft Research tiene un proyecto interesante llamado
Es una extensión C # con un tipo no nulo y un mecanismo para verificar que sus objetos no sean nulos , aunque, en mi humilde opinión, aplicar el principio de diseño por contrato puede ser más apropiado y más útil para muchas situaciones problemáticas causadas por referencias nulas.
fuente
Viniendo del fondo .NET, siempre pensé que null tenía un punto, es útil. Hasta que llegué a conocer las estructuras y lo fácil que era trabajar con ellas evitando mucho código repetitivo. Tony Hoare, que habló en QCon London en 2009, se disculpó por inventar la referencia nula . Para citarlo:
Vea esta pregunta también en programadores
fuente
Robert Nystrom ofrece un buen artículo aquí:
http://journal.stuffwithstuff.com/2010/08/23/void-null-maybe-and-nothing/
describiendo su proceso de pensamiento al agregar soporte para ausencia y falla a su lenguaje de programación Magpie .
fuente
Siempre he considerado Nulo (o nulo) como la ausencia de un valor .
A veces quieres esto, a veces no. Depende del dominio con el que esté trabajando. Si la ausencia es significativa: sin segundo nombre, entonces su aplicación puede actuar en consecuencia. Por otro lado, si el valor nulo no debería estar allí: el primer nombre es nulo, entonces el desarrollador recibe la llamada telefónica proverbial de las 2 am.
También he visto código sobrecargado y demasiado complicado con comprobaciones de nulo. Para mí, esto significa una de dos cosas:
a) un error más arriba en el árbol de aplicaciones
b) diseño incorrecto / incompleto
En el lado positivo: Nulo es probablemente una de las nociones más útiles para verificar si algo está ausente, y los lenguajes sin el concepto de nulo terminarán complicando las cosas cuando llegue el momento de validar los datos. En este caso, si no se inicializa una nueva variable, dichos idiomas generalmente establecerán las variables en una cadena vacía, 0 o una colección vacía. Sin embargo, si una cadena vacía o 0 o colección vacía son valores válidos para su aplicación, entonces tiene un problema.
A veces, esto se evita al inventar valores especiales / extraños para que los campos representen un estado no inicializado. Pero entonces, ¿qué sucede cuando un usuario bien intencionado ingresa el valor especial? Y no nos metamos en el lío que esto provocará con las rutinas de validación de datos. Si el lenguaje apoyara el concepto nulo, todas las preocupaciones desaparecerían.
fuente
Los lenguajes vectoriales a veces pueden salirse con la suya sin tener un valor nulo.
El vector vacío sirve como nulo escrito en este caso.
fuente