¿Por qué la mayoría de los lenguajes imperativos / OO "conocidos" permiten el acceso sin control a tipos que pueden representar un valor de "nada"?

29

He estado leyendo sobre la (des) conveniencia de tener en nulllugar de (por ejemplo) Maybe. Después de leer este artículo , estoy convencido de que sería mucho mejor usarMaybe (o algo similar). Sin embargo, me sorprende ver que todos los lenguajes de programación imperativos u orientados a objetos "bien conocidos" todavía se usan null(lo que permite el acceso sin control a tipos que pueden representar un valor de "nada"), y eso Maybese usa principalmente en lenguajes de programación funcionales.

Como ejemplo, mira el siguiente código C #:

void doSomething(string username)
{
    // Check that username is not null
    // Do something
}

Algo huele mal aquí ... ¿Por qué deberíamos verificar si el argumento es nulo? ¿No deberíamos suponer que cada variable contiene una referencia a un objeto? Como puede ver, el problema es que, por definición, casi todas las variables pueden contener una referencia nula. ¿Qué pasaría si pudiéramos decidir qué variables son "anulables" y cuáles no? Eso nos ahorraría mucho esfuerzo al depurar y buscar una "NullReferenceException". Imagine que, por defecto, ningún tipo podría contener una referencia nula . En lugar de eso, declararía explícitamente que una variable puede contener una referencia nula , solo si realmente la necesita. Esa es la idea detrás de Quizás. Si tiene una función que en algunos casos falla (por ejemplo, división por cero), puede devolver unMaybe<int>, declarando explícitamente que el resultado puede ser un int, ¡pero también nada! Esta es una razón para preferir Quizás en lugar de nulo. Si está interesado en más ejemplos, le sugiero que lea este artículo .

Los hechos son que, a pesar de las desventajas de hacer que la mayoría de los tipos sean anulables por defecto, la mayoría de los lenguajes de programación OO realmente lo hacen. Por eso me pregunto sobre:

  • ¿Qué tipo de argumentos tendrías que implementar nullen tu lenguaje de programación en lugar de Maybe? ¿Hay alguna razón o es solo "equipaje histórico"?

Asegúrese de comprender la diferencia entre nulo y Quizás antes de responder esta pregunta.

aochagavia
fuente
3
Sugiero leer sobre Tony Hoare , en particular su error de mil millones de dólares.
Oded
2
Bueno, esa fue su razón. Y el resultado desafortunado es que fue un error copiado a la mayoría de los idiomas que siguieron, hasta hoy. No son los idiomas en que nullexisten o no su concepto (IIRC Haskell es un ejemplo de ello).
Oded
9
El equipaje histórico no es algo para subestimar. Y recuerde que los sistemas operativos en los que se basan estos se escribieron en idiomas que han tenido nulls en ellos durante mucho tiempo. No es fácil dejar eso.
Oded
3
Creo que debería arrojar algo de luz sobre el concepto de Quizás en lugar de publicar un enlace.
JeffO
1
@ GlenH7 Las cadenas en C # son valores de referencia (pueden ser nulos). Sé que int es un valor primitivo, pero fue útil para mostrar el uso de quizás.
aochagavia

Respuestas:

15

Creo que es principalmente equipaje histórico.

El lenguaje más prominente y antiguo con nullC y C ++. Pero aquí, nulltiene sentido. Los punteros siguen siendo un concepto bastante numérico y de bajo nivel. Y cómo alguien más dijo, en la mentalidad de los programadores de C y C ++, tener que decir explícitamente que el puntero puede ser nullno tiene sentido.

En segundo lugar viene Java. Teniendo en cuenta que los desarrolladores de Java estaban tratando de acercarse a C ++, para que pudieran hacer la transición de C ++ a Java más simple, probablemente no querían meterse con ese concepto central del lenguaje. Además, la implementación explícita nullrequeriría mucho más esfuerzo, ya que debe verificar si la referencia no nula se establece correctamente después de la inicialización.

Todos los demás lenguajes son iguales a Java. Por lo general, copian la forma en que C ++ o Java lo hacen, y teniendo en cuenta cómo es el concepto central de implícito nullde los tipos de referencia, se vuelve realmente difícil diseñar un lenguaje que esté usando explícito null.

