¿Qué hace que el sistema de tipos Haskell sea tan venerado (en comparación con Java)?

204

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 stringcontiene 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!

phatmanace
fuente
31
Dejaré que las personas tengan más conocimiento que escriban respuestas reales, pero lo esencial es esto: los lenguajes de tipo estático como C # tienen un sistema de tipos que intentan ayudarte a escribir código defendible ; escriba sistemas como el intento de Haskell de ayudarlo a escribir el código correcto (es decir, demostrable) El principio básico en el trabajo es mover cosas que se pueden verificar en la etapa de compilación; Haskell comprueba más cosas en tiempo de compilación.
Robert Harvey
8
No sé mucho sobre Haskell, pero puedo hablar sobre Java. Si bien parece fuertemente tipado, aún te permite hacer cosas "atroces" como dijiste. Para casi todas las garantías que Java hace con respecto a su sistema de tipos, hay una forma de evitarlo.
12
No sé por qué todas las respuestas mencionan Maybesolo 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.
Paul
1
Habrá excelentes respuestas aquí, pero en un intento de ayudar, estudie las firmas de tipo. Permiten a los humanos y a los programas razonar sobre los programas de una manera que ilustrará cómo Java está en los medios rectos tipos.
Michael Pascua
66
Para ser justos, debo señalar que "el todo si se compila, funcionará" es un eslogan, no una declaración literal de los hechos. Sí, los programadores de Haskell sabemos que pasar el verificador de tipos ofrece una buena posibilidad de corrección, para algunas nociones limitadas de corrección, ¡pero ciertamente no es una declaración literal y universalmente "verdadera"!
Tom Ellis

Respuestas:

