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 Nothing
que 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 Maybe
que 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. Int
y Maybe Int
son dos tipos completamente separados. Encontrar Nothing
en una llanura Int
serí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 Maybe
tipo 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 Maybe
es una Nothing
o una Just a
". Específicamente:
Maybe
es el constructor de tipos : puede considerarse (incorrectamente) como una clase genérica (donde a
está la variable de tipo). La analogía de C # es class Maybe<a>{}
.
Just
es un constructor de valores : es una función que toma un argumento de tipo a
y devuelve un valor de tipo Maybe a
que contiene el valor. Entonces el código x = Just 17
es análogo a int? x = 17;
.
Nothing
es otro constructor de valores, pero no necesita argumentos y el Maybe
valor devuelto no tiene otro valor que no sea "Nothing". x = Nothing
es análogo a int? x = null;
(suponiendo que limitamos nuestro ser a
en Haskell Int
, lo que se puede hacer escribiendo x = Nothing :: Maybe Int
).
Ahora que los conceptos básicos del Maybe
tipo 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 null
porque 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 do
notació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-expression
puede 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 Int
a una Double
? Utilice la fromIntegral
funció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 }
( foo
y bar
realmente son funciones de acceso a campos anónimos aquí en lugar de campos reales, pero podemos ignorar este detalle).
El NotSoBroken
constructor de datos es incapaz de tomar cualquier acción que no sea tomando una Foo
y una Bar
(que no son anulables) y haciendo una NotSoBroken
de 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 Broken
siempre falla. No hay forma de romper el NotSoBroken
constructor 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: makeNotSoBroken
toma a Foo
y a Bar
como argumentos y produce a Maybe NotSoBroken
).
El tipo de retorno debe ser Maybe NotSoBroken
y no simplemente NotSoBroken
porque 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 useNotSoBroken
que espera a NotSoBroken
como argumento:
useNotSoBroken :: NotSoBroken -> Whatever
( useNotSoBroken
acepta a NotSoBroken
como 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: makeNotSoBroken
devuelve a Maybe NotSoBroken
, pero useNotSoBroken
espera a NotSoBroken
. Estos tipos no son intercambiables y el código no se compila.
Para evitar esto, podemos usar una case
declaración para ramificar en función de la estructura del Maybe
valor (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,
makeNotSoBroken
se evalúa, lo que garantiza que producirá un valor de tipo Maybe NotSoBroken
.
- La
case
declaració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
Just
valor, se ejecuta la otra rama. Observe cómo la cláusula de coincidencia identifica simultáneamente el valor como una Just
construcción y vincula su NotSoBroken
campo interno a un nombre (en este caso x
). x
entonces puede usarse como el NotSoBroken
valor 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.