¿Qué significa la sintaxis "Just" en Haskell?

118

He buscado en Internet una explicación real de lo que hace esta palabra clave. Cada tutorial de Haskell que he visto comienza a usarlo al azar y nunca explica lo que hace (y he visto muchos).

Aquí hay una pieza básica de código de Real World Haskell que usa Just. Entiendo lo que hace el código, pero no entiendo cuál es su propósito o función Just.

lend amount balance = let reserve    = 100
                      newBalance = balance - amount
                  in if balance < reserve
                     then Nothing
                     else Just newBalance

Por lo que he observado, se relaciona con Maybe mecanografía, pero eso es prácticamente todo lo que he logrado aprender.

Se Justagradecería mucho una buena explicación de lo que significa.

reem
fuente

Respuestas:

211

En realidad, es solo un constructor de datos normal que se define en el Preludio , que es la biblioteca estándar que se importa automáticamente en cada módulo.

Lo que quizás es, estructuralmente

La definición se parece a esto:

data Maybe a = Just a
             | Nothing

Esa declaración define un tipo, Maybe aque está parametrizado por una variable de tipo a, lo que solo significa que puede usarlo con cualquier tipo en lugar de a.

Construyendo y Destruyendo

El tipo tiene dos constructores Just ay Nothing. Cuando un tipo tiene varios constructores, significa que un valor del tipo debe haberse construido con solo uno de los posibles constructores. Para este tipo, se construyó un valor mediante Justo Nothing, no hay otras posibilidades (sin error).

Dado que Nothingno tiene un tipo de parámetro, cuando se usa como constructor, nombra un valor constante que es un miembro de tipo Maybe apara todos los tipos a. Pero el Justconstructor tiene un parámetro de tipo, lo que significa que cuando se usa como constructor actúa como una función de tipo aa Maybe a, es decir, tiene el tipoa -> Maybe a

Entonces, los constructores de un tipo construyen un valor de ese tipo; el otro lado de las cosas es cuando le gustaría usar ese valor, y ahí es donde entra en juego la coincidencia de patrones. A diferencia de las funciones, los constructores se pueden usar en expresiones de enlace de patrones, y esta es la forma en que puede realizar análisis de casos de valores que pertenecen a tipos con más de un constructor.

Para usar un Maybe avalor en una coincidencia de patrones, debe proporcionar un patrón para cada constructor, así:

case maybeVal of
    Nothing   -> "There is nothing!"
    Just val  -> "There is a value, and it is " ++ (show val)

En esa expresión de caso, el primer patrón coincidiría si el valor fuera Nothing, y el segundo coincidiría si el valor se construyera con Just. Si el segundo coincide, también vincula el nombre valal parámetro que se pasó al Justconstructor cuando se construyó el valor con el que está haciendo coincidir.

Lo que quizás significa

Quizás ya estaba familiarizado con cómo funcionaba esto; Realmente no hay magia en los Maybevalores, es solo un tipo de datos algebraicos Haskell (ADT) normal. Pero se usa bastante porque efectivamente "eleva" o extiende un tipo, como el Integerde su ejemplo, a un nuevo contexto en el que tiene un valor adicional ( Nothing) que representa una falta de valor. El sistema de tipos requiere que verifique ese valor adicional antes de que le permita obtener el Integerque podría estar allí. Esto evita una gran cantidad de errores.

Hoy en día, muchos lenguajes manejan este tipo de valor "sin valor" a través de referencias NULL. Tony Hoare, un científico informático eminente (inventó Quicksort y es un ganador del premio Turing), reconoce esto como su "error de mil millones de dólares" . El tipo Maybe no es la única forma de solucionar este problema, pero ha demostrado ser una forma eficaz de hacerlo.

Quizás como Functor

La idea de transformar un tipo en otro de modo que las operaciones en el tipo antiguo también se puedan transformar para trabajar en el nuevo tipo es el concepto detrás de la clase de tipos Haskell llamada Functor, que Maybe atiene una instancia útil de.

Functorproporciona un método llamado fmap, que asigna funciones que varían sobre valores desde el tipo base (como Integer) a funciones que varían sobre valores desde el tipo elevado (como Maybe Integer). Una función transformada con fmappara trabajar en un Maybevalor funciona así:

case maybeVal of
  Nothing  -> Nothing         -- there is nothing, so just return Nothing
  Just val -> Just (f val)    -- there is a value, so apply the function to it

Entonces, si tiene un Maybe Integervalor m_xy una Int -> Intfunción f, puede fmap f m_xaplicar la función fdirectamente al Maybe Integersin preocuparse si realmente tiene un valor o no. De hecho, podría aplicar una cadena completa de Integer -> Integerfunciones elevadas a los Maybe Integervalores y solo tendrá que preocuparse por verificar explícitamente Nothinguna vez cuando haya terminado.

