¿Hay alguna manera de hacer que las firmas de funciones sean más informativas en Haskell?
syntax coding-style (6)
Hay otras opciones, dependiendo de lo tonto y / o pedante que desee obtener con sus tipos.
Por ejemplo, podrías hacer esto ...
type Meaning a b = a
bmiTell :: (RealFloat a) => a `Meaning` weight -> a `Meaning` height -> String
bmiTell weight height = -- etc.
... pero eso es increíblemente tonto, potencialmente confuso y no ayuda en la mayoría de los casos. Lo mismo ocurre con esto, que además requiere el uso de extensiones de idioma:
bmiTell :: (RealFloat weight, RealFloat height, weight ~ height)
=> weight -> height -> String
bmiTell weight height = -- etc.
Un poco más sensato sería este:
type Weight a = a
type Height a = a
bmiTell :: (RealFloat a) => Weight a -> Height a -> String
bmiTell weight height = -- etc.
... pero todavía es un poco tonto y tiende a perderse cuando GHC expande sinónimos tipo.
El verdadero problema aquí es que está adjuntando contenido semántico adicional a diferentes valores del mismo tipo polimórfico, lo que va contra el grano del lenguaje en sí mismo y, como tal, generalmente no es idiomático.
Una opción, por supuesto, es simplemente tratar con variables de tipo no informativas. Pero eso no es muy satisfactorio si hay una distinción significativa entre dos cosas del mismo tipo que no son obvias en el orden en que se dan.
Lo que recomiendo que intente, en su lugar, es utilizar wrappers newtype
para especificar semántica:
newtype Weight a = Weight { getWeight :: a }
newtype Height a = Height { getHeight :: a }
bmiTell :: (RealFloat a) => Weight a -> Height a -> String
bmiTell (Weight weight) (Height height)
Hacer esto no es tan común como merece ser, creo. Es un poco de tipeo extra (ha, ha) pero no solo hace que las firmas de tipo sean más informativas incluso con los sinónimos de tipo expandidos, sino que permite al verificador de tipos detectar si usa un peso por error como una altura, o tal. Con la extensión GeneralizedNewtypeDeriving
puede incluso obtener instancias automáticas incluso para clases de tipos que normalmente no se pueden derivar.
Me doy cuenta de que esto podría ser considerado una pregunta subjetiva o quizás fuera del tema, así que espero que, en lugar de cerrarla, se migre, tal vez a los programadores.
Estoy empezando a aprender Haskell, principalmente para mi propia edificación, y me gustan muchas de las ideas y principios que respaldan el lenguaje. Me fascinó con los lenguajes funcionales después de tomar una clase de teoría del lenguaje donde jugamos con Lisp, y había escuchado muchas cosas buenas sobre lo productivo que podía ser Haskell, así que pensé que lo investigaría yo mismo. Hasta el momento, me gusta el lenguaje, excepto por una cosa de la que no me puedo escapar: esas firmas de funciones de madre e hija.
Mi experiencia profesional es principalmente hacer OO, especialmente en Java. La mayoría de los lugares para los que he trabajado han forjado muchos de los dogmas modernos estándar; Ágil, código limpio, TDD, etc. Después de unos años de trabajar de esta manera, definitivamente se ha convertido en mi zona de confort; especialmente la idea de que el "buen" código debe auto documentarse. Me he acostumbrado a trabajar en un IDE, donde los nombres largos y verbosos de métodos con firmas muy descriptivas no son un problema con la autocompletación inteligente y una gran variedad de herramientas analíticas para navegar por paquetes y símbolos; si puedo presionar Ctrl + Espacio en Eclipse, luego deduzco qué hace un método al ver su nombre y las variables de ámbito local asociadas con sus argumentos en lugar de jalar hacia arriba los JavaDocs, soy tan feliz como un cerdo en caca.
Esto es, decididamente, no parte de las mejores prácticas de la comunidad en Haskell. He leído muchas opiniones diferentes sobre el asunto, y entiendo que la comunidad de Haskell considera que su brevedad es ser un "profesional". He leído Cómo leer Haskell , y entiendo la razón de ser de muchas de las decisiones, pero eso no significa que me gusten; nombres de variables de una letra, etc. no son divertidos para mí. Reconozco que tendré que acostumbrarme a eso si quiero seguir pirateando con el lenguaje.
Pero no puedo superar las firmas de funciones. Tome este ejemplo, como se extrajo de Aprenda una sección de Haskell [...] sobre la sintaxis de la función:
bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You''re underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You''re supposedly normal. Pffft, I bet you''re ugly!"
| weight / height ^ 2 <= 30.0 = "You''re fat! Lose some weight, fatty!"
| otherwise = "You''re a whale, congratulations!"
Me doy cuenta de que este es un ejemplo tonto que solo se creó con el propósito de explicar los resguardos y las restricciones de clase, pero si examinaras solo la firma de esa función, no tendrías idea de cuál de sus argumentos pretendía ser el peso o la altura Incluso si usara Float
o Double
lugar de cualquier tipo, aún no sería discernible de inmediato.
Al principio, pensé que sería lindo e inteligente y brillante y trataría de burlarlo usando nombres de variables de tipo más largo con múltiples restricciones de clase:
bmiTell :: (RealFloat weight, RealFloat height) => weight -> height -> String
Esto escupió un error (como comentario aparte, si alguien puede explicarme el error, le agradecería):
Could not deduce (height ~ weight)
from the context (RealFloat weight, RealFloat height)
bound by the type signature for
bmiTell :: (RealFloat weight, RealFloat height) =>
weight -> height -> String
at example.hs:(25,1)-(27,27)
`height'' is a rigid type variable bound by
the type signature for
bmiTell :: (RealFloat weight, RealFloat height) =>
weight -> height -> String
at example.hs:25:1
`weight'' is a rigid type variable bound by
the type signature for
bmiTell :: (RealFloat weight, RealFloat height) =>
weight -> height -> String
at example.hs:25:1
In the first argument of `(^)'', namely `height''
In the second argument of `(/)'', namely `height ^ 2''
In the first argument of `(<=)'', namely `weight / height ^ 2''
No entendiendo completamente por qué eso no funcionó, comencé a buscar en Google, e incluso encontré esta pequeña publicación que sugiere parámetros nombrados, específicamente, suplantando parámetros nombrados a través de newtype
, pero eso parece ser un poco demasiado.
¿No hay una forma aceptable de crear firmas de funciones informativas? ¿Es "The Haskell Way" simplemente para Haddock la mierda de todo?
Los eglefinos y / o también mirando la ecuación de la función (los nombres a los que se les vinculan las cosas) son las formas en que digo lo que está sucediendo. Puede hacer los parámetros individuales de Haddock, como tal,
bmiTell :: (RealFloat a) => a -- ^ your weight
-> a -- ^ your height
-> String -- ^ what I''d think about that
así que no es solo una burbuja de texto que explica todas las cosas.
La razón por la cual tus lindas variables de tipo no funcionaron es que tu función es:
(RealFloat a) => a -> a -> String
Pero su intento de cambio:
(RealFloat weight, RealFloat height) => weight -> height -> String
es equivalente a esto:
(RealFloat a, RealFloat b) => a -> b -> String
Por lo tanto, en esta firma tipo ha dicho que los primeros dos argumentos tienen diferentes tipos, pero GHC ha determinado que (en función de su uso) deben tener el mismo tipo. Por lo tanto, se queja de que no puede determinar que el weight
y la height
son del mismo tipo, aunque deben ser (es decir, la firma de tipo propuesta no es lo suficientemente estricta y permitiría usos no válidos de la función).
Posiblemente no sea relevante para una función con dos argumentos simples, sin embargo ... Si tiene una función que toma muchos y muchos argumentos, de tipos similares o simplemente de orden poco claro, puede valer la pena definir una estructura de datos que los represente. Por ejemplo,
data Body a = Body {weight, height :: a}
bmiTell :: (RealFloat a) => Body a -> String
Ahora puede escribir
bmiTell (Body {weight = 5, height = 2})
o
bmiTell (Body {height = 2, weight = 5})
y valdrá la pena en ambos sentidos, y también será obvio para cualquiera que intente leer su código.
Sin embargo, probablemente valga la pena para funciones con una mayor cantidad de argumentos. Para solo dos, me gustaría ir con todos los demás y simplemente newtype
para que la firma de tipo documente el orden correcto de los parámetros y obtendrá un error en tiempo de compilación si los mezcla.
Prueba esto:
type Height a = a
type Weight a = a
bmiTell :: (RealFloat a) => Weight a -> Height a -> String
Una firma de tipo no es una firma de estilo Java. Una firma de estilo Java le dirá qué parámetro es el peso y cuál es la altura solo porque mezcla los nombres de los parámetros con los tipos de parámetros. Haskell no puede hacer esto como una regla general, porque las funciones se definen usando la coincidencia de patrones y múltiples ecuaciones, como en:
map :: (a -> b) -> [a] -> [b]
map f (x:xs) = f x : map f xs
map _ [] = []
Aquí el primer parámetro se llama f
en la primera ecuación y _
(que significa "sin nombre") en el segundo. El segundo parámetro no tiene un nombre en ninguna de las ecuaciones; en las primeras partes tiene nombres (y el programador probablemente lo considerará como "la lista xs"), mientras que en el segundo es una expresión completamente literal.
Y luego hay definiciones sin puntos como:
concat :: [[a]] -> [a]
concat = foldr (++) []
La firma tipo nos dice que toma un parámetro que es del tipo [[a]]
, pero no aparece ningún nombre para este parámetro en ninguna parte del sistema.
Fuera de una ecuación individual para una función, los nombres que utiliza para referirse a sus argumentos son irrelevantes de todos modos, excepto como documentación. Dado que la idea de un "nombre canónico" para el parámetro de una función no está bien definida en Haskell, el lugar para la información "el primer parámetro de bmiTell
representa peso mientras que el segundo representa altura" está en documentación, no en la firma de tipo.
Estoy absolutamente de acuerdo en que lo que hace una función debe ser claro desde la información "pública" disponible al respecto. En Java, ese es el nombre de la función y los tipos y nombres de los parámetros. Si (como es habitual) el usuario necesitará más información que esa, la agrega a la documentación. En Haskell, la información pública sobre una función es el nombre de la función y los tipos de parámetros. Si el usuario necesitará más información que esa, la agrega a la documentación. Tenga en cuenta que los IDEs para Haskell como Leksah le mostrarán fácilmente los comentarios de Haddock.
Tenga en cuenta que lo que prefiere hacer en un lenguaje con un sistema de tipo fuerte y expresivo como el de Haskell es intentar hacer tantos errores como sea posible detectables como errores de tipo. Por lo tanto, una función como bmiTell
inmediatamente me bmiTell
señales de advertencia por las siguientes razones:
- Toma dos parámetros del mismo tipo que representan cosas diferentes
- Hará lo incorrecto si pasa los parámetros en el orden incorrecto
- Los dos tipos no tienen una posición natural (como lo hacen los dos
[a]
argumentos a++
)
Una cosa que a menudo se hace para aumentar la seguridad del tipo es, de hecho, crear nuevos tipos, como en el enlace que encontraste. Realmente no creo que esto tenga mucho que ver con el paso de parámetros con nombre, más que con hacer un tipo de datos que represente explícitamente la altura , en lugar de cualquier otra cantidad que desee medir con un número. Así que no tendría los nuevos valores de tipo que aparecen solo en la llamada; Usaría el valor de tipo nuevo siempre que obtuviera los datos de alto, y lo pasaría como datos de altura en lugar de como un número, de modo que obtengo el beneficio de seguridad de tipo (y documentación) en todas partes. Solo desenvolvería el valor en un número sin procesar cuando necesite pasarlo a algo que opera en números y no en altura (como las operaciones aritméticas dentro de bmiTell
).
Tenga en cuenta que esto no tiene gastos generales de tiempo de ejecución; Los nuevos tipos se representan de forma idéntica a los datos "dentro" del nuevo envoltorio de tipo, por lo que las operaciones de envoltura / desenvoltura no son operativas en la representación subyacente y simplemente se eliminan durante la compilación. Agrega solo caracteres adicionales en el código fuente, pero esos caracteres son exactamente la documentación que está buscando, con el beneficio adicional de ser aplicados por el compilador; Las firmas de estilo Java te dicen qué parámetro es el peso y cuál es la altura, ¡pero el compilador aún no podrá decir si pasastelas por el camino equivocado!
weight
tiene que ser del mismo tipo que la height
porque los está dividiendo (no hay moldes implícitos). weight ~ height
significa que son del mismo tipo. ghc ha explicado un poco cómo llegó a la conclusión de que era necesaria la weight ~ height
, lo siento. Se le permite decir lo que / que quería usando la sintaxis de la extensión de familias tipo:
{-# LANGUAGE TypeFamilies #-}
bmiTell :: (RealFloat weight, RealFloat height,weight~height) => weight -> height -> String
bmiTell weight height
| weight / height ^ 2 <= 18.5 = "You''re underweight, you emo, you!"
| weight / height ^ 2 <= 25.0 = "You''re supposedly normal. Pffft, I bet you''re ugly!"
| weight / height ^ 2 <= 30.0 = "You''re fat! Lose some weight, fatty!"
| otherwise = "You''re a whale, congratulations!"
Sin embargo, esto tampoco es ideal. Debes tener en cuenta que Haskell usa un paradigma muy diferente y debes tener cuidado de no dar por sentado que lo importante en otro idioma es importante aquí. Estás aprendiendo más cuando estás fuera de tu zona de confort. Es como si alguien de Londres apareciera en Toronto y se quejara de que la ciudad es confusa porque todas las calles son iguales, mientras que alguien de Toronto podría decir que Londres es confuso porque no hay regularidad en las calles. Lo que llamas ofuscación se llama claridad por Haskellers.
Si desea volver a una claridad de propósito más orientada a objetos, haga que bmiTell funcione solo para las personas, por lo que
data Person = Person {name :: String, weight :: Float, height :: Float}
bmiOffence :: Person -> String
bmiOffence p
| weight p / height p ^ 2 <= 18.5 = "You''re underweight, you emo, you!"
| weight p / height p ^ 2 <= 25.0 = "You''re supposedly normal. Pffft, I bet you''re ugly!"
| weight p / height p ^ 2 <= 30.0 = "You''re fat! Lose some weight, fatty!"
| otherwise = "You''re a whale, congratulations!"
Esto, creo, es el tipo de forma en que lo dejarías claro en OOP. Realmente no creo que estés usando el tipo de argumentos de tu método OOP para obtener esta información, debes usar los nombres de los parámetros para mayor claridad en lugar de los tipos, y no es justo esperar que Haskell te diga los nombres de los parámetros cuando descartó leer los nombres de los parámetros en su pregunta. [ver * a continuación] El sistema de tipos en Haskell es notablemente flexible y muy poderoso, por favor no se dé por vencido solo porque inicialmente es alienante para usted.
Si realmente desea que los tipos le digan, podemos hacer eso por usted:
type Weight = Float -- a type synonym - Float and Weight are exactly the same type, but human-readably different
type Height = Float
bmiClear :: Weight -> Height -> String
....
Ese es el enfoque utilizado con cadenas que representan nombres de archivos, por lo que definimos
type FilePath = String
writeFile :: FilePath -> String -> IO () -- take the path, the contents, and make an IO operation
que da la claridad que estabas buscando. Sin embargo, se siente que
type FilePath = String
carece de seguridad tipo, y eso
newtype FilePath = FilePath String
o algo incluso más inteligente sería una idea mucho mejor. Vea la respuesta de Ben para un punto muy importante sobre la seguridad del tipo.
[*] OK, puedes hacer: t en ghci y obtener la firma de tipo sin el nombre del parámetro, pero ghci es para el desarrollo interactivo del código fuente. Su biblioteca o módulo no debe permanecer indocumentado y hacky, debe utilizar el sistema de documentación de eglefino sintodo increíblemente ligero e instalar el eglefino a nivel local. Una versión más legítima de su queja sería que no hay un comando: v que imprima el código fuente de su función bmiTell. Las métricas sugieren que su código Haskell para el mismo problema será más corto por un factor (encuentro aproximadamente 10 en mi caso en comparación con OO equivalente o código no-oo imperativo), por lo que mostrar la definición dentro de gchi suele ser razonable. Deberíamos enviar una solicitud de función.