Así es como lo hace Haskell: (no es exactamente un contador de las declaraciones de Lippert ya que Haskell no es un lenguaje orientado a objetos).
ADVERTENCIA: larga respuesta sin aliento de un fanático serio de Haskell por delante.
TL; DR
Este ejemplo ilustra exactamente cuán diferente es Haskell de C #. En lugar de delegar la logística de la construcción de la estructura a un constructor, debe manejarse en el código circundante. No hay forma de Nothingque aparezca un valor de valor nulo (o en Haskell) donde esperamos un valor no nulo porque los valores nulos solo pueden ocurrir dentro de tipos especiales de envoltorios llamados Maybeque no son intercambiables con / directamente convertibles a regulares, no tipos anulables. Para utilizar un valor anulado envolviéndolo en a Maybe, primero debemos extraer el valor utilizando la coincidencia de patrones, lo que nos obliga a desviar el flujo de control hacia una rama donde sabemos con certeza que tenemos un valor no nulo.
Por lo tanto:
¿podemos saber siempre que una referencia no anulable nunca se considera inválida bajo ninguna circunstancia?
Si. Inty Maybe Intson dos tipos completamente separados. Encontrar Nothingen una llanura Intsería comparable a encontrar la cadena "pez" en un Int32.
¿Qué pasa en el constructor de un objeto con un campo de tipo de referencia no anulable?
No es un problema: los constructores de valores en Haskell no pueden hacer nada más que tomar los valores que se les dan y juntarlos. Toda la lógica de inicialización tiene lugar antes de que se llame al constructor.
¿Qué pasa con el finalizador de dicho objeto, donde el objeto se finaliza porque el código que se suponía que debía completar la referencia arrojó una excepción?
No hay finalizadores en Haskell, por lo que realmente no puedo abordar esto. Sin embargo, mi primera respuesta sigue en pie.
Respuesta completa :
Haskell no tiene nulo y utiliza el Maybetipo de datos para representar valores anulables. Quizás es un tipo de datos algabraicos definido de esta manera:
data Maybe a = Just a | Nothing
Para aquellos de ustedes que no están familiarizados con Haskell, lean esto como "A Maybees una Nothingo una Just a". Específicamente:
Maybees el constructor de tipos : puede considerarse (incorrectamente) como una clase genérica (donde aestá la variable de tipo). La analogía de C # es class Maybe<a>{}.
Justes un constructor de valores : es una función que toma un argumento de tipo ay devuelve un valor de tipo Maybe aque contiene el valor. Entonces el código x = Just 17es análogo a int? x = 17;.
Nothinges otro constructor de valores, pero no necesita argumentos y el Maybevalor devuelto no tiene otro valor que no sea "Nothing". x = Nothinges análogo a int? x = null;(suponiendo que limitamos nuestro ser aen Haskell Int, lo que se puede hacer escribiendo x = Nothing :: Maybe Int).
Ahora que los conceptos básicos del Maybetipo están fuera del camino, ¿cómo evita Haskell los problemas discutidos en la pregunta del OP?
Bueno, Haskell es realmente diferente de la mayoría de los idiomas discutidos hasta ahora, así que comenzaré explicando algunos principios básicos del lenguaje.
En primer lugar, en Haskell, todo es inmutable . Todo. Los nombres se refieren a valores, no a ubicaciones de memoria donde se pueden almacenar valores (esto solo es una fuente enorme de eliminación de errores). A diferencia de C #, donde la declaración y la asignación de variables son dos operaciones separadas, en Haskell los valores se crean definiendo su valor (p x = 15. Ej . y = "quux", z = Nothing), Que nunca puede cambiar. Por lo tanto, código como:
ReferenceType x;
No es posible en Haskell. No hay problemas con la inicialización de valores nullporque todo debe inicializarse explícitamente en un valor para que exista.
En segundo lugar, Haskell no es un lenguaje orientado a objetos : es un lenguaje puramente funcional , por lo que no hay objetos en el sentido estricto de la palabra. En cambio, simplemente hay funciones (constructores de valores) que toman sus argumentos y devuelven una estructura amalgamada.
A continuación, no hay absolutamente ningún código de estilo imperativo. Con esto, quiero decir que la mayoría de los idiomas siguen un patrón similar a este:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
El comportamiento del programa se expresa como una serie de instrucciones. En los lenguajes orientados a objetos, las declaraciones de clase y función también juegan un papel importante en el flujo del programa, pero es esencial, la "carne" de la ejecución de un programa toma la forma de una serie de instrucciones para ser ejecutadas.
En Haskell, esto no es posible. En cambio, el flujo del programa se dicta completamente mediante el encadenamiento de funciones. Incluso la donotación de aspecto imperativo es simplemente azúcar sintáctica para pasar funciones anónimas al >>=operador. Todas las funciones toman la forma de:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
Donde body-expressionpuede haber cualquier cosa que se evalúe como un valor. Obviamente, hay más funciones de sintaxis disponibles, pero el punto principal es la ausencia total de secuencias de declaraciones.
Por último, y probablemente lo más importante, el sistema de tipos de Haskell es increíblemente estricto. Si tuviera que resumir la filosofía de diseño central del sistema de tipos de Haskell, diría: "Haga que salgan mal tantas cosas como sea posible en el momento de la compilación para que lo menos posible salga mal en tiempo de ejecución". No hay conversiones implícitas en absoluto (¿desea promover una Inta una Double? Utilice la fromIntegralfunción). Lo único que posiblemente tenga un valor no válido en tiempo de ejecución es usarlo Prelude.undefined(que aparentemente solo tiene que estar allí y es imposible de eliminar ).
Con todo esto en mente, veamos el ejemplo "roto" de amon e intentemos volver a expresar este código en Haskell. Primero, la declaración de datos (usando la sintaxis de registro para los campos con nombre):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
( fooy barrealmente son funciones de acceso a campos anónimos aquí en lugar de campos reales, pero podemos ignorar este detalle).
El NotSoBrokenconstructor de datos es incapaz de tomar cualquier acción que no sea tomando una Fooy una Bar(que no son anulables) y haciendo una NotSoBrokende ellas. No hay lugar para poner código imperativo o incluso asignar manualmente los campos. Toda la lógica de inicialización debe tener lugar en otro lugar, muy probablemente en una función de fábrica dedicada.
En el ejemplo, la construcción de Brokensiempre falla. No hay forma de romper el NotSoBrokenconstructor de valores de manera similar (simplemente no hay ningún lugar para escribir el código), pero podemos crear una función de fábrica que sea igualmente defectuosa.
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(la primera línea es una declaración de firma de tipo: makeNotSoBrokentoma a Fooy a Barcomo argumentos y produce a Maybe NotSoBroken).
El tipo de retorno debe ser Maybe NotSoBrokeny no simplemente NotSoBrokenporque le dijimos que evaluara Nothing, que es un constructor de valores para Maybe. Los tipos simplemente no se alinearían si escribiéramos algo diferente.
Además de ser absolutamente inútil, esta función ni siquiera cumple su propósito real, como veremos cuando intentemos usarla. Creemos una función llamada useNotSoBrokenque espera a NotSoBrokencomo argumento:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBrokenacepta a NotSoBrokencomo argumento y produce a Whatever).
Y úsalo así:
useNotSoBroken (makeNotSoBroken)
En la mayoría de los idiomas, este tipo de comportamiento puede causar una excepción de puntero nulo. En Haskell, los tipos no coinciden: makeNotSoBrokendevuelve a Maybe NotSoBroken, pero useNotSoBrokenespera a NotSoBroken. Estos tipos no son intercambiables y el código no se compila.
Para evitar esto, podemos usar una casedeclaración para ramificar en función de la estructura del Maybevalor (usando una característica llamada coincidencia de patrones ):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
Obviamente, este fragmento debe colocarse dentro de algún contexto para compilar realmente, pero demuestra los conceptos básicos de cómo Haskell maneja los valores nulables. Aquí hay una explicación paso a paso del código anterior:
- Primero,
makeNotSoBrokense evalúa, lo que garantiza que producirá un valor de tipo Maybe NotSoBroken.
- La
casedeclaración inspecciona la estructura de este valor.
- Si el valor es
Nothing, se evalúa el código "manejar situación aquí".
- Si el valor coincide con un
Justvalor, se ejecuta la otra rama. Observe cómo la cláusula de coincidencia identifica simultáneamente el valor como una Justconstrucción y vincula su NotSoBrokencampo interno a un nombre (en este caso x). xentonces puede usarse como el NotSoBrokenvalor normal que es.
Por lo tanto, la coincidencia de patrones proporciona una función poderosa para hacer cumplir la seguridad de tipos, ya que la estructura del objeto está inseparablemente unida a la ramificación del control.
Espero que esta haya sido una explicación comprensible. Si no tiene sentido, ¡entra en Learn You A Haskell For Great Good! , uno de los mejores tutoriales de idiomas en línea que he leído. Espero que veas la misma belleza en este idioma que yo.