230

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)

  • Seguridad . Los tipos de Haskell tienen propiedades de "seguridad de tipo" bastante buenas. Esto es bastante específico, pero esencialmente significa que los valores en algún tipo no pueden transformarse sin sentido en otro tipo. Esto a veces está en desacuerdo con la mutabilidad (ver restricción de valor de OCaml )
  • Tipos de datos algebraicos . Los tipos en Haskell tienen esencialmente la misma estructura que las matemáticas de secundaria. Esto es escandalosamente simple y consistente, sin embargo, resulta que es tan poderoso como podría desear. Es simplemente una gran base para un sistema de tipos.
    • Programación genérica de tipo de datos . Esto no es lo mismo que los tipos genéricos (ver generalización ). En cambio, debido a la simplicidad de la estructura de tipo como se señaló anteriormente, es relativamente fácil escribir código que opera genéricamente sobre esa estructura. Más adelante hablo sobre cómo Equn 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.
  • Tipos recursivos mutuos . Esto es solo un componente esencial de la escritura de tipos no triviales.
    • Tipos anidados . Esto le permite definir tipos recursivos sobre variables que se repiten en diferentes tipos. Por ejemplo, un tipo de árboles balanceados es data Bt a = Here a | There (Bt (a, a)). Piense detenidamente en los valores válidos Bt ay observe cómo funciona ese tipo. ¡Es complicado!
  • La generalización . Esto es casi demasiado tonto para no tenerlo en un sistema de tipos (ejem, mirándote, Go) Es importante tener nociones de variables de tipo y la capacidad de hablar sobre el código que es independiente de la elección de esa variable. Hindley Milner es un sistema de tipos que se deriva del Sistema F. El sistema de tipos de Haskell es una elaboración del tipo HM y el Sistema F es esencialmente el corazón de la generalización. Lo que quiero decir es que Haskell tiene una muy buena historia de generalización.
  • Tipos abstractos . La historia de Haskell aquí no es genial, pero tampoco inexistente. Es posible escribir tipos que tengan una interfaz pública pero una implementación privada. Esto nos permite admitir cambios en el código de implementación en un momento posterior y, lo que es más importante ya que es la base de todas las operaciones en Haskell, escribir tipos "mágicos" que tengan interfaces bien definidas como 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.
  • Parametricity . Haskell valores no tienen ningún operaciones universales. Java viola esto con cosas como igualdad de referencia y hashing y aún más flagrantemente con coacciones. Lo que esto significa es que obtienes teoremas gratuitos sobre tipos que te permiten conocer el significado de una operación o valor en un grado notable completamente de su tipo --- ciertos tipos son tales que solo puede haber un número muy pequeño de habitantes.
  • Los tipos de tipo superior muestran todo el tipo al codificar cosas más complicadas. Functor / Aplicativo / Mónada, Plegable / Traversable, todo el mtlsistema 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.
  • Clases de tipo . Si piensa en los sistemas de tipos como lógicas, lo cual es útil, a menudo se le exige que pruebe las cosas. En muchos casos, esto es esencialmente ruido de línea: puede haber solo una respuesta correcta y es una pérdida de tiempo y esfuerzo para el programador decir esto. Las clases de tipos son una forma para que Haskell genere las pruebas para usted. En términos más concretos, esto le permite resolver simples "sistemas de ecuaciones de tipo" como "¿En qué tipo pretendemos hacer las (+)cosas juntos? ¡Oh Integer, está bien! ¡Vamos a insertar el código correcto ahora!". En sistemas más complejos, puede estar estableciendo restricciones más interesantes.
    • Cálculo de restricciones . Las restricciones en Haskell, que son el mecanismo para llegar al sistema del prólogo de la clase de tipos, se tipifican estructuralmente. Esto proporciona una forma muy simple de relación de subtipo que le permite ensamblar restricciones complejas a partir de otras más simples. Toda la mtlbiblioteca se basa en esta idea.
    • Derivado . Para impulsar la canonicidad del sistema typeclass es necesario escribir una gran cantidad de código trivial para describir las restricciones que los tipos definidos por el usuario deben crear. Con la estructura muy normal de los tipos de Haskell, a menudo es posible pedirle al compilador que haga esto por usted.
    • Escriba el prólogo de clase . El solucionador de clases de tipo Haskell, el sistema que genera esas "pruebas" a las que me referí anteriormente, es esencialmente una forma inválida de Prolog con propiedades semánticas más agradables. Esto significa que puede codificar cosas realmente peludas en prólogo de tipo y esperar que se manejen todas en tiempo de compilación. Un buen ejemplo podría ser resolver una prueba de que dos listas heterogéneas son equivalentes si olvida el orden: son "conjuntos" heterogéneos equivalentes.
    • Clases de tipo multiparamétrico y dependencias funcionales . Estos son solo refinamientos masivamente útiles para el prólogo de la clase de tipos base. Si conoce Prolog, puede imaginar cuánto aumenta el poder expresivo cuando puede escribir predicados de más de una variable.
  • Bastante buena inferencia . Los idiomas basados ​​en los sistemas de tipo Hindley Milner tienen muy buena inferencia. HM tiene una inferencia completa, lo que significa que nunca necesitará escribir una variable de tipo. Haskell 98, la forma más simple de Haskell, ya lo descarta en algunas circunstancias muy raras. En general, el Haskell moderno ha sido un experimento para reducir lentamente el espacio de inferencia completa al tiempo que agrega más potencia a HM y observa cuándo los usuarios se quejan. La gente rara vez se queja: la inferencia de Haskell es bastante buena.
  • Subtipo muy, muy, muy débil solamente . Mencioné anteriormente que el sistema de restricción del prólogo de tipo de clase tiene una noción de subtipo estructural. Esa es la única forma de subtipo en Haskell . El subtipo es terrible para el razonamiento y la inferencia. Hace que cada uno de esos problemas sea significativamente más difícil (un sistema de desigualdades en lugar de un sistema de igualdades). También es realmente fácil de entender mal (¿Subclasificar es lo mismo que subtipo? ¡Por supuesto que no! ¡Pero la gente confunde con frecuencia eso y muchos idiomas ayudan en esa confusión! ¿Cómo terminamos aquí? Supongo que nadie examina el LSP).
    • Note recientemente (a principios de 2017) Steven Dolan publicó su tesis sobre MLsub , una variante de la inferencia de tipo ML e Hindley-Milner que tiene una historia de subtipo muy agradable ( ver también ). Esto no obvia lo que he escrito anteriormente: la mayoría de los sistemas de subtipificación están rotos y tienen una mala inferencia, pero sí sugiere que solo hoy podemos haber descubierto algunas formas prometedoras de tener una inferencia completa y un juego de subtipificación muy bien juntos. Ahora, para ser totalmente claros, las nociones de subtipo de Java no son capaces de aprovechar los algoritmos y sistemas de Dolan. Requiere un replanteamiento de lo que significa subtipo.
  • Tipos de rango superior . Anteriormente hablé sobre la generalización, pero más que una simple generalización, es útil poder hablar sobre los tipos que tienen variables generalizadas dentro de ellos . Por ejemplo, un mapeo entre estructuras de orden superior que es ajeno (ver parametricidad ) a lo que esas estructuras "contienen" tiene un tipo similar (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 la avariable está vinculada solo dentro del argumento. Esto significa que el definidor de la función mapFreedecide qué ase instancia cuando se usa, no el usuario de mapFree.
  • Tipos existenciales . Mientras que los tipos de rango superior nos permiten hablar sobre la cuantificación universal, los tipos existenciales nos permiten hablar sobre la cuantificación existencial: la idea de que simplemente existe algún tipo desconocido que satisface algunas ecuaciones. Esto termina siendo útil y continuar por más tiempo tomaría mucho tiempo.
  • Escriba familias . A veces, los mecanismos de la clase de tipos son inconvenientes ya que no siempre pensamos en Prolog. Las familias de tipos nos permiten escribir relaciones funcionales directas entre los tipos.
    • Familias de tipo cerrado . Las familias de tipos están abiertas de forma predeterminada, lo que es molesto porque significa que, si bien puede ampliarlas en cualquier momento, no puede "invertirlas" con ninguna esperanza de éxito. Esto se debe a que no puede probar la inyectividad , pero con familias de tipos cerrados sí puede.
  • 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 Statees un tipo algebraico muy simple que también promovió sus valores en el nivel de tipo. Entonces, posteriormente, podemos hablar de constructores de tipos como Handlecomo tomar argumentos específicos a tipos como State. Es confuso entender todos los detalles, pero también muy correcto.

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
    
  • 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 . El Typeablesistema de Haskell permite la creación de una caja fuerte coerce :: (Typeable a, Typeable b) => a -> Maybe b. Este sistema se basa en Typeableimplementarse 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.

  • Pureza . Haskell no permite efectos secundarios para una muy, muy amplia definición de "efecto secundario". Esto lo obliga a poner más información en los tipos, ya que los tipos gobiernan las entradas y salidas y sin efectos secundarios, todo debe tenerse en cuenta en las entradas y salidas.
    • IO . Posteriormente, Haskell necesitaba una forma de hablar sobre los efectos secundarios, ya que cualquier programa real debe incluir algunos, por lo que una combinación de clases de tipos, tipos de tipo superior y tipos abstractos dio lugar a la noción de utilizar un tipo particular super especial llamado IO aa representar cálculos de efectos secundarios que dan como resultado valores de tipo a. Esta es la base de un sistema de efectos muy agradable incrustado dentro de un lenguaje puro.
  • La falta denull . Todo el mundo sabe que nulles 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 tipo Aen el tipo Maybe A, mitigan completamente el problema de null.
  • Recurrencia polimórfica . Esto le permite definir funciones recursivas que generalizan las variables de tipo a pesar de usarlas en diferentes tipos en cada llamada recursiva en su propia generalización. Es difícil hablar de esto, pero es especialmente útil para hablar sobre tipos anidados. Mirar hacia atrás en el Bt atipo de delante y tratar de escribir una función para calcular su tamaño: size :: Bt a -> Int. Se verá un poco como size (Here a) = 1y size (There bt) = 2 * size bt. Operacionalmente, eso no es demasiado complejo, pero tenga en cuenta que la llamada recursiva sizeen la última ecuación ocurre en un tipo diferente , sin embargo, la definición general tiene un tipo generalizado agradable size :: 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.

J. Abrahamson
fuente
77
Nulo no fue un "error" de mil millones de dólares. Hay casos en los que no es posible verificar estáticamente que el puntero no será desreferenciado antes de que pueda existir algo significativo ; Tener un intento de trampa de desreferencia en tal caso es a menudo mejor que requerir que el puntero identifique inicialmente un objeto sin sentido. Creo que el error más grande relacionado con la anulación fue tener implementaciones que, dado , se atraparán , pero no se atraparán nichar *p = NULL;*p=1234char *q = p+5678;*q = 1234;
Supercat
37
Eso es una cita directa de Tony Hoare: en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retraction . Si bien estoy seguro de que hay momentos en los que nulles 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.
J. Abrahamson
18
@supercat, de hecho puedes escribir un idioma sin nulo. Ya sea permitirlo o no es una opción.
Paul Draper
66
@supercat: ese problema también existe en Haskell, pero en una forma diferente. Haskell es normalmente vago e inmutable y, por lo tanto, le permite escribir p = undefinedsiempre que pno se evalúe. De manera más útil, puede poner undefinedalgú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.
Christian Conkle
66
@supercat Haskell carece por completo de semántica de referencia (esta es la noción de transparencia referencial que implica que todo se preserva al reemplazar las referencias con sus referentes). Por lo tanto, creo que su pregunta está mal planteada.
J. Abrahamson
78
  • Inferencia de tipo completo. En realidad, puede usar tipos complejos de manera ubicua sin sentir, "Santo cielo, todo lo que hago es escribir firmas de tipo".
  • Los tipos son completamente algebraicos , lo que hace que sea muy fácil expresar algunas ideas complejas.
  • Haskell tiene clases de tipo, que son una especie de interfaces similares, excepto que no tiene que poner todas las implementaciones para un tipo en el mismo lugar. Puede crear implementaciones de sus propias clases de tipos para los tipos de terceros existentes, sin necesidad de acceder a su fuente.
  • Las funciones recursivas y de orden superior tienden a poner más funcionalidad en el ámbito del verificador de tipos. Tomar filtro , por ejemplo. En un lenguaje imperativo, puede escribir un forbucle para implementar la misma funcionalidad, pero no tendrá las mismas garantías de tipo estático, porque un forbucle no tiene el concepto de un tipo de retorno.
  • La falta de subtipos simplifica enormemente el polimorfismo paramétrico.
  • Los tipos de tipo superior (tipos de tipos) son relativamente fáciles de especificar y usar en Haskell, lo que le permite crear abstracciones en torno a tipos que son completamente insondables en Java.
Karl Bielefeldt
fuente
77
Buena respuesta: ¿podría darme un ejemplo simple de un tipo de clase superior? Piense que eso me ayudaría a entender por qué es imposible hacerlo en Java.
phatmanace
3
Hay algunos buenos ejemplos aquí .
Karl Bielefeldt
3
La coincidencia de patrones también es muy importante, significa que puede usar el tipo de un objeto para tomar decisiones con mucha facilidad.
Benjamin Gruenbaum
2
@BenjaminGruenbaum No creo que llame a eso una característica del sistema de tipos.
Doval
3
Si bien los ADT y HKT son definitivamente parte de la respuesta, dudo que alguien que haga esta pregunta sepa por qué son útiles, sugiero que ambas secciones deben ampliarse para explicar esto
jk.
62
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

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?)