Eufórico
fuente
"Probablemente no quisieron meterse con ese concepto central del lenguaje". Ya eliminaron por completo los punteros, y eliminarlos nullno sería un gran cambio, creo.
svick
2
@svick Para Java, las referencias son sustitutos de los punteros. Y en muchos casos, los punteros en C ++ se usaron de la misma manera que las referencias de Java. Algunas personas incluso afirman que Java tiene punteros ( programmers.stackexchange.com/questions/207196/… )
Eufórico el
He marcado esto como la respuesta correcta. Me gustaría votarlo pero no tengo suficiente reputación.
aochagavia
1
De acuerdo. Tenga en cuenta que en C ++ (a diferencia de Java y C), ser nulo es la excepción. std::stringNo puede ser null. int&No puede ser null. Una int*lata, y C ++ permite el acceso sin control a ella por dos razones: 1. porque C lo hizo y 2. porque se supone que debes entender lo que estás haciendo cuando usas punteros sin formato en C ++.
MSalters
@MSalters: si un tipo no tiene un valor predeterminado de copia de bits, crear una matriz de ese tipo requerirá llamar a un constructor para cada elemento antes de permitir el acceso a la matriz en sí. Esto puede requerir un trabajo inútil (si se sobrescriben algunos o todos los elementos antes de que se lean), puede presentar complicaciones si el constructor de un elemento posterior falla después de que se haya construido un elemento anterior, y puede terminar realmente no logrando mucho de todos modos (si un valor adecuado para algunos elementos de la matriz no podría determinarse sin leer otros).
supercat
15

En realidad, nulles una gran idea. Dado un puntero, queremos designar que este puntero no hace referencia a un valor válido. Por lo tanto, tomamos una ubicación de memoria, la declaramos inválida y nos apegamos a esa convención (una convención que a veces se aplica con segfaults). Ahora, cada vez que tengo un puntero, puedo verificar si contiene Nothing( ptr == null) o Some(value)( ptr != null, value = *ptr). Quiero que entiendas que esto es equivalente a un Maybetipo.

Los problemas con esto son:

  1. En muchos idiomas, el sistema de tipos no ayuda aquí para garantizar una referencia no nula.

    Este es un bagaje histórico, ya que muchos lenguajes imperativos convencionales o OOP solo han tenido avances incrementales en sus sistemas de tipo en comparación con sus predecesores. Los pequeños cambios tienen la ventaja de que los nuevos idiomas son más fáciles de aprender. C # es un lenguaje convencional que ha introducido herramientas de nivel de lenguaje para manejar mejor los valores nulos.

  2. Los diseñadores de API pueden regresar nullen caso de falla, pero no una referencia a lo real en sí mismo en el éxito. A menudo, la cosa (sin una referencia) se devuelve directamente. Este aplanamiento de un nivel de puntero hace que sea imposible usarlo nullcomo valor.

    Esto es solo pereza por parte del diseñador y no se puede evitar sin imponer un anidamiento adecuado con un sistema de tipo adecuado. Algunas personas también pueden tratar de justificar esto con consideraciones de rendimiento o con la existencia de comprobaciones opcionales (una colección puede devolver nullo el artículo en sí, pero también proporcionar un containsmétodo).

  3. En Haskell hay una vista clara sobre el Maybetipo como una mónada. Esto facilita componer transformaciones en el valor contenido.

    Por otro lado, los lenguajes de bajo nivel como C apenas tratan las matrices como un tipo separado, por lo que no estoy seguro de lo que esperamos. En los lenguajes OOP con polimorfismo parametrizado, un Maybetipo verificado en tiempo de ejecución es bastante trivial de implementar.

