haskell - ¿Es una mala forma hacer nuevos tipos/datos para mayor claridad?
type-safety type-alias (5)
Me gustaría saber si es una mala forma de hacer algo como esto:
data Alignment = LeftAl | CenterAl | RightAl
type Delimiter = Char
type Width = Int
setW :: Width -> Alignment -> Delimiter -> String -> String
En lugar de algo como esto:
setW :: Int -> Char -> Char -> String -> String
Sé que rehacer esos tipos no hace nada más que ocupar algunas líneas a cambio de un código más claro.
Sin embargo, si uso el
Delimiter
tipo para múltiples funciones, esto sería mucho más claro para alguien que esté importando este módulo o que lea el código más adelante.
Soy relativamente nuevo en Haskell, así que no sé qué es una buena práctica para este tipo de cosas. Si esto no es una buena idea, o si hay algo que mejoraría la claridad, ¿cuál sería?
Definitivamente es bueno, y aquí hay otro ejemplo, supongamos que tiene este tipo de datos con alguna op:
data Form = Square Int | Rectangle Int Int | EqTriangle Int
perimeter :: Form -> Int
perimeter (Square s) = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s) = s * 3
area :: Form -> Int
area (Square s) = s ^ 2
area (Rectangle b h) = (b * h)
area (EqTriangle s) = (s ^ 2) `div` 2
Ahora imagina que agregas el círculo:
data Form = Square Int | Rectangle Int Int | EqTriangle Int | Cicle Int
sumar sus operaciones:
perimeter (Cicle r ) = pi * 2 * r
area (Cicle r) = pi * r ^ 2
no es muy bueno verdad? Ahora quiero usar Float ... Tengo que cambiar cada Int por Float
data Form = Square Double | Rectangle Double Double | EqTriangle Double | Cicle Double
area :: Form -> Double
perimeter :: Form -> Double
pero, ¿qué pasa si, para mayor claridad e incluso para reutilización, utilizo el tipo?
data Form = Square Side | Rectangle Side Side | EqTriangle Side | Cicle Radius
type Distance = Int
type Side = Distance
type Radius = Distance
type Area = Distance
perimeter :: Form -> Distance
perimeter (Square s) = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s) = s * 3
perimeter (Cicle r ) = pi * 2 * r
area :: Form -> Area
area (Square s) = s * s
area (Rectangle b h) = (b * h)
area (EqTriangle s) = (s * 2) / 2
area (Cicle r) = pi * r * r
Eso me permite cambiar el tipo solo cambiando una línea en el código, supongo que quiero que la Distancia esté en Int, solo cambiaré eso
perimeter :: Form -> Distance
...
totalDistance :: [Form] -> Distance
totalDistance = foldr (/x rs -> perimeter x + rs) 0
Quiero que la Distancia esté en Flotador, así que solo cambio:
type Distance = Float
Si quiero cambiarlo a Int, tengo que hacer algunos ajustes en las funciones, pero eso es otro problema.
El uso de
newtype
que crea un nuevo tipo con la misma representación que el tipo subyacente pero no sustituible con él, se considera una
buena
forma.
Es una forma barata de evitar
la obsesión primitiva
, y es especialmente útil para Haskell porque en Haskell los nombres de los argumentos de función no están visibles en la firma.
Newtypes también puede ser un lugar donde colgar instancias de clase de tipos útiles.
Dado que los nuevos tipos son omnipresentes en Haskell, con el tiempo el lenguaje ha ganado algunas herramientas e idiomas para manipularlos:
-
Coercible Una Coercible "mágica" que simplifica las conversiones entre los nuevos tipos y sus tipos subyacentes, cuando el constructor del nuevo tipo está dentro del alcance. A menudo es útil para evitar repeticiones en implementaciones de funciones.
ghci> coerce (Sum (5::Int)) :: Int
ghci> coerce [Sum (5::Int)] :: [Int]
ghci> coerce ((+) :: Int -> Int -> Int) :: Identity Int -> Identity Int -> Identity Int
-
ala
Un idioma (implementado en varios paquetes) que simplifica la selección de un nuevo tipo que podríamos usar con funciones comofoldMap
.ala Sum foldMap [1,2,3,4 :: Int] :: Int
-
GeneralizedNewtypeDeriving
. Una extensión para las instancias de derivación automática para su newtype según las instancias disponibles en el tipo subyacente. -
DerivingVia
Una extensión más general, para las instancias de derivación automática para su newtype basadas en instancias disponibles en otro newtype con el mismo tipo subyacente.
Estás usando alias de tipo, solo ayudan ligeramente con la legibilidad del código.
Sin embargo, es mejor usar
newtype
lugar del
type
para una mejor seguridad de tipos.
Me gusta esto:
data Alignment = LeftAl | CenterAl | RightAl
newtype Delimiter = Delimiter { unDelimiter :: Char }
newtype Width = Width { unWidth :: Int }
setW :: Width -> Alignment -> Delimiter -> String -> String
Usted se ocupará de la envoltura y desenvolvimiento adicional de
newtype
.
Pero el código será más robusto contra futuras refactorizaciones.
Esta guía de estilo
sugiere utilizar el
type
solo para especializar tipos polimórficos.
No consideraría esa mala forma, pero claramente, no hablo por la comunidad de Haskell en general. La función de idioma existe, por lo que puedo decir, para ese propósito particular: hacer que el código sea más fácil de leer.
Uno puede encontrar ejemplos del uso de alias de tipo en varias bibliotecas ''centrales''.
Por ejemplo, la clase de
Read
define este método:
readList :: ReadS [a]
El tipo
ReadS
es solo un alias de tipo
type ReadS a = String -> [(a, String)]
Otro ejemplo es el
tipo de
Forest
en
Data.Tree
:
type Forest a = [Tree a]
Como señala
Shersh
, también puede envolver nuevos tipos en declaraciones
newtype
.
Esto suele ser útil si necesita restringir de alguna manera el tipo original (por ejemplo, con
constructores inteligentes
) o si desea agregar funcionalidad a un tipo sin crear instancias huérfanas (un ejemplo típico es definir instancias
Arbitrary
QuickCheck para tipos que no lo hacen). De lo contrario vengo con tal instancia).
Una cosa importante a tener en cuenta es que la
Alignment
contra
Char
no es solo una cuestión de claridad, sino una cuestión de corrección.
Su tipo de
Alignment
expresa el hecho de que solo hay tres alineaciones válidas, a diferencia de la cantidad de habitantes que tiene
Char
.
Al usarlo, evita problemas con valores y operaciones no válidos, y también permite que GHC le informe informativamente sobre las coincidencias de patrones incompletos si las advertencias están activadas.
En cuanto a los sinónimos, las opiniones varían.
Personalmente, creo que los sinónimos de
type
para tipos pequeños como
Int
pueden aumentar la carga cognitiva, al hacer que rastreen diferentes nombres para lo que es rigurosamente lo mismo.
Dicho esto,
Leftaroundabout hace un gran punto
en que este tipo de sinónimo puede ser útil en las primeras etapas de creación de prototipos de una solución, cuando no necesariamente quiere preocuparse por los detalles de la representación concreta que va a adoptar para su dominio. objetos.
(Vale la pena mencionar que los comentarios aquí sobre el
type
no se aplican en gran medida al
newtype
. Sin embargo, los casos de uso son diferentes: mientras que el
type
simplemente introduce un nombre diferente para la misma cosa,
newtype
introduce una cosa diferente por fiat. un movimiento sorprendentemente poderoso - vea
la respuesta de danidiaz
para una discusión más a fondo.)