WolfeFan
fuente
11
+1 mientras esta respuesta no está completa, creo que es mucho mejor en el nivel de la pregunta
jk.
1
+1 Aunque ayudaría a explicar que otros lenguajes también tienen Maybe(por ejemplo, Java Optionaly Scala Option), pero en esos idiomas es una solución poco elaborada, ya que siempre puede asignar nulla 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 como fromJustcuando tiene un Nothing, pero esas funciones probablemente están mal vistas).
Andres F.
2
"un entero cuyo valor proviene del mundo exterior" - ¿no IO Integerestaría más cerca del 'subprograma que, cuando se ejecuta, da un entero'? Como a) en main = c >> cel valor devuelto por el primero cpuede ser diferente que por el segundo, cmientras aque 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).
Maciej Piechotka
44
Maciej, eso sería más exacto. Estaba luchando por la simplicidad.
WolfeFan
30

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:

foobar :: x -> y -> y

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:

fubar :: Int -> (x -> y) -> y

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).

public static TY foobar<TX, TY>(TX in1, TY in2)

Hay un par de cosas que este método podría hacer:

  • Regresar in2como el resultado.
  • Bucle para siempre, y nunca devolver nada.
  • Lanza una excepción y nunca devuelvas nada.

En realidad, Haskell también tiene estas tres opciones. Pero C # también le brinda las opciones adicionales:

  • Regreso nulo. (Haskell no tiene nulo).
  • Modifique in2antes de devolverlo. (Haskell no tiene modificación en el lugar).
  • Usa la reflexión. (Haskell no tiene reflejo).
  • Realice múltiples acciones de E / S antes de devolver un resultado. (Haskell no le permitirá realizar E / S a menos que declare que realiza E / S aquí).

