Considere esta una pregunta "académica". Me he estado preguntando acerca de cómo evitar NULL de vez en cuando y este es un ejemplo en el que no puedo encontrar una solución satisfactoria.
Supongamos que almaceno medidas donde en ocasiones se sabe que la medida es imposible (o que falta). Me gustaría almacenar ese valor "vacío" en una variable evitando NULL. Otras veces el valor puede ser desconocido. Entonces, teniendo las medidas para un cierto marco de tiempo, una consulta sobre una medida dentro de ese período de tiempo podría devolver 3 tipos de respuestas:
- La medición real en ese momento (por ejemplo, cualquier valor numérico incluido
0
) - Un valor "faltante" / "vacío" (es decir, se realizó una medición y se sabe que el valor está vacío en ese punto).
- Un valor desconocido (es decir, no se ha realizado ninguna medición en ese punto. Podría estar vacío, pero también podría ser cualquier otro valor).
Aclaración importante:
Suponiendo que tiene una función que get_measurement()
devuelve una de "vacío", "desconocido" y un valor de tipo "entero". Tener un valor numérico implica que se pueden realizar ciertas operaciones en el valor de retorno (multiplicación, división, ...) pero el uso de tales operaciones en NULL bloqueará la aplicación si no se detecta.
Me gustaría poder escribir código, evitando verificaciones NULL, por ejemplo (pseudocódigo):
>>> value = get_measurement() # returns `2`
>>> print(value * 2)
4
>>> value = get_measurement() # returns `Empty()`
>>> print(value * 2)
Empty()
>>> value = get_measurement() # returns `Unknown()`
>>> print(value * 2)
Unknown()
Tenga en cuenta que ninguna de las print
declaraciones causó excepciones (ya que no se utilizaron NULL). Por lo tanto, los valores vacíos y desconocidos se propagarían según sea necesario y la verificación de si un valor es realmente "desconocido" o "vacío" podría retrasarse hasta que sea realmente necesario (como almacenar / serializar el valor en algún lugar).
Nota al margen: La razón por la que me gustaría evitar los NULL es principalmente un desafío para la mente. Si quiero hacer cosas, no me opongo a usar NULL, pero descubrí que evitarlas puede hacer que el código sea mucho más robusto en algunos casos.
0
,[]
o{}
(el escalar 0, la lista vacía y el mapa vacío, respectivamente). Además, ese valor "perdido" / "desconocido" es básicamente exactamente para lo quenull
sirve: representa que podría haber un objeto allí, pero no lo hay.Respuestas:
La forma común de hacer esto, al menos con lenguajes funcionales es usar una unión discriminada. Este es entonces un valor que es uno de un int válido, un valor que denota "falta" o un valor que denota "desconocido". En F #, podría verse algo así como:
Un
Measurement
valor será entonces aReading
, con un valor int, o aMissing
, o anUnknown
con los datos sin procesar comovalue
(si es necesario).Sin embargo, si no está utilizando un lenguaje que admite uniones discriminadas, o su equivalente, es probable que este patrón no le sea de mucha utilidad. Entonces, podría usar, por ejemplo, una clase con un campo enum que denote cuál de los tres contiene los datos correctos.
fuente
std::variant
(y sus predecesores espirituales).Si aún no sabe qué es una mónada, hoy sería un gran día para aprender. Tengo una suave introducción para los programadores de OO aquí:
https://ericlippert.com/2013/02/21/monads-part-one/
Su escenario es una pequeña extensión de "tal vez mónada", también conocida como
Nullable<T>
en C # yOptional<T>
en otros lenguajes.Supongamos que tiene un tipo abstracto para representar la mónada:
y luego tres subclases:
Necesitamos una implementación de Bind:
A partir de esto, puede escribir esta versión simplificada de Bind:
Y ahora que has terminado. Tienes una
Measurement<int>
en la mano. Quieres duplicarlo:Y sigue la lógica; si
m
esEmpty<int>
entoncesasString
esEmpty<String>
excelente.Del mismo modo, si tenemos
y
entonces podemos combinar dos medidas:
y de nuevo, si
First()
es,Empty<int>
entoncesd
esEmpty<double>
y así sucesivamente.El paso clave es obtener la operación de enlace correcta . Piénsalo bien.
fuente
Null
conNullable
+ algún código repetitivo? :)Measurement<T>
es el tipo monádico.Creo que en este caso sería útil una variación en un patrón de objeto nulo:
Puede convertirlo en una estructura, anular Equals / GetHashCode / ToString, agregar conversiones implícitas desde o hacia
int
, y si desea un comportamiento similar a NaN, también puede implementar sus propios operadores aritméticos, por ejemplo.Measurement.Unknown * 2 == Measurement.Unknown
.Dicho esto, C # 's
Nullable<int>
implementa todo eso, con la única advertencia de que no se puede diferenciar entre diferentes tipos denull
s. No soy una persona Java, pero entiendo que el JavaOptionalInt
es similar, y es probable que otros lenguajes tengan sus propias instalaciones para representar unOptional
tipo.fuente
Value
captador, que absolutamente debe fallar, ya que no puede convertir unaUnknown
copia de seguridad en unint
. Si la medición tuviera, por ejemplo, unSaveToDatabase()
método, entonces una buena implementación probablemente no realizaría una transacción si el objeto actual es un objeto nulo (ya sea mediante la comparación con un singleton o una anulación del método).Si literalmente DEBE usar un número entero, entonces solo hay una solución posible. Use algunos de los valores posibles como 'números mágicos' que significan 'falta' y 'desconocido'
por ejemplo, 2,147,483,647 y 2,147,483,646
Si solo necesita el int para mediciones 'reales', cree una estructura de datos más complicada
Aclaración importante:
Puede lograr el requisito matemático sobrecargando los operadores para la clase
fuente
Option<Option<Int>>
type Measurement = Option<Int>
resultado que era un entero o una lectura vacía está bien, y también lo esOption<Measurement>
para una medición que podría haberse tomado o no. .Si sus variables son números de punto flotante, IEEE754 (el estándar de número de punto flotante que es compatible con la mayoría de los procesadores e idiomas modernos) le respalda: es una característica poco conocida, pero el estándar no define uno, sino una familia completa de Valores de NaN (no un número), que pueden usarse para significados arbitrarios definidos por la aplicación. En flotantes de precisión simple, por ejemplo, tiene 22 bits libres que puede usar para distinguir entre 2 ^ {22} tipos de valores no válidos.
Normalmente, las interfaces de programación exponen solo una de ellas (por ejemplo, Numpy's
nan
); No sé si hay una forma integrada de generar los otros que no sea la manipulación explícita de bits, pero es solo una cuestión de escribir un par de rutinas de bajo nivel. (También necesitará uno para distinguirlos, porque, por diseño,a == b
siempre devuelve falso cuando uno de ellos es un NaN).Usarlos es mejor que reinventar su propio "número mágico" para indicar datos no válidos, porque se propagan correctamente y señalan la invalidez: por ejemplo, no corre el riesgo de dispararse en el pie si usa una
average()
función y se olvida de verificar Sus valores especiales.El único riesgo es que las bibliotecas no los admitan correctamente, ya que son una característica bastante oscura: por ejemplo, una biblioteca de serialización puede 'aplanarlos' a todos de la misma manera
nan
(lo que parece equivalente para la mayoría de los propósitos).fuente
Siguiendo la respuesta de David Arno , puede hacer algo como una unión discriminada en OOP, y en un estilo funcional de objeto como el que ofrece Scala, los tipos funcionales de Java 8, o una biblioteca Java FP como Vavr o Fugue, se siente bastante natural escribir algo como:
impresión
( Implementación completa como una esencia ).
Un lenguaje o biblioteca FP proporciona otras herramientas como
Try
(también conocido comoMaybe
) (un objeto que contiene un valor o un error) yEither
(un objeto que contiene un valor de éxito o un valor de falla) que también podrían usarse aquí.fuente
La solución ideal para su problema dependerá de por qué le importa la diferencia entre una falla conocida y una medición no confiable conocida, y qué procesos posteriores desea respaldar. Tenga en cuenta que los 'procesos posteriores' para este caso no excluyen a los operadores humanos ni a otros desarrolladores.
El simple hecho de obtener un "segundo sabor" de nulo no le da al conjunto de procesos aguas abajo suficiente información para derivar un conjunto razonable de comportamientos.
Si, en cambio, confía en suposiciones contextuales sobre la fuente de los malos comportamientos que está haciendo el código descendente, llamaría a esa mala arquitectura.
Si sabe lo suficiente como para distinguir entre una razón de falla y una falla sin una razón conocida, y esa información informará comportamientos futuros, debe comunicar ese conocimiento corriente abajo o manejarlo en línea.
Algunos patrones para manejar esto:
null
fuente
Si me preocupara "hacer algo" en lugar de una solución elegante, el truco rápido y sucio sería simplemente usar las cadenas "desconocido", "faltante" y "representación de cadena de mi valor numérico", que luego sería convertido de una cadena y utilizado según sea necesario. Implementado más rápido que escribir esto, y en al menos algunas circunstancias, totalmente adecuado. (Ahora estoy formando un grupo de apuestas sobre el número de votos negativos ...)
fuente
Lo esencial si la pregunta parece ser "¿Cómo devuelvo dos datos no relacionados de un método que devuelve un solo int? Nunca quiero verificar mis valores de retorno, y los valores nulos son malos, no los use".
Veamos lo que quieres pasar. Está pasando una razón int o una razón no int de por qué no puede dar int. La pregunta afirma que solo habrá dos razones, pero cualquiera que haya hecho una enumeración sabe que cualquier lista crecerá. El alcance para especificar otras razones simplemente tiene sentido.
Inicialmente, entonces, parece que podría ser un buen caso para lanzar una excepción.
Cuando desea decirle a la persona que llama algo especial que no está en el tipo de retorno, las excepciones son a menudo el sistema apropiado: las excepciones no son solo para estados de error, y le permiten devolver una gran cantidad de contexto y justificación para explicar por qué simplemente puede No estoy hoy.
Y este es el ÚNICO sistema que le permite devolver entradas válidas garantizadas y garantizar que todos los operadores y métodos int que toman entradas pueden aceptar el valor de retorno de este método sin necesidad de verificar valores no válidos como valores nulos o mágicos.
Pero las excepciones son realmente solo una solución válida si, como su nombre lo indica, este es un caso excepcional , no el curso normal de los negocios.
Y un try / catch and handler es tan repetitivo como un cheque nulo, que fue lo que se objetó en primer lugar.
Y si la persona que llama no contiene el try / catch, entonces la persona que llama tiene que hacerlo, y así sucesivamente.
Un segundo paso ingenuo es decir "Es una medida. Las mediciones de distancia negativas son poco probables". Entonces, para algunas mediciones Y, solo puedes tener consts para
Esta es la forma en que se hace en muchos sistemas C antiguos, e incluso en sistemas modernos donde hay una restricción genuina para int, y no se puede ajustar a una estructura o mónada de algún tipo.
Si las mediciones pueden ser negativas, simplemente aumenta el tipo de datos (por ejemplo, int largo) y hace que los valores mágicos sean más altos que el rango de int, e idealmente comience con algún valor que se muestre claramente en un depurador.
Sin embargo, hay buenas razones para tenerlos como una variable separada, en lugar de solo tener números mágicos. Por ejemplo, mecanografía estricta, mantenibilidad y conforme a las expectativas.
En nuestro tercer intento, entonces, observamos casos en los que es normal que los negocios tengan valores no int. Por ejemplo, si una colección de estos valores puede contener múltiples entradas no enteras. Esto significa que un controlador de excepciones puede ser el enfoque incorrecto.
En ese caso, parece un buen caso para una estructura que pasa el int y la justificación. Nuevamente, esta justificación puede ser una constante como la anterior, pero en lugar de mantener ambas en el mismo int, las almacena como partes distintas de una estructura. Inicialmente, tenemos la regla de que si se establece la justificación, no se establecerá el int. Pero ya no estamos atados a esta regla; También podemos proporcionar fundamentos para números válidos, si es necesario.
De cualquier manera, cada vez que lo llame, todavía necesita repetitivo, para probar la justificación para ver si el int es válido, luego retire y use la parte int si la justificación nos lo permite.
Aquí es donde debe investigar su razonamiento detrás de "no usar nulo".
Al igual que las excepciones, nulo significa un estado excepcional.
Si una persona que llama está llamando a este método e ignorando completamente la parte "racional" de la estructura, esperando un número sin ningún manejo de errores, y obtiene un cero, entonces manejará el cero como un número, y estará equivocado. Si obtiene un número mágico, lo tratará como un número y se equivocará. Pero si se anula, se caerá , como debería hacerlo.
Por lo tanto, cada vez que llame a este método, debe realizar comprobaciones de su valor de retorno; sin embargo, maneja los valores no válidos, ya sea dentro o fuera de banda, try / catch, verificando la estructura para un componente "racional", verificando el int para un número mágico, o buscando un int para un nulo ...
La alternativa, para manejar la multiplicación de una salida que puede contener un int inválido y una justificación como "Mi perro se comió esta medida", es sobrecargar el operador de multiplicación para esa estructura.
... Y luego sobrecargue a cualquier otro operador en su aplicación que pueda aplicarse a estos datos.
... Y luego sobrecargue todos los métodos que puedan tomar ints.
... Y todas esas sobrecargas aún deberán contener comprobaciones de entradas inválidas, solo para que pueda tratar el tipo de retorno de este método como si siempre fuera un int válido en el momento en que lo llama.
Entonces, la premisa original es falsa de varias maneras:
fuente
No entiendo la premisa de su pregunta, pero aquí está la respuesta nominal. Para Missing or Empty, puede hacer
math.nan
(Not a Number). Puede realizar cualquier operación matemáticamath.nan
y permanecerámath.nan
.Puede usar
None
(nulo de Python) para un valor desconocido. De todos modos, no debe manipular un valor desconocido, y algunos lenguajes (Python no es uno de ellos) tienen operadores nulos especiales para que la operación solo se realice si el valor no es nulo; de lo contrario, el valor permanece nulo.Otros idiomas tienen cláusulas de guardia (como Swift o Ruby), y Ruby tiene un retorno anticipado condicional.
He visto esto resuelto en Python de diferentes maneras:
__mult__
modo que no se generen excepciones cuando aparezcan sus valores Desconocido o Falta. Numpy y los pandas podrían tener tal capacidad en ellos.Unknown
o -1 / -2) y una declaración iffuente
La forma en que se almacena el valor en la memoria depende del idioma y los detalles de implementación. Creo que lo que quieres decir es cómo debe comportarse el objeto para el programador. (Así es como leo la pregunta, dime si me equivoco).
Ya ha propuesto una respuesta a eso en su pregunta: use su propia clase que acepte cualquier operación matemática y se devuelva sin generar una excepción. Dices que quieres esto porque quieres evitar cheques nulos.
Solución 1: no evite las verificaciones nulas
Missing
se puede representar comomath.nan
Unknown
se puede representar comoNone
Si tiene más de un valor,
filter()
solo puede aplicar la operación en valores que no sonUnknown
oMissing
, o cualquier valor que desee ignorar para la función.No puedo imaginar un escenario en el que necesite una comprobación nula de una función que actúa en un solo escalar. En ese caso, es bueno forzar comprobaciones nulas.
Solución 2: use un decorador que capture excepciones
En este caso,
Missing
podría aumentarMissingException
yUnknown
podría aumentarUnknownException
cuando se realizan operaciones en él.La ventaja de este enfoque es que las propiedades de
Missing
yUnknown
solo se suprimen cuando se solicita explícitamente que se supriman. Otra ventaja es que este enfoque es autodocumentado: cada función muestra si espera o no un desconocido o falta y cómo funciona la función.Cuando llama a una función que no espera que un Missing obtenga un Missing, la función se elevará inmediatamente, mostrándole exactamente dónde ocurrió el error en lugar de fallar silenciosamente y propagar un Missing en la cadena de llamadas. Lo mismo vale para Desconocido.
sigmoid
aún puede llamarsin
, aunque no espera unMissing
oUnknown
, ya quesigmoid
el decorador detectará la excepción.fuente
Ambas suenan como condiciones de error, por lo que juzgaría que la mejor opción aquí es simplemente
get_measurement()
lanzar ambas como excepciones de inmediato (comoDataSourceUnavailableException
oSpectacularFailureToGetDataException
, respectivamente). Luego, si se produce alguno de estos problemas, el código de recopilación de datos puede reaccionar ante él de inmediato (por ejemplo, al intentarlo de nuevo en el último caso), yget_measurement()
solo tiene que devolver unoint
en el caso de que pueda obtener con éxito los datos de los datos fuente - y sabes queint
es válido.Si su situación no admite excepciones o no puede hacer mucho uso de ellas, entonces una buena alternativa es usar códigos de error, tal vez devueltos a través de una salida separada a
get_measurement()
. Este es el patrón idiomático en C, donde la salida real se almacena en un puntero de entrada y se devuelve un código de error como valor de retorno.fuente
Las respuestas dadas están bien, pero aún no reflejan la relación jerárquica entre valor, vacío y desconocido.
Feo (por su abstracción que falla), pero completamente operativo sería (en Java):
Aquí los lenguajes funcionales con un buen sistema de tipos son mejores.
De hecho: Los vacíos / faltantes y * desconocidos que no son valores parecen más bien parte de algún estado del proceso, alguna línea de producción. Al igual que las celdas de hoja de cálculo Excel con fórmulas que hacen referencia a otras celdas. Allí se podría pensar en almacenar lambdas contextuales. Cambiar una celda volvería a evaluar todas las celdas recursivamente dependientes.
En ese caso, un proveedor int obtendría un valor int. Un valor vacío daría a un proveedor int lanzando una excepción vacía, o evaluando para vaciar (recursivamente hacia arriba). Su fórmula principal conectaría todos los valores y posiblemente también devolvería un valor vacío (valor / excepción). Un valor desconocido deshabilitaría la evaluación lanzando una excepción.
Los valores probablemente serían observables, como una propiedad vinculada a Java, notificando a los oyentes sobre el cambio.
En resumen: el patrón recurrente de la necesidad de valores con estados adicionales vacíos y desconocidos parece indicar que una hoja de cálculo más como el modelo de datos de propiedades vinculadas podría ser mejor.
fuente
Sí, el concepto de múltiples tipos de NA diferentes existe en algunos idiomas; más aún en los estadísticos, donde es más significativo (a saber, la gran distinción entre Missing-At-Random, Missing-Complely-At-Random, Missing-At-Random ).
si solo estamos midiendo las longitudes de los widgets, entonces no es crucial distinguir entre 'falla del sensor' o 'corte de energía' o 'falla de la red' (aunque el 'desbordamiento numérico' transmite información)
pero, por ejemplo, en la minería de datos o en una encuesta, preguntando a los encuestados, por ejemplo, sus ingresos o su estado de VIH, un resultado de 'Desconocido' es distinto de 'Rechazar respuesta', y puede ver que nuestras suposiciones anteriores sobre cómo imputar a este último tenderán ser diferente al primero. Entonces, los lenguajes como SAS admiten múltiples tipos de NA diferentes; el lenguaje R no lo hace, pero los usuarios a menudo tienen que hackear eso; Los NA en diferentes puntos de una tubería pueden usarse para denotar cosas muy diferentes.
En cuanto a cómo representa diferentes tipos de NA en lenguajes de uso general que no los admiten, en general las personas piratean cosas como NaN de punto flotante (requiere la conversión de enteros), enumeraciones o centinelas (por ejemplo, 999 o -1000) para enteros o valores categóricos Por lo general, no hay una respuesta muy clara, lo siento.
fuente
R tiene soporte de valor perdido incorporado. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17
Editar: porque fui rechazado voy a explicar un poco.
Si va a tratar con estadísticas, le recomiendo que use un lenguaje de estadísticas como R porque R está escrito por estadísticos para estadísticos. La falta de valores es un tema tan grande que te enseñan todo un semestre. Y hay grandes libros solo sobre valores perdidos.
Sin embargo, puede marcar sus datos faltantes, como un punto o "falta" o lo que sea. En R puedes definir lo que quieres decir con faltar. No necesitas convertirlos.
La forma normal de definir el valor perdido es marcarlos como
NA
.Entonces puede ver qué valores faltan;
Y entonces el resultado será;
Como puedes ver
""
no falta. Puedes amenazar""
como desconocido. YNA
falta.fuente
¿Hay alguna razón por la que la funcionalidad del
*
operador no se pueda alterar en su lugar?La mayoría de las respuestas implican un valor de búsqueda de algún tipo, pero podría ser más fácil modificar el operador matemático en este caso.
A continuación, sería capaz de tener similares
empty()
/unknown()
funcionalidad a través de todo el proyecto.fuente