amon
fuente
"C # está alejando pasos de referencias nulas". ¿Qué pasos son esos? No he visto nada de eso en los cambios al idioma. ¿O quiere decir simplemente que las bibliotecas comunes están usando nullmenos de lo que hicieron en el pasado?
svick
@svick Mal fraseo de mi parte. " C # es un lenguaje convencional que ha introducido herramientas de nivel de lenguaje para manejar mejor nulls " - Estoy hablando de los Nullabletipos y el ??operador predeterminado. No resuelve el problema en este momento en presencia de código heredado, pero es un paso hacia un futuro mejor.
amon
Estoy de acuerdo contigo. Votaría su respuesta, pero no tengo suficiente reputación :( Sin embargo, Nullable funciona solo para tipos primitivos. Por lo tanto, es solo un pequeño paso.
aochagavia
1
@svick Nullable no tiene nada que ver con esto. Estamos hablando de todos los tipos de referencia que permiten implícitamente un valor nulo, en lugar de que el programador lo defina explícitamente. Y Nullable solo se puede usar en tipos de valor.
Eufórico
@Eufórico Creo que su comentario fue una respuesta a amon, no lo mencioné Nullable.
svick
9

Entiendo que nullera una construcción necesaria para abstraer los lenguajes de programación fuera del ensamblaje. 1 Los programadores necesitaban la capacidad de indicar que un puntero o valor de registro era not a valid valuey se nullconvirtió en el término común para ese significado.

Reforzando el punto que nulles solo una convención para representar un concepto, el valor real para nullpoder / puede variar según el lenguaje de programación y la plataforma.

Si estuviera diseñando un nuevo idioma y quisiera evitarlo, nullpero en su maybelugar lo usaría , entonces recomendaría un término más descriptivo como not a valid valueo navvpara indicar la intención. Pero el nombre de ese no-valor es un concepto separado de si debería permitir que existan incluso valores en su idioma.

Antes de poder decidir sobre cualquiera de esos dos puntos, debe definir qué significaría el significado de maybepara su sistema. Puede encontrar que es solo un cambio de nombre del nullsignificado de not a valid valueo puede encontrar que tiene una semántica diferente para su idioma.

Del mismo modo, la decisión de verificar el acceso con el nullacceso o la referencia es otra decisión de diseño de su idioma.

Para proporcionar un poco de historia, Ctenía una suposición implícita de que los programadores entendían lo que intentaban hacer al manipular la memoria. Como era una abstracción superior al ensamblaje y los lenguajes imperativos que lo precedieron, me aventuraría a que no se les ocurriera la idea de proteger al programador de una referencia incorrecta.

Creo que algunos compiladores o sus herramientas adicionales pueden proporcionar una medida de verificación contra el acceso de puntero no válido. Entonces, otros han notado este problema potencial y han tomado medidas para protegerse contra él.

Si debe permitirlo o no, depende de lo que desea que logre su idioma y del grado de responsabilidad que desea llevar a los usuarios de su idioma. También depende de su capacidad de crear un compilador para restringir ese tipo de comportamiento.

Entonces para responder a sus preguntas:

  1. "Qué tipo de argumentos ..." - Bueno, depende de lo que quieras que haga el lenguaje. Si desea simular el acceso de metal desnudo, entonces puede permitirlo.

  2. "¿es solo equipaje histórico?" Quizás, quizás no. nullciertamente tuvo / tiene significado para varios idiomas y ayuda a impulsar la expresión de esos idiomas. El precedente histórico puede haber afectado los idiomas más recientes y su aceptación, nullpero es un poco demasiado difícil agitar las manos y declarar el concepto de equipaje histórico inútil.


1 Vea este artículo de Wikipedia, aunque se da crédito a Hoare por valores nulos y lenguajes orientados a objetos. Creo que los idiomas imperativos progresaron a lo largo de un árbol genealógico diferente al de Algol.


fuente
El punto es que a la mayoría de las variables en, por ejemplo, C # o Java, se les puede asignar una referencia nula. Parece que sería mucho mejor asignar referencias nulas solo a objetos que indiquen explícitamente que "Quizás" no hay referencia. Entonces, mi pregunta es sobre el "concepto" nulo, y no la palabra.
aochagavia
2
"Las referencias de puntero nulo pueden aparecer como errores durante la compilación" ¿Qué compiladores pueden hacer eso?
svick
Para ser completamente justos, nunca se asigna una referencia nula a un objeto ... la referencia al objeto simplemente no existe (la referencia apunta a nada (0x000000000), que es por definición null).
mgw854
La cita de la especificación C99 habla del carácter nulo , no del puntero nulo , esos son dos conceptos muy diferentes.
svick
2
@ GlenH7 No hace eso por mí. El código se object o = null; o.ToString();compila muy bien para mí, sin errores ni advertencias en VS2012. ReSharper se queja de eso, pero ese no es el compilador.
svick
7

Si observa los ejemplos en el artículo que citó, la mayoría de las veces el uso Maybeno acorta el código. No obvia la necesidad de verificar Nothing. La única diferencia es que le recuerda hacerlo a través del sistema de tipos.

Tenga en cuenta que digo "recordar", no forzar. Los programadores son flojos. Si un programador está convencido de que un valor no puede serlo Nothing, desreferenciará el Maybesin verificarlo, al igual que desreferencia un puntero nulo ahora. El resultado final es que convierte una excepción de puntero nulo en una excepción de "vacío tal vez desreferenciado".

El mismo principio de la naturaleza humana se aplica en otras áreas donde los lenguajes de programación intentan obligar a los programadores a hacer algo. Por ejemplo, los diseñadores de Java intentaron obligar a las personas a manejar la mayoría de las excepciones, lo que resultó en una gran cantidad de repeticiones que ignora silenciosamente o propaga ciegamente las excepciones.

Lo que Maybees bueno es cuando se toman muchas decisiones a través de la coincidencia de patrones y el polimorfismo en lugar de verificaciones explícitas. Por ejemplo, podría crear funciones separadas processData(Some<T>)y con las processData(Nothing<T>)que no puede hacer nada null. Automáticamente mueve su manejo de errores a una función separada, lo cual es muy deseable en la programación funcional donde las funciones se pasan y evalúan de manera perezosa en lugar de llamarse siempre de arriba hacia abajo. En OOP, la forma preferida de desacoplar su código de manejo de errores es con excepciones.

Karl Bielefeldt
fuente
¿Crees que sería una característica deseable en un nuevo lenguaje OO?
aochagavia
Puede implementarlo usted mismo si desea obtener los beneficios del polimorfismo. Lo único para lo que necesita soporte de idiomas es la no anulabilidad. Me encantaría ver una manera de especificar eso para usted, similar const, pero hacerlo opcional. Algunos tipos de código de nivel inferior, como las listas vinculadas, por ejemplo, serían muy molestos de implementar con objetos no anulables.
Karl Bielefeldt
2
Sin embargo, no tiene que consultar con el tipo Quizás. El tipo Quizás es una mónada, por lo que debe tener las funciones map :: Maybe a -> (a -> b) y bind :: Maybe a -> (a -> Maybe b)definirse en él, para que pueda continuar y enhebrar más cálculos en su mayor parte con la refundición utilizando una instrucción if else. Y le getValueOrDefault :: Maybe a -> (() -> a) -> apermite manejar el caso anulable. Es mucho más elegante que la coincidencia de patrones Maybe aexplícitamente.
DetriusXii
1

Maybees una forma muy funcional de pensar un problema: hay una cosa y puede o no tener un valor definido. Sin embargo, en un sentido orientado a objetos, reemplazamos esa idea de una cosa (no importa si tiene un valor o no) con un objeto. Claramente, un objeto tiene un valor. Si no es así, decimos que el objeto es null, pero lo que realmente queremos decir es que no hay ningún objeto en absoluto. La referencia que tenemos al objeto no apunta a nada. La traducción Maybea un concepto OO no tiene nada de novedoso, de hecho, solo genera un código más desordenado. Aún debe tener una referencia nula para el valor deMaybe<T>. Todavía tiene que hacer verificaciones nulas (de hecho, tiene que hacer muchas más verificaciones nulas, abarrotando su código), incluso si ahora se llaman "tal vez verificaciones". Claro, escribirás un código más robusto como afirma el autor, pero diría que es solo el caso porque has hecho el lenguaje mucho más abstracto y obtuso, lo que requiere un nivel de trabajo que es innecesario en la mayoría de los casos. De buena gana tomaría una NullReferenceException de vez en cuando que lidiar con el código de espagueti haciendo una comprobación Quizás cada vez que accedo a una nueva variable.

mgw854
fuente
2
Creo que conduciría a verificaciones menos nulas, porque solo tiene que verificar si ve un Quizás <T> y no tiene que preocuparse por el resto de los tipos.
aochagavia
1
@svick Maybe<T>tiene que permitir nullcomo valor, porque el valor puede no existir. Si tengo Maybe<MyClass>, y no tiene un valor, el campo de valor debe contener una referencia nula. No hay nada más que sea verificablemente seguro.
mgw854
1
@ mgw854 Claro que sí. En lenguaje OO, Maybepodría ser una clase abstracta, con dos clases que heredan de ella: Some(que tiene un campo para el valor) y None(que no tiene ese campo). De esa manera, el valor nunca es null.
svick
66
Con "no nulabilidad" de forma predeterminada y Quizás <T> puede estar seguro de que algunas variables siempre contienen objetos. A saber, todas las variables que no son tal vez <T>
aochagavia
3
@ mgw854 El objetivo de este cambio sería un mayor poder expresivo para el desarrollador. En este momento, el desarrollador siempre debe suponer que la referencia o el puntero pueden ser nulos y deben comprobarse para asegurarse de que haya un valor utilizable. Al implementar este cambio, le da poder al desarrollador para decir que realmente necesita un valor válido y tiene una verificación del compilador para asegurarse de que se haya pasado el valor válido. Pero aún así da la opción de desarrollador, para optar y no pasar un valor.
Eufórico
1

El concepto de nullpuede remontarse fácilmente a C, pero ese no es el problema.

Mi lenguaje de elección diario es C # y me quedaría nullcon una diferencia. C # tiene dos tipos de tipos, valores y referencias . Los valores nunca pueden ser null, pero a veces me gustaría poder expresar que ningún valor está perfectamente bien. Para hacer esto, C # usa Nullabletipos, por intlo que sería el valor y int?el valor anulable. Así es como creo que los tipos de referencia también deberían funcionar.

Ver también: la referencia nula puede no ser un error :

Las referencias nulas son útiles y a veces indispensables (considere la cantidad de problemas si puede o no devolver una cadena en C ++). El error realmente no está en la existencia de punteros nulos, sino en cómo los maneja el sistema de tipos. Desafortunadamente, la mayoría de los lenguajes (C ++, Java, C #) no los manejan correctamente.

Daniel Little
fuente
0

Creo que esto se debe a que la programación funcional está muy preocupada por los tipos , especialmente los tipos que combinan otros tipos (tuplas, funciones como tipos de primera clase, mónadas, etc.) que la programación orientada a objetos (o al menos inicialmente lo hizo).

Las versiones modernas de los lenguajes de programación de los que creo que estás hablando (C ++, C #, Java) se basan en lenguajes que no tenían ninguna forma de programación genérica (C, C # 1.0, Java 1). Sin eso, aún puede generar algún tipo de diferencia entre los objetos anulables y no anulables en el lenguaje (como las referencias de C ++, que no pueden ser null, pero también son limitadas), pero es mucho menos natural.

svick
fuente
Creo que en el caso de la programación funcional, es el caso de FP que no tiene tipos de referencia o puntero. En FP todo es un valor. Y si tiene un tipo de puntero, se vuelve fácil decir "puntero a nada".
Eufórico
0

Creo que la razón fundamental es que se requieren relativamente pocas verificaciones nulas para que un programa sea "seguro" contra la corrupción de datos. Si un programa intenta usar el contenido de un elemento de matriz u otra ubicación de almacenamiento que se supone que se escribió con una referencia válida pero no lo fue, el mejor resultado es que se produzca una excepción. Idealmente, la excepción indicará exactamente dónde ocurrió el problema, pero lo que importa es que se arroje algún tipo de excepción antes de que la referencia nula se almacene en algún lugar que pueda causar corrupción de datos. A menos que un método almacene un objeto sin intentar usarlo de alguna manera primero, un intento de usar un objeto constituirá, en sí mismo, una especie de "verificación nula".

Si uno quiere asegurarse de que una referencia nula que aparece donde no debería causar una excepción particular que no sea NullReferenceException, a menudo será necesario incluir verificaciones nulas en todo el lugar. Por otro lado, el simple hecho de garantizar que se produzca alguna excepción antes de que una referencia nula pueda causar un "daño" más allá de cualquier cosa que ya se haya hecho a menudo requerirá relativamente pocas pruebas; las pruebas generalmente solo se requerirán en los casos en que un objeto almacenaría un referencia sin tratar de usarlo, y la referencia nula sobrescribiría una válida o haría que otro código malinterpretara otros aspectos del estado del programa. Tales situaciones existen, pero no son tan comunes; la mayoría de las referencias nulas accidentales quedarán atrapadas muy rápidamentesi uno los busca o no .

Super gato
fuente
esta publicación es bastante difícil de leer (muro de texto). ¿Te importaría editarlo en una mejor forma?
mosquito
1
@gnat: ¿Eso está mejor?
supercat
0

" Maybe," como está escrito, es una construcción de nivel más alto que nulo. Usando más palabras para definirlo, Quizás es "un puntero a una cosa o un puntero a nada, pero el compilador aún no ha recibido suficiente información para determinar cuál". Esto lo obliga a verificar explícitamente cada valor constantemente, a menos que construya una especificación del compilador que sea lo suficientemente inteligente como para mantenerse al día con el código que escribe.

Puede realizar una implementación de Maybe con un lenguaje que tenga valores nulos fácilmente. C ++ tiene uno en forma de boost::optional<T>. Hacer el equivalente de nulo con Quizás es muy difícil. En particular, si tengo un Maybe<Just<T>>, no puedo asignarlo a nulo (porque dicho concepto no existe), mientras que T**en un lenguaje con nulo es muy fácil de asignar a nulo. Esto obliga a uno a usar Maybe<Maybe<T>>, lo cual es totalmente válido, pero lo obligará a hacer muchas más comprobaciones para usar ese objeto.

Algunos lenguajes funcionales usan Quizás porque nulo requiere un comportamiento indefinido o manejo de excepciones, ninguno de los cuales es un concepto fácil de mapear en sintaxis de lenguaje funcional. Quizás desempeña el papel mucho mejor en tales situaciones funcionales, pero en lenguajes de procedimiento, nulo es el rey. No se trata de lo correcto y lo incorrecto, sino de lo que hace que sea más fácil decirle a la computadora que haga lo que usted quiere que haga.

Cort Ammon - Restablece a Monica
fuente