¿Tiene sentido tener tanto el concepto de 'nulo' como 'quizás'?

11

Al crear un cliente para una API web en C #, me encontré con un problema relacionado nullcon un valor en el que representaría dos cosas diferentes:

  • nada , por ejemplo, un foopuede o no tener unbar
  • desconocido : por defecto, la respuesta API solo incluye un subconjunto de propiedades, debe indicar qué propiedades adicionales desea. Tan desconocido significa que la propiedad no se solicitó a la API.

Después de algunas búsquedas, descubrí el tipo Quizás (u Opción), cómo se usa en lenguajes funcionales y cómo "resuelve" problemas de anulación de referencia al obligar al usuario a pensar en la posible ausencia de un valor. Sin embargo, todos los recursos que encontré hablaron de reemplazar nulo con Quizás . Encontré algunas menciones de lógica de tres valores , pero no lo entiendo completamente y la mayoría de las veces su mención fue en el contexto de "es algo malo".

Ahora me pregunto si tiene sentido tener tanto el concepto de nulo como el de Quizás , para representar lo desconocido y nada, respectivamente. ¿Es esta la lógica de tres valores sobre la que leí, o tiene otro nombre? ¿O es la forma prevista de anidar un tal vez en un tal vez?

Stijn
fuente
99
No tiene sentido tener null. Es una idea completamente rota.
Andrej Bauer
77
No hagas un quizás-quizás-foo que tenga una semántica diferente a la de quizás-foo. Quizás es una mónada , y una de las leyes de la mónada es eso M M xy M xdebería tener la misma semántica.
Eric Lippert
2
Puede considerar mirar el diseño de las primeras versiones de Visual Basic, que tenían Nothing (referencia a ningún objeto), Null (semántica nula de la base de datos), Empty (una variable no inicializada) y Missing (no se pasó un parámetro opcional). Este diseño era complicado y en muchos sentidos inconsistente, pero hay una razón por la cual esos conceptos no se combinaron entre sí.
Eric Lippert
3
Maybe aa+1Maybe Maybe aa+1+1UserInput a
12
@EricLippert: es falso que " M (M x)y M xdebería tener la misma semántica". Tomemos M = Listpor ejemplo: las listas de listas no son lo mismo que las listas. Cuando Mes una mónada, hay una transformación (es decir, la multiplicación de la mónada) a partir M (M x)de la M xcual se explica la relación entre ellos, pero no tienen "la misma semántica".
Andrej Bauer

Respuestas:

14

El nullvalor como presente predeterminado en todas partes es solo una idea realmente rota , así que olvídate de eso.

Siempre debe tener exactamente el concepto que mejor describa sus datos reales. Si necesita un tipo que indique "desconocido", "nada" y "valor", debe tener precisamente eso. Pero si no se ajusta a sus necesidades reales, entonces no debe hacerlo. Es una buena idea ver qué usan otras personas y qué han propuesto, pero no tiene que seguirlas a ciegas.

Las personas que diseñan bibliotecas estándar intentan adivinar patrones de uso comunes, y generalmente lo hacen bien, pero si hay algo específico que necesita, debe definirlo usted mismo. Por ejemplo, en Haskell, podría definir:

data UnknownNothing a =
     Unknown
   | Nothing
   | Value a

Incluso puede darse el caso de que deba usar algo más descriptivo que sea más fácil de recordar:

data UserInput a =
     Unknown
   | NotGiven
   | Answer a

Podría definir 10 de estos tipos para ser utilizados en diferentes escenarios. El inconveniente es que no tendrá funciones de biblioteca preexistentes disponibles (como las de Maybe), pero esto generalmente resulta ser un detalle. No es tan difícil agregar su propia funcionalidad.

Andrej Bauer
fuente
Tiene mucho sentido cuando lo ves enunciado así. Me interesan principalmente las ideas, el soporte de la biblioteca existente es solo una ventaja :)
Stijn
Si tan solo la barrera para crear tales tipos fuera menor en muchos idiomas. : /
Raphael
Si tan solo las personas dejaran de usar idiomas de la década de 1960 (o sus reencarnaciones de la década de 1980).
Andrej Bauer
@AndrejBauer: ¿qué? ¡Pero los teóricos no tendrían nada de qué quejarse!
Yttrill
No nos quejamos, estamos en lo alto.
Andrej Bauer el
4

Hasta donde sé, el valor nullen C # es un valor posible para algunas variables, dependiendo de su tipo (¿estoy en lo cierto?). Por ejemplo, instancias de alguna clase. Para el resto de los tipos (como int, booletc.) puede agregar este valor de excepción declarando variables con int?o en su bool?lugar (esto es exactamente lo que hace el Maybeconstructor, como describiré a continuación).

El Maybeconstructor de tipos de la programación funcional agrega este nuevo valor de excepción para un tipo de datos dado. Entonces, si Intes el tipo de enteros o Gamees el tipo de estado de un juego, entonces Maybe Inttiene todos los enteros más un valor nulo (a veces llamado nada ). Lo mismo para Maybe Game. Aquí, no hay tipos que vienen con el nullvalor. Lo agrega cuando lo necesita.