Quizás como una mónada

No estoy seguro de qué tan familiarizado está con el concepto de un Monadtodavía, pero al menos lo ha usado IO aantes, y la firma de tipo se IO ave muy similar a Maybe a. Aunque IOes especial en el sentido de que no le expone sus constructores y, por lo tanto, solo puede ser "ejecutado" por el sistema de tiempo de ejecución de Haskell, también es un Functorcomplemento de Monad. De hecho, hay un sentido importante en el que a Monades solo un tipo especial Functorcon algunas características adicionales, pero este no es el lugar para entrar en eso.

De todos modos, a las mónadas les gustan IOlos tipos de mapas de nuevos tipos que representan "cálculos que dan como resultado valores" y puedes convertir funciones en Monadtipos a través de una fmapfunción muy similar a la llamada liftMque convierte una función regular en un "cálculo que da como resultado el valor obtenido al evaluar el función."

Probablemente haya adivinado (si ha leído hasta aquí) que Maybetambién es un Monad. Representa "cálculos que podrían no devolver un valor". Al igual que con el fmapejemplo, esto le permite hacer un montón de cálculos sin tener que buscar errores explícitamente después de cada paso. Y de hecho, la forma en que Monadse construye la instancia, un cálculo de Maybevalores se detiene tan pronto como Nothingse encuentra a, por lo que es como un aborto inmediato o una devolución sin valor en medio de un cálculo.

Podrías haber escrito quizás

Como dije antes, no hay nada inherente al Maybetipo que está integrado en la sintaxis del lenguaje o en el sistema de ejecución. Si Haskell no lo proporcionó de forma predeterminada, ¡usted mismo podría proporcionar todas sus funciones! De hecho, podría volver a escribirlo usted mismo de todos modos, con diferentes nombres, y obtener la misma funcionalidad.

Espero que entiendas el Maybetipo y sus constructores ahora, pero si todavía hay algo que no está claro, ¡avísame!

Levi Pearson
fuente
19
¡Qué excelente respuesta! Cabe mencionar que Haskell a menudo usa Maybedonde otros lenguajes usarían nullo nil(con desagradables NullPointerExceptions acechando en cada esquina). Ahora, otros lenguajes también comienzan a usar esta construcción: Scala as Option, e incluso Java 8 tendrá el Optionaltipo.
Landei
3
Esta es una excelente explicación. Muchas de las explicaciones que he leído insinúan la idea de que Just es un constructor para el tipo Maybe, pero ninguna lo hizo realmente explícito.
reem
@Landei, gracias por la sugerencia. Hice una edición para referirme al peligro de referencias nulas.
Levi Pearson
@Landei El tipo de opción ha existido desde ML en los años 70, es más probable que Scala lo haya tomado de allí porque Scala usa la convención de nomenclatura de ML, Opción con constructores algunos y ninguno.
Stonemetal
@Landei Swift de Apple también utiliza bastante los opcionales
Jamin
37

La mayoría de las respuestas actuales son explicaciones altamente técnicas de cómo Justfuncionan los amigos; Pensé que podría intentar explicar para qué sirve.

Muchos lenguajes tienen un valor como nullese que se puede usar en lugar de un valor real, al menos para algunos tipos. Esto ha enfurecido a mucha gente y ha sido considerado como una mala decisión. Aún así, a veces es útil tener un valor como nullpara indicar la ausencia de algo.

Haskell resuelve este problema haciéndote marcar explícitamente lugares donde puedes tener un Nothing(su versión de a null). Básicamente, si su función normalmente devolvería el tipo Foo, en su lugar debería devolver el tipo Maybe Foo. Si desea indicar que no hay valor, regrese Nothing. Si desea devolver un valor bar, debe volver Just bar.

Básicamente, si no puede tener Nothing, no lo necesita Just. Si puede Nothing, lo necesita Just.

No tiene nada de mágico Maybe; está construido sobre el sistema de tipos Haskell. Eso significa que puede usar todos los trucos habituales de coincidencia de patrones de Haskell con él.

Brent Royal-Gordon
fuente
1
Muy buena respuesta junto a las otras respuestas, pero creo que aún se beneficiaría de un ejemplo de código :)
PascalVKooten
13

Dado un tipo t, un valor de Just tes un valor de tipo existente t, donde Nothingrepresenta una falla para alcanzar un valor, o un caso en el que tener un valor no tendría sentido.

En su ejemplo, tener un saldo negativo no tiene sentido, por lo que, si tal cosa ocurriera, se reemplaza por Nothing.

