La mejor explicación para idiomas sin nulo

225

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í)
Roman A. Taycher
fuente
11
Si agrega etiquetas a esta pregunta para programación funcional o F #, seguramente obtendrá algunas respuestas fantásticas.
Stephen Swensen el
Agregué una etiqueta de programación funcional ya que el tipo de opción provenía del mundo ml. Prefiero no marcarlo F # (demasiado específico). Por cierto, alguien con poderes de taxonomía necesita agregar etiquetas de tipo quizás u opción.
Roman A. Taycher
44
sospecho que hay poca necesidad de etiquetas tan específicas. Las etiquetas son principalmente para permitir que las personas encuentren preguntas relevantes (por ejemplo, "preguntas que conozco mucho y que podré responder", y "programación funcional" es muy útil allí. Pero algo como "nulo" o " tipo de opción "son mucho menos útiles. Es probable que pocas personas monitoreen una etiqueta de" tipo de opción "en busca de preguntas que puedan responder.;)
jalf
No olvidemos que una de las principales razones de nulo es que las computadoras evolucionaron fuertemente ligadas a la teoría de conjuntos. Null es uno de los conjuntos más importantes en toda la teoría de conjuntos. Sin ella, los algoritmos completos se romperían. Por ejemplo, realice una ordenación por fusión. Esto implica romper una lista por la mitad varias veces. ¿Qué pasa si la lista tiene 7 elementos? Primero lo divide en 4 y 3. Luego 2, 2, 2 y 1. Luego 1, 1, 1, 1, 1, 1, 1 y ... ¡nulo! Null tiene un propósito, solo uno que no se ve prácticamente. Existe más para el ámbito teórico.
stevendesu
66
@steven_desu - No estoy de acuerdo. En los idiomas 'anulables', puede tener una referencia a una lista vacía [], y también una referencia de lista nula. Esta pregunta se relaciona con la confusión entre los dos.
stusmith

Respuestas:

433

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

class Door
    private bool isShut
    private bool isLocked

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 definir

type DoorState =
    | Open | ShutAndUnlocked | ShutAndLocked

entonces podría definir

class Door
    private DoorState state

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 nulles que cada tipo de referencia obtiene este estado adicional en su espacio que normalmente no es deseado. Una stringvariable podría ser cualquier secuencia de caracteres, o podría ser este loco nullvalor extra que no se asigna a mi dominio problemático. Un Triangleobjeto tiene tres Points, que ellos mismos tienen Xy Yvalores, pero desafortunadamente el Points o el Trianglemismo 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 Personuna tiene a FirstNamey a LastName, pero solo algunas personas tienen MiddleNames, entonces me gustaría decir algo como

class Person
    private string FirstName
    private Option<string> MiddleName
    private string LastName

donde stringaquí se supone que es un tipo no anulable. Entonces no hay invariantes difíciles de establecer y no hay NullReferenceExceptions inesperados al tratar de calcular la longitud del nombre de alguien. El sistema de tipos asegura que cualquier código que se ocupe de las MiddleNamecuentas tiene la posibilidad de serlo None, mientras que cualquier código que se ocupe de FirstNameeso puede asumir con seguridad que hay un valor allí.

Entonces, por ejemplo, usando el tipo anterior, podríamos crear esta función tonta:

let TotalNumCharsInPersonsName(p:Person) =
    let middleLen = match p.MiddleName with
                    | None -> 0
                    | Some(s) -> s.Length
    p.FirstName.Length + middleLen + p.LastName.Length

sin preocupaciones Por el contrario, en un lenguaje con referencias anulables para tipos como string, luego suponiendo

class Person
    private string FirstName
    private string MiddleName
    private string LastName

terminas creando cosas como

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length + p.MiddleName.Length + p.LastName.Length

que explota si el objeto Person entrante no tiene la invariante de que todo sea no nulo, o

let TotalNumCharsInPersonsName(p:Person) =
    (if p.FirstName=null then 0 else p.FirstName.Length)
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + (if p.LastName=null then 0 else p.LastName.Length)

o tal vez

let TotalNumCharsInPersonsName(p:Person) =
    p.FirstName.Length
    + (if p.MiddleName=null then 0 else p.MiddleName.Length)
    + p.LastName.Length

asumiendo que eso pasegura 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 FirstNameno puede ser null, un stringpuede 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 un NonEmptyStringtipo). 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).