En mi opinión, este último enfoque es el mejor para cualquier lenguaje de programación.

Euge
fuente
Sí, tu comprensión de C # es correcta. Entonces, ¿puedo deducir de su respuesta que sugiere anidar Maybey soltar por nullcompleto?
Stijn
2
Mi opinión es sobre cómo debe ser un idioma. Realmente no conozco las mejores prácticas de programación en C #. En su caso, la mejor opción sería definir un tipo de datos como lo describió @Andrej Bauer, pero creo que no puede hacerlo en C #.
Euge
@Euge: los tipos de suma algebraica se pueden aproximar (aproximadamente) con el subtipo clásico de estilo OO y el envío de mensajes polimórficos de subtipo. Así es como se hace en Scala, por ejemplo, con un giro adicional para permitir tipos cerrados . El tipo se convierte en una superclase abstracta, los constructores de tipos se convierten en subclases concretas, la coincidencia de patrones sobre los constructores se puede aproximar moviendo los casos a métodos sobrecargados en las subclases o isinstancepruebas concretas . Scala también tiene una coincidencia de patrones "adecuada", y C♯ en realidad también obtuvo patrones simples recientemente.
Jörg W Mittag
El "giro adicional" que mencioné para Scala es que, además de los modificadores "estándar" "virtuales" (para una clase que se puede heredar de) y final(para una clase que no se puede heredar de), también tiene sealed, para un clase que solo se puede extender dentro de la misma unidad de compilación . Esto significa que el compilador puede conocer estáticamente todas las subclases posibles (siempre que todas sean ellas mismas sealedo final) y, por lo tanto, realizar comprobaciones exhaustivas de coincidencias de patrones, lo que no es posible estáticamente para un idioma con carga de código de tiempo de ejecución y herencia sin restricciones.
Jörg W Mittag
Por supuesto, puede diseñar tipos con esa semántica en C #. Tenga en cuenta que C # no permite que el tipo incorporado (llamado Nullable en C #) se anide. int??es ilegal en C #.
Eric Lippert
3

Si puede definir uniones de tipo, como X or Y, y si tiene un tipo llamado Nullque representa solo el nullvalor (y nada más), entonces, para cualquier tipo T, en T or Nullrealidad no es tan diferente de Maybe T. El único problema con nulles cuando las golosinas de idioma un tipo Tcomo T or Nullimplícita, por lo que nullun valor válido para todo tipo (excepto los tipos primitivos en lenguajes que los tienen). Esto es lo que sucede, por ejemplo, en Java, donde una función que toma a Stringtambién acepta null.

Si trata los tipos como un conjunto de valores y orcomo una unión de conjuntos, (T or Null) or Nullrepresenta el conjunto de valores T ∪ {null} ∪ {null}, que es lo mismo que T or Null. Hay compiladores que realizan este tipo de análisis (SBCL). Como se dijo en los comentarios, no puede distinguir fácilmente entre Maybe Ty Maybe Maybe Tcuando ve los tipos como dominios (por otro lado, esa vista es bastante útil para los programas de tipo dinámico). Pero también podría mantener los tipos como expresiones algebraicas, donde (T or Null) or Nullrepresenta múltiples niveles de Maybes.

volcado de memoria
fuente
2
En realidad, es importante que Maybe (Maybe X)no sea lo mismo que Maybe X. NothingNo es lo mismo que Just Nothing. La combinación Maybe (Maybe X)con Maybe Xhace imposible el tratamiento Maybe _polimórfico.
Gilles 'SO- deja de ser malvado'
1

Necesita averiguar lo que desea expresar y luego encontrar la forma de expresarlo.

Primero, tiene valores en su dominio problemático (como números, cadenas, registros de clientes, booleanos, etc.). En segundo lugar, tiene algunos valores genéricos adicionales además de los valores de su dominio problemático: por ejemplo, "nada" (la ausencia conocida de un valor), "desconocido" (sin conocimiento sobre la presencia o ausencia de un valor), no mencionado fue " algo "(definitivamente hay un valor, pero no sabemos cuál). Quizás puedas pensar en otras situaciones.

Si quisiera poder representar todos los valores más estos tres valores adicionales, crearía una enumeración con los casos "nada", "desconocido", "algo" y "valor" e iría desde allí.

En algunos idiomas, algunos casos son más simples. Por ejemplo, en Swift tiene para cada tipo T el tipo "T opcional" que tiene sus posibles valores nulo más todos los valores de T. Esto le permite manejar fácilmente muchas situaciones, y es perfecto si tiene "nada" y " valor". Puede usarlo si tiene "desconocido" y "valor". Usted podría no usarlo si usted necesita para manejar "nada", "desconocido" y "valor". Otros idiomas pueden usar "nulo" o "tal vez", pero esa es solo otra palabra.

En JSON, tiene diccionarios con pares clave / valor. Aquí, una clave puede faltar en un diccionario o puede tener un valor nulo. Entonces, al describir cuidadosamente cómo se almacenan las cosas, puede representar valores genéricos más dos valores adicionales.

gnasher729
fuente