Para otro ejemplo, esto podría usarse en la división, definiendo una función de división que toma ay b, y devuelve Just a/bsi bes distinto de cero, y de Nothingotro modo. A menudo se usa así, como una alternativa conveniente a las excepciones, o como su ejemplo anterior, para reemplazar valores que no tienen sentido.

qaphla
fuente
1
Entonces, digamos que en el código anterior eliminé el Just, ¿por qué no funcionaría? ¿Tienes que tener Just en cualquier momento que quieras tener Nada? ¿Qué sucede con una expresión donde una función dentro de esa expresión devuelve Nothing?
reem
7
Si eliminó el Just, su código no se marcaría. El motivo Justes mantener los tipos adecuados. Hay un tipo (una mónada, en realidad, pero es más fácil pensar que es solo un tipo) Maybe t, que consta de elementos de la forma Just ty Nothing. Dado que Nothingtiene tipo Maybe t, una expresión que puede evaluarse como uno Nothingo algún valor de tipo tno se escribe correctamente. Si una función regresa Nothingen algunos casos, cualquier expresión que use esa función debe tener alguna forma de verificar eso ( isJusto una declaración de caso), para manejar todos los casos posibles.
qaphla
2
Entonces, Just está ahí para ser consistente dentro del tipo Maybe porque una t regular no está en el tipo Maybe. Todo está mucho más claro ahora. ¡Gracias!
reem
3
@qaphla: Su comentario acerca de que es una "mónada, en realidad, [...]" es engañoso. Maybe t es solo un tipo. El hecho de que haya una Monadinstancia de Maybeno la transforma en algo que no sea un tipo.
Sarah
2

Una función total a-> b puede encontrar un valor de tipo b para cada valor posible de tipo a.

En Haskell no todas las funciones son totales. En este caso particular, la función lendno es total; no está definida para el caso en el que el saldo es menor que la reserva (aunque, a mi gusto, tendría más sentido no permitir que newBalance sea menor que la reserva; tal como está, puede pedir prestado 101 de un saldo de 100).

Otros diseños que se ocupan de funciones no totales:

  • lanzar excepciones al verificar el valor de entrada no se ajusta al rango
  • devolver un valor especial (tipo primitivo): la opción favorita es un valor negativo para las funciones enteras que están destinadas a devolver números naturales (por ejemplo, String.indexOf: cuando no se encuentra una subcadena, el índice devuelto está diseñado comúnmente para ser negativo)
  • devolver un valor especial (puntero): NULL o algo así
  • Devolver silenciosamente sin hacer nada: por ejemplo, se lendpodría escribir para devolver el saldo anterior, si no se cumple la condición para prestar
  • devolver un valor especial: Nada (o envolviendo a la izquierda algún objeto de descripción de error)

Estas son limitaciones de diseño necesarias en lenguajes que no pueden imponer la totalidad de funciones (por ejemplo, Agda puede, pero eso conduce a otras complicaciones, como volverse turing-incompleto).

El problema de devolver un valor especial o lanzar excepciones es que es fácil para la persona que llama omitir el manejo de tal posibilidad por error.

El problema de descartar silenciosamente una falla también es obvio: está limitando lo que la persona que llama puede hacer con la función. Por ejemplo, si se lenddevuelve el saldo anterior, la persona que llama no tiene forma de saber si el saldo ha cambiado. Puede que sea un problema o no, según el propósito previsto.

La solución de Haskell obliga a quien llama a una función parcial a lidiar con el tipo like Maybe a, o Either error adebido al tipo de retorno de la función.

De esta manera, lendtal como se define, es una función que no siempre calcula el nuevo saldo; en algunas circunstancias, no se define el nuevo saldo. Señalamos esta circunstancia a la persona que llama devolviendo el valor especial Nothing o ajustando el nuevo saldo en Just. La persona que llama ahora tiene la libertad de elegir: manejar la falta de préstamo de una manera especial o ignorar y usar el saldo anterior, por ejemplo maybe oldBalance id $ lend amount oldBalance,.

Sassa NF
fuente
-1

La función if (cond :: Bool) then (ifTrue :: a) else (ifFalse :: a)debe tener el mismo tipo de ifTruey ifFalse.

Entonces, cuando escribimos then Nothing, debemos usar Maybe atype inelse f

if balance < reserve
       then (Nothing :: Maybe nb)         -- same type
       else (Just newBalance :: Maybe nb) -- same type
ingenio
fuente
1
Estoy seguro de que quería decir algo muy profundo aquí
sehe
1
Haskell tiene inferencia de tipos. No es necesario indicar el tipo deNothing yJust newBalance explícitamente.
Derecha
Para esta explicación a los no iniciados, los tipos explícitos aclaran el significado del uso de Just.
philip