La reflexión es un martillo particularmente grande; usando la reflexión, puedo construir un nuevo TYobjeto 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 foobarfunció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 -> IntPodrí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!)

Orquídea matemática
fuente
44
+1 Gran respuesta! Esto es muy poderoso y, a menudo, poco apreciado por los recién llegados a Haskell. Por cierto, una función "imposible" más simple sería 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)
Andres F.
Hay muchas firmas tipo más simples que no puedes escribir, sí. Por ejemplo, foobar :: xes bastante poco implementable ...
MathematicalOrchid
En realidad, no puede hacer que el código puro no sea ​​seguro para subprocesos, pero aún puede hacerlo multiproceso. Sus opciones son "antes de evaluar esto, evaluar esto", "cuando evalúa esto, es posible que también desee evaluar esto en un hilo separado", y "cuando evalúa esto, es posible que desee evaluar esto también, posiblemente en un hilo separado ". El valor predeterminado es "haz lo que quieras", lo que esencialmente significa "evaluar lo más tarde posible".
John Dvorak
Más prosaicamente, puede llamar a los métodos de instancia en in1 o in2 que tienen efectos secundarios. O puede modificar el estado global (que, por supuesto, se modela como una acción de E / S en Haskell, pero podría no ser lo que la mayoría de la gente piensa como E / S).
Doug McClean
2
@isomorphismes El tipo x -> y -> yes perfectamente implementable. El tipo (x -> y) -> yno es. El tipo x -> y -> ytoma dos entradas y devuelve la segunda. El tipo (x -> y) -> ytoma una función que funciona x, y de alguna manera tiene que ysalir de eso ...
MathematicalOrchid
17