Brian
fuente
31
Re: nombres - De hecho. Y tal vez le importe modelar una puerta que está abierta pero con el cerrojo sobresaliendo, evitando que la puerta se cierre. Hay mucha complejidad en el mundo. La clave es no agregar más complejidad al implementar el mapeo entre "estados mundiales" y "estados de programa" en su software.
Brian
59
¿Qué, nunca has cerrado las puertas?
Joshua
58
No entiendo por qué la gente se preocupa por la semántica de un dominio en particular. Brian representó los defectos con nulo de una manera concisa y simple, sí, simplificó el dominio del problema en su ejemplo al decir que todos tienen nombres y apellidos. La pregunta fue respondida a una 'T', Brian: si alguna vez estás en Boston, ¡te debo una cerveza por todo lo que publicas aquí!
akaphenom
67
@akaphenom: gracias, pero tenga en cuenta que no todas las personas beben cerveza (no soy bebedor). Pero agradezco que solo estés usando un modelo simplificado del mundo para comunicar gratitud, por lo que no voy a discutir más sobre los supuestos erróneos de tu modelo mundial. : P (¡Tanta complejidad en el mundo real! :))
Brian
44
¡Extrañamente, hay 3 puertas de estado en este mundo! Se usan en algunos hoteles como puertas de baño. Un pulsador actúa como una llave desde el interior, que bloquea la puerta desde el exterior. Se desbloquea automáticamente, tan pronto como se mueve el pestillo.
comonad
65

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:

  1. la variable no está inicializada. Esto debería, idealmente, nunca suceder. Una variable no debería existir a menos que se inicialice.
  2. la variable contiene algunos datos "opcionales": debe ser capaz de representar el caso donde no hay datos . Esto a veces es necesario. Quizás esté tratando de encontrar un objeto en una lista, y no sabe de antemano si está allí o no. Entonces necesitamos poder representar que "no se encontró ningún objeto".

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.

jalf
fuente
Y en el segundo significado, queremos que el compilador nos advierta (¿detener?) Si intentamos acceder a tales variables sin verificar primero la nulidad. Aquí hay un gran artículo sobre la próxima característica nula / no nula de C # (¡finalmente!) Blogs.msdn.microsoft.com/dotnet/2017/11/15/…
Ohad Schneider
44

Todas las respuestas hasta ahora se centran en por qué nulles 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 Optiono Maybepara 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 Optionsasí.

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

def fullNameLength(p:Person) = {
  val middleLen =
    if (null == p.middleName)
      p.middleName.length
    else
      0
  p.firstName.length + middleLen + p.lastName.length
}

al código de opciones

def fullNameLength(p:Person) = {
  val middleLen = p.middleName match {
    case Some(x) => x.length
    case _ => 0
  }
  p.firstName.length + middleLen + p.lastName.length
}

no hay mucha diferencia! Pero también es un forma terrible de usar Opciones ... Este enfoque es mucho más limpio:

def fullNameLength(p:Person) = {
  val middleLen = p.middleName map {_.length} getOrElse 0
  p.firstName.length + middleLen + p.lastName.length
}

O incluso:

def fullNameLength(p:Person) =       
  p.firstName.length +
  p.middleName.map{length}.getOrElse(0) +
  p.lastName.length

Cuando comienzas a lidiar con la Lista de opciones, se pone aún mejor. Imagina que la listapeople es en sí misma opcional:

people flatMap(_ find (_.firstName == "joe")) map (fullNameLength)

¿Como funciona esto?

//convert an Option[List[Person]] to an Option[S]
//where the function f takes a List[Person] and returns an S
people map f

//find a person named "Joe" in a List[Person].
//returns Some[Person], or None if "Joe" isn't in the list
validPeopleList find (_.firstName == "joe")

//returns None if people is None
//Some(None) if people is valid but doesn't contain Joe
//Some[Some[Person]] if Joe is found
people map (_ find (_.firstName == "joe")) 

//flatten it to return None if people is None or Joe isn't found
//Some[Person] if Joe is found
people flatMap (_ find (_.firstName == "joe")) 

//return Some(length) if the list isn't None and Joe is found
//otherwise return None
people flatMap (_ find (_.firstName == "joe")) map (fullNameLength)

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.

Kevin Wright
fuente
8
+1, este es un buen punto para enfatizar. Un apéndice: en Haskell-land, flatMapse llamaría (>>=), el operador de "vinculación" para las mónadas. Así es, a Haskellers le gusta tanto hacer flatMapping a las cosas que lo ponemos en el logotipo de nuestro idioma.
CA McCann
1
+1 Con suerte, una expresión de 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)
Suficientemente fácil de hacer: 'List (null) .headOption'. Tenga en cuenta que esto significa algo muy diferente a un valor de retorno de 'Ninguno'
Kevin Wright
44
Te di recompensas porque realmente me gusta lo que dijiste sobre la composición, que otras personas no parecían mencionar.
Roman A. Taycher
Excelente respuesta con excelentes ejemplos!
thSoft
38

Dado que la gente parece estar perdiendo: nulles 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: nulles un caso extremo.

  • Es null = null?
  • Es nan = nan?
  • Es inf = inf?
  • Es +0 = -0?
  • Es +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:

