Estoy empezando a aprender Haskell . Soy muy nuevo en esto, y solo estoy leyendo un par de libros en línea para entender los conceptos básicos.
Uno de los 'memes' de los que las personas familiarizadas con él han hablado a menudo es el asunto "si se compila, funcionará *", lo que creo que está relacionado con la fuerza del sistema de tipos.
Estoy tratando de entender por qué exactamente Haskell es mejor que otros lenguajes estáticamente escritos a este respecto.
Dicho de otra manera, supongo que en Java, podría hacer algo atroz como enterrar
ArrayList<String>()
para contener algo que realmente debería ser ArrayList<Animal>()
. Lo atroz aquí es que string
contiene elephant, giraffe
, etc., y si alguien lo instala Mercedes
, su compilador no lo ayudará.
Si yo hice hacer ArrayList<Animal>()
a continuación, en algún momento posterior en el tiempo, si decido mi programa no es realmente acerca de los animales, se trata de vehículos, entonces puedo cambiar, por ejemplo, una función que produce ArrayList<Animal>
para producir ArrayList<Vehicle>
y mi IDE me informes en todas partes hay Es un descanso de compilación.
Mi suposición es que esto es lo que la gente quiere decir con un sistema de tipo fuerte , pero no es obvio para mí por qué Haskell es mejor. Dicho de otra manera, puedes escribir Java bueno o malo, supongo que puedes hacer lo mismo en Haskell (es decir, meter cosas en cadenas / ints que realmente deberían ser tipos de datos de primera clase).
Sospecho que me falta algo importante / básico.
¡Me encantaría que me mostraran el error de mis caminos!
fuente
Maybe
solo hacia el final. Si tuviera que elegir una cosa que los idiomas más populares deberían tomar prestada de Haskell, sería esta. Es una idea muy simple (por lo que no es muy interesante desde un punto de vista teórico), pero esto solo facilitaría mucho nuestro trabajo.Respuestas:
Aquí hay una lista desordenada de las características del sistema de tipos disponibles en Haskell y no están disponibles o son menos agradables en Java (que yo sepa, que es ciertamente débil wrt Java)
Eq
un compilador de Haskell puede derivar automáticamente algo como la calidad para un tipo definido por el usuario. Esencialmente, la forma en que lo hace es caminar sobre la estructura simple y común que subyace a cualquier tipo definido por el usuario y combinarla entre valores, una forma muy natural de igualdad estructural.data Bt a = Here a | There (Bt (a, a))
. Piense detenidamente en los valores válidosBt a
y observe cómo funciona ese tipo. ¡Es complicado!IO
. Java, en realidad, probablemente tenga una mejor historia de tipo abstracto, para ser honesto, pero no creo que hasta que Interfaces se volviera más popular fuera realmente cierto.mtl
sistema de tipificación de efectos, puntos de fijación de functor generalizados. La lista sigue y sigue. Hay muchas cosas que se expresan mejor en tipos superiores y relativamente pocos sistemas de tipos incluso permiten al usuario hablar sobre estas cosas.(+)
cosas juntos? ¡OhInteger
, está bien! ¡Vamos a insertar el código correcto ahora!". En sistemas más complejos, puede estar estableciendo restricciones más interesantes.mtl
biblioteca se basa en esta idea.(forall a. f a -> g a)
. En HM recta se puede escribir una función en este tipo, pero con tipos de rango superior que exigir una función como un argumento así:mapFree :: (forall a . f a -> g a) -> Free f -> Free g
. Observe que laa
variable está vinculada solo dentro del argumento. Esto significa que el definidor de la funciónmapFree
decide quéa
se instancia cuando se usa, no el usuario demapFree
.Tipos indexados y promoción de tipos . Me estoy volviendo realmente exótico en este punto, pero estos tienen un uso práctico de vez en cuando. Si desea escribir un tipo de identificadores que estén abiertos o cerrados, puede hacerlo muy bien. Observe en el siguiente fragmento que
State
es un tipo algebraico muy simple que también promovió sus valores en el nivel de tipo. Entonces, posteriormente, podemos hablar de constructores de tipos comoHandle
como tomar argumentos específicos a tipos comoState
. Es confuso entender todos los detalles, pero también muy correcto.Representaciones de tipo de tiempo de ejecución que funcionan . Java es conocido por tener borrado de tipo y que esa característica llueva en los desfiles de algunas personas. Sin embargo, el borrado de tipos es el camino correcto, ya que si tiene una función
getRepr :: a -> TypeRepr
, al menos viola la parametricidad. Lo que es peor es que si esa es una función generada por el usuario que se utiliza para desencadenar coacciones inseguras en tiempo de ejecución ... entonces tiene una gran preocupación de seguridad . ElTypeable
sistema de Haskell permite la creación de una caja fuertecoerce :: (Typeable a, Typeable b) => a -> Maybe b
. Este sistema se basa enTypeable
implementarse en el compilador (y no en el país de usuario) y tampoco se le podría dar una semántica tan agradable sin el mecanismo de tipo de clase de Haskell y las leyes que se garantiza que siga.Sin embargo, más que estos, el valor del sistema de tipos de Haskell también se relaciona con la forma en que los tipos describen el lenguaje. Aquí hay algunas características de Haskell que generan valor a través del sistema de tipos.
IO a
a representar cálculos de efectos secundarios que dan como resultado valores de tipoa
. Esta es la base de un sistema de efectos muy agradable incrustado dentro de un lenguaje puro.null
. Todo el mundo sabe quenull
es el error de mil millones de dólares de los lenguajes de programación modernos. Los tipos algebraicos, en particular la capacidad de simplemente agregar un estado "no existe" en los tipos que tiene al transformar un tipoA
en el tipoMaybe A
, mitigan completamente el problema denull
.Bt a
tipo de delante y tratar de escribir una función para calcular su tamaño:size :: Bt a -> Int
. Se verá un poco comosize (Here a) = 1
ysize (There bt) = 2 * size bt
. Operacionalmente, eso no es demasiado complejo, pero tenga en cuenta que la llamada recursivasize
en la última ecuación ocurre en un tipo diferente , sin embargo, la definición general tiene un tipo generalizado agradablesize :: Bt a -> Int
. Tenga en cuenta que esta es una característica que rompe la inferencia total, pero si proporciona una firma de tipo, entonces Haskell lo permitirá.Podría seguir, pero esta lista debería ayudarlo a comenzar, y algo más.
fuente
char *p = NULL;
*p=1234
char *q = p+5678;
*q = 1234;
null
es necesario en la aritmética de punteros, en su lugar interpreto que decir que la aritmética de punteros es un mal lugar para albergar la semántica de su idioma, no es que no sea un error nulo.p = undefined
siempre quep
no se evalúe. De manera más útil, puede ponerundefined
algún tipo de referencia mutable, de nuevo siempre que no la evalúe. El desafío más serio es con cálculos lentos que pueden no terminar, lo que por supuesto es indecidible. La principal diferencia es que todos estos son errores de programación inequívocos y nunca se utilizan para expresar la lógica ordinaria.for
bucle para implementar la misma funcionalidad, pero no tendrá las mismas garantías de tipo estático, porque unfor
bucle no tiene el concepto de un tipo de retorno.fuente
En Haskell: un número entero, un número entero que podría ser nulo, un número entero cuyo valor provenía del mundo exterior y un número entero que podría ser una cadena, son todos tipos distintos, y el compilador hará cumplir esto . No puede compilar un programa Haskell que no respete estas distinciones.
(Sin embargo, puede omitir las declaraciones de tipo. En la mayoría de los casos, el compilador puede determinar el tipo más general para sus variables, lo que dará como resultado una compilación exitosa. ¿No es genial?)
fuente
Maybe
(por ejemplo, JavaOptional
y ScalaOption
), pero en esos idiomas es una solución poco elaborada, ya que siempre puede asignarnull
a una variable de ese tipo y hacer que su programa explote al ejecutar- hora. Esto no puede suceder con Haskell [1], porque no hay un valor nulo , por lo que simplemente no puedes hacer trampa. ([1]: en realidad, puede generar un error similar a una NullPointerException usando funciones parciales comofromJust
cuando tiene unNothing
, pero esas funciones probablemente están mal vistas).IO Integer
estaría más cerca del 'subprograma que, cuando se ejecuta, da un entero'? Como a) enmain = c >> c
el valor devuelto por el primeroc
puede ser diferente que por el segundo,c
mientrasa
que tendrá el mismo valor independientemente de su posición (siempre que estemos en un solo alcance) b) hay tipos que denotan valores del mundo exterior para hacer cumplir su satisfacción (es decir, no ponerlos directamente, pero primero verifique si la entrada del usuario es correcta / no maliciosa).Mucha gente ha enumerado cosas buenas sobre Haskell. Pero en respuesta a su pregunta específica "¿por qué el sistema de tipos hace que los programas sean más correctos?", Sospecho que la respuesta es "polimorfismo paramétrico".
Considere la siguiente función de Haskell:
Hay literalmente solamente una forma posible de implementar esta función. Solo por la firma de tipo, puedo decir con precisión qué hace esta función, porque solo hay una cosa posible que puede hacer. [OK, no del todo, ¡pero casi!]
Detente y piensa en eso por un momento. Eso es realmente un gran problema! Significa que si escribo una función con esta firma, en realidad es imposible que la función haga algo más de lo que pretendía. (La firma de tipo en sí puede estar equivocada, por supuesto. Ningún lenguaje de programación evitará todos los errores).
Considere esta función:
Esta función es imposible . Literalmente no puede implementar esta función. Puedo decir eso solo por la firma de tipo.
Como puede ver, ¡una firma tipo Haskell le dice mucho!
Compare con C #. (Lo siento, mi Java está un poco oxidado).
Hay un par de cosas que este método podría hacer:
in2
como el resultado.En realidad, Haskell también tiene estas tres opciones. Pero C # también le brinda las opciones adicionales:
in2
antes de devolverlo. (Haskell no tiene modificación en el lugar).La reflexión es un martillo particularmente grande; usando la reflexión, puedo construir un nuevo
TY
objeto de la nada, ¡y devolverlo! Puedo inspeccionar ambos objetos y hacer diferentes acciones dependiendo de lo que encuentre. Puedo hacer modificaciones arbitrarias a los dos objetos pasados.I / O es un martillo igualmente grande. El código podría estar mostrando mensajes al usuario, abriendo conexiones de bases de datos, o formateando su disco duro, o cualquier cosa, realmente.
La
foobar
función de Haskell , por el contrario, solo puede tomar algunos datos y devolverlos sin cambios. No puede "mirar" los datos, porque su tipo es desconocido en tiempo de compilación. No puede crear nuevos datos, porque ... bueno, ¿cómo se construyen los datos de cualquier tipo posible? Necesitarías reflexión para eso. No puede realizar ninguna E / S, porque la firma de tipo no declara que se está realizando la E / S. Por lo tanto, no puede interactuar con el sistema de archivos o la red, ¡ni siquiera ejecutar hilos en el mismo programa! (Es decir, es 100% garantizado a prueba de hilos).Como puede ver, al no permitirle hacer un montón de cosas, Haskell le permite hacer garantías muy sólidas sobre lo que realmente hace su código. Tan apretado, de hecho, que (para un código realmente polimórfico) generalmente solo hay una forma posible de que las piezas puedan encajar.
(Para ser claros: aún es posible escribir funciones Haskell donde la firma de tipo no le dice mucho.
Int -> Int
Podría ser casi cualquier cosa. Pero incluso entonces, sabemos que la misma entrada siempre producirá la misma salida con 100% de certeza. ¡Java ni siquiera garantiza eso!)fuente
fubar :: a -> b
, ¿no? (Sí, lo séunsafeCoerce
. Supongo que no estamos hablando de nada con "inseguro" en su nombre, ¡y tampoco deberían preocuparse los recién llegados! D)foobar :: x
es bastante poco implementable ...x -> y -> y
es perfectamente implementable. El tipo(x -> y) -> y
no es. El tipox -> y -> y
toma dos entradas y devuelve la segunda. El tipo(x -> y) -> y
toma una función que funcionax
, y de alguna manera tiene quey
salir de eso ...Una pregunta SO relacionada .
No, realmente no puedes, al menos no de la misma manera que Java. En Java, sucede este tipo de cosas:
y Java felizmente intentará convertir su no String como un String. Haskell no permite este tipo de cosas, eliminando toda una clase de errores de tiempo de ejecución.
null
es parte del sistema de tipos (asNothing
), por lo que debe solicitarse y manejarse explícitamente, eliminando toda una clase de errores de tiempo de ejecución.También hay muchos otros beneficios sutiles, especialmente en torno a la reutilización y las clases de tipos, que no tengo la experiencia para conocer lo suficiente como para comunicarme.
Sin embargo, la mayoría de las veces se debe a que el sistema de tipos de Haskell permite mucha expresividad. Puedes hacer muchas cosas con solo unas pocas reglas. Considere el siempre presente árbol Haskell:
Ha definido un árbol binario genérico completo (y dos constructores de datos) en una línea de código bastante legible. Todo solo usando algunas reglas (que tienen tipos de suma y tipos de productos ). Eso es 3-4 archivos de código y clases en Java.
Especialmente entre aquellos que son propensos a venerar los sistemas tipográficos, este tipo de concisión / elegancia es muy valorado.
fuente
interface
s que se pueden agregar después del hecho, y no "olvidan" el tipo que las está implementando. Es decir, puede asegurarse de que dos argumentos para una función tengan el mismo tipo, a diferencia deinterface
s donde dosList<String>
s pueden tener implementaciones diferentes. Técnicamente, podría hacer algo muy similar en Java agregando un parámetro de tipo a cada interfaz, pero el 99% de las interfaces existentes no lo hacen y confundirá a sus pares.Object
.any
. Haskell tampoco te impedirá hacerlo, ya que ... bueno, tiene condiciones. Haskell le puede dar herramientas, no puede forzosamente dejar que haga cosas estúpidas si insiste en Greenspunning suficiente de un intérprete para reinventarnull
en un contexto anidado. Ningún idioma puede.Esto es principalmente cierto con pequeños programas. Haskell le impide cometer errores que son fáciles en otros idiomas (por ejemplo, comparar una
Int32
y unaWord32
y algo explota), pero no evita que cometa todos los errores.Haskell realmente facilita mucho la refactorización . Si su programa era correcto anteriormente y se comprueba, existe la posibilidad de que siga siendo correcto después de modificaciones menores.
Los tipos en Haskell son bastante ligeros, ya que es fácil declarar nuevos tipos. Esto contrasta con un lenguaje como Rust, donde todo es un poco más engorroso.
Haskell tiene muchas características más allá de la suma simple y los tipos de productos; también tiene tipos universalmente cuantificados (p
id :: a -> a
. ej. ). También puede crear tipos de registros que contengan funciones, que es bastante diferente de un lenguaje como Java o Rust.GHC también puede derivar algunas instancias basadas solo en tipos, y desde el advenimiento de los genéricos, puede escribir funciones que son genéricas entre los tipos. Esto es bastante conveniente, y es más fluido que lo mismo en Java.
Otra diferencia es que Haskell tiende a tener errores de tipo relativamente buenos (al menos al momento de escribir). La inferencia de tipo de Haskell es sofisticada, y es bastante raro que necesite proporcionar anotaciones de tipo para obtener algo para compilar. Esto está en contraste con Rust, donde la inferencia de tipos a veces puede requerir anotaciones incluso cuando el compilador en principio puede deducir el tipo.
Finalmente, Haskell tiene clases tipográficas, entre ellas la famosa mónada. Las mónadas resultan ser una forma particularmente agradable de manejar los errores; básicamente le brindan casi toda la comodidad
null
sin la depuración horrible y sin renunciar a su seguridad de tipo. Por lo tanto, la capacidad de escribir funciones en estos tipos realmente importa bastante cuando se trata de alentarnos a usarlas.Tal vez sea cierto, pero le falta un punto crucial: el punto en el que comienza a dispararse en el pie en Haskell está más avanzado que el punto en el que comienza a dispararse en el pie en Java.
fuente