Una pregunta SO relacionada .

Supongo que puede hacer lo mismo en haskell (es decir, insertar cosas en cadenas / ints que realmente deberían ser tipos de datos de primera clase)

No, realmente no puedes, al menos no de la misma manera que Java. En Java, sucede este tipo de cosas:

String x = (String)someNonString;

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.

nulles parte del sistema de tipos (as Nothing), 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:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

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.

Telastyn
fuente
Entendí solo NullPointerExceptions de su respuesta. ¿Podría incluir más ejemplos?
Jesvin Jose
2
No necesariamente cierto, JLS §5.5.1 : Si T es un tipo de clase, entonces | S | <: | T | o | T | <: | S |. De lo contrario, se produce un error en tiempo de compilación. Por lo tanto, el compilador no le permitirá lanzar tipos inconvertibles, obviamente hay formas de evitarlo.
Boris the Spider
En mi opinión, la forma más sencilla de poner las ventajas de las clases de tipos es que son como interfaces 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 de interfaces donde dos List<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.
Doval
2
@BoristheSpider Es cierto, pero las excepciones de conversión casi siempre implican el downcast de una superclase a una subclase o de una interfaz a una clase, y no es inusual que la superclase sea Object.
Doval
2
Creo que el punto en la pregunta sobre las cadenas no tiene que ver con los errores de tipo de conversión y tiempo de ejecución, sino el hecho de que si no desea usar tipos, Java no lo hará, como en realidad, almacenar sus datos en serie forma, abusando de las cadenas como un tipo ad-hoc 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 reinventar nullen un contexto anidado. Ningún idioma puede.
Leushenko
0

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.

Esto es principalmente cierto con pequeños programas. Haskell le impide cometer errores que son fáciles en otros idiomas (por ejemplo, comparar una Int32y una Word32y 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.

Estoy tratando de entender por qué exactamente Haskell es mejor que otros lenguajes estáticamente escritos a este respecto.

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.

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.

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 nullsin 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.

Dicho de otra manera, puedes escribir Java bueno o malo, supongo que puedes hacer lo mismo en Haskell

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