UNIQUE(a, b, id_a)
UNIQUE(a, b, id_b)

id_asolo 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 como assert((bool)actionSheet + (bool)alertView == 1).

tc.
fuente
Sin embargo, los matemáticos reales no usan el concepto de "NaN", pueden estar seguros.
Noldorin
@Noldorin: Sí, pero usan el término "forma indeterminada".
IJ Kennedy el
@ IJKennedy: Esa es una universidad diferente, que sé muy bien, gracias. Algunos 'NaN's pueden representar una forma indeterminada, pero como el FPA no hace un razonamiento simbólico, ¡equipararlo con una forma indeterminada es bastante engañoso!
Noldorin
¿Qué tiene de malo assert(actionSheet ^ alertView)? ¿O no puede su idioma XOR bools?
gato
16

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:

  1. La referencia / puntero no está inicializada: el problema aquí es el mismo que la mutabilidad en general. Por un lado, hace que sea más difícil analizar su código.
  2. La variable ser nula en realidad significa algo: este es el caso que los tipos de opciones realmente formalizan.

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 #:

//first we create the option list, and then filter out all None Option types and 
//map all Some Option types to their values.  See how type-inference shines.
let optionList = [Some(1); Some(2); None; Some(3); None]
optionList |> List.choose id //evaluates to [1;2;3]

//here is a simple pattern-matching example
//which prints "1;2;None;3;None;".
//notice how value is extracted from op during the match
optionList 
|> List.iter (function Some(value) -> printf "%i;" value | None -> printf "None;")

Sin embargo, en un lenguaje como Java sin soporte directo para los tipos de Opción, tendríamos algo como:

//here we perform the same filter/map operation as in the F# example.
List<Option<Integer>> optionList = Arrays.asList(new Some<Integer>(1),new Some<Integer>(2),new None<Integer>(),new Some<Integer>(3),new None<Integer>());
List<Integer> filteredList = new ArrayList<Integer>();
for(Option<Integer> op : list)
    if(op instanceof Some)
        filteredList.add(((Some<Integer>)op).getValue());

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.

Stephen Swensen
fuente
Este es un motivo favorito, pero c # no es un lenguaje similar a c.
Roman A. Taycher
44
Iba por Java aquí, ya que C # probablemente tendría una mejor solución ... pero le agradezco mucho, lo que la gente realmente quiere decir es "un lenguaje con sintaxis inspirada en c". Seguí adelante y reemplacé la declaración "c-like".
Stephen Swensen
Con linq, a la derecha. Estaba pensando en C # y no me di cuenta de eso.
Roman A. Taycher
1
Sí, con la sintaxis inspirada en c en su mayoría, pero creo que también he oído hablar de lenguajes de programación imperativos como python / ruby ​​con muy poca sintaxis c como los programadores funcionales denominan c-like.
Roman A. Taycher
11

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:

  • Decir que algo no está definido .
  • Decir que algo es opcional .

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:

struct PhoneNumber { ... };
struct MotorbikeLicence { ... };
struct CarLicence { ... };
struct TruckLicence { ... };

struct Driver {
  char name[32]; /* Null terminated */
  struct PhoneNumber * emergency_phone_number;
  struct MotorbikeLicence * motorbike_licence;
  struct CarLicence * car_licence;
  struct TruckLicence * truck_licence;
};

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í:

type phone_number = { ... }
type motorbike_licence = { ... }
type car_licence = { ... }
type truck_licence = { ... }

type driver = {
  name: string;
  emergency_phone_number: phone_number option;
  motorbike_licence: motorbike_licence option;
  car_licence: car_licence option;
  truck_licence: truck_licence option;
}

Digamos ahora que queremos imprimir los nombres de todos los conductores junto con sus números de licencia de camión.

C ª:

#include <stdio.h>

void print_driver_with_truck_licence_number(struct Driver * driver) {
  /* Check may be redundant but better be safe than sorry */
  if (driver != NULL) {
    printf("driver %s has ", driver->name);
    if (driver->truck_licence != NULL) {
      printf("truck licence %04d-%04d-%08d\n",
        driver->truck_licence->area_code
        driver->truck_licence->year
        driver->truck_licence->num_in_year);
    } else {
      printf("no truck licence\n");
    }
  }
}

void print_drivers_with_truck_licence_numbers(struct Driver ** drivers, int nb) {
  if (drivers != NULL && nb >= 0) {
    int i;
    for (i = 0; i < nb; ++i) {
      struct Driver * driver = drivers[i];
      if (driver) {
        print_driver_with_truck_licence_number(driver);
      } else {
        /* Huh ? We got a null inside the array, meaning it probably got
           corrupt somehow, what do we do ? Ignore ? Assert ? */
      }
    }
  } else {
    /* Caller provided us with erroneous input, what do we do ?
       Ignore ? Assert ? */
  }
}

En OCaml eso sería:

open Printf

(* Here we are guaranteed to have a driver instance *)
let print_driver_with_truck_licence_number driver =
  printf "driver %s has " driver.name;
  match driver.truck_licence with
    | None ->
        printf "no truck licence\n"
    | Some licence ->
        (* Here we are guaranteed to have a licence *)
        printf "truck licence %04d-%04d-%08d\n"
          licence.area_code
          licence.year
          licence.num_in_year

(* Here we are guaranteed to have a valid list of drivers *)
let print_drivers_with_truck_licence_numbers drivers =
  List.iter print_driver_with_truck_licence_number drivers

Como puede ver en este ejemplo trivial, no hay nada complicado en la versión segura:

  • Es terser.
  • Obtiene garantías mucho mejores y no se requiere ningún cheque nulo.
  • El compilador se aseguró de que manejaste correctamente la opción

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.

bltxd
fuente
Nunca lo he intentado, pero en.wikipedia.org/wiki/Cyclone_%28programming_language%29 afirma que permite punteros no nulos para c.
Roman A. Taycher
1
No estoy de acuerdo con su afirmación de que a nadie le interesa el primer caso. Muchas personas, especialmente aquellas en las comunidades de lenguaje funcional, están extremadamente interesadas en esto y desalientan o prohíben completamente el uso de variables no inicializadas.
Stephen Swensen el
Creo NULLque 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).
1
@Stephen: Probablemente quisimos decir lo mismo. Para mí, desalientan o prohíben el uso de cosas no inicializadas precisamente porque no tiene sentido discutir cosas indefinidas ya que no podemos hacer nada sensato o útil con ellas. No tendría ningún interés en absoluto.
bltxd
2
como @tc. dice, null no tiene nada que ver con el ensamblaje. En el ensamblaje, los tipos generalmente no son anulables. Un valor cargado en un registro de propósito general puede ser cero o puede ser un número entero distinto de cero. Pero nunca puede ser nulo. Incluso si carga una dirección de memoria en un registro, en la mayoría de las arquitecturas comunes, no hay una representación separada del "puntero nulo". Ese es un concepto introducido en lenguajes de nivel superior, como C.
enero
5

Microsoft Research tiene un proyecto interesante llamado

Especificaciones#

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.

Jahan
fuente
4

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:

Lo llamo mi error de mil millones de dólares. Fue la invención de la referencia nula en 1965. En ese momento, estaba diseñando el primer sistema de tipos completo para referencias en un lenguaje orientado a objetos (ALGOL W). Mi objetivo era asegurar que todo uso de referencias debería ser absolutamente seguro, con una verificación realizada automáticamente por el compilador. Pero no pude resistir la tentación de poner una referencia nula, simplemente porque era muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y fallas en el sistema, lo que probablemente ha causado miles de millones de dólares de dolor y daños en los últimos cuarenta años. En los últimos años, varios analizadores de programas como PREfix y PREfast en Microsoft se han utilizado para verificar referencias y advertir si existe el riesgo de que no sean nulos. Los lenguajes de programación más recientes como Spec # han introducido declaraciones para referencias no nulas. Esta es la solución, que rechacé en 1965.

Vea esta pregunta también en programadores

nawfal
fuente
1

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.

Jon
fuente
Hola @ Jon, es un poco difícil seguirte aquí. Finalmente me di cuenta de que por valores "especiales / extraños" probablemente se refiera a algo como 'indefinido' de Javascript o 'NaN' de IEEE. Pero además de eso, realmente no aborda ninguna de las preguntas que hizo el OP. Y la afirmación de que "Nulo es probablemente la noción más útil para verificar si algo está ausente" es casi ciertamente errónea. Los tipos de opciones son una alternativa de tipo nulo bien considerada y segura.
Stephen Swensen
@Stephen: en realidad, mirando hacia atrás en mi mensaje, creo que toda la segunda mitad debería trasladarse a una pregunta aún por formular. Pero todavía digo que nulo es muy útil para verificar si algo está ausente.
Jon
0

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.

Joshua
fuente
Creo que entiendo de lo que estás hablando, pero ¿podrías enumerar algunos ejemplos? ¿Especialmente de aplicar múltiples funciones a un valor posiblemente nulo?
Roman A. Taycher
Bien, la aplicación de una transformación vectorial a un vector vacío da como resultado otro vector vacío. Para su información, SQL es principalmente un lenguaje vectorial.
Joshua
1
OK, mejor aclaro eso. SQL es un lenguaje vectorial para filas y un lenguaje de valores para columnas.
Joshua