Los tipos de Elm permiten tipos genéricos, que por lo general agregan flexibilidad a una interfaz.
Por ejemplo, el tipo Maybe a puede contener un valor de cualquier tipo.
type Maybe a = Nothing | Just a
Fíjate que en el ejemplo de arriba, el parámetro de tipo a se usa en ambos lados del signo igual =.
Decimos que a está vinculado a algún dato dentro de la definición del tipo.
Sin embargo, en ciertos casos el parámetro de tipo solo aparece en el lado izquierdo de la ecuación.
type Distance unit = Distance Float
En la definición de Distance de arriba, unit es un parámetro de tipo libre, no está vinculado a ningún dato del tipo.
También podemos llamar a este tipo un tipo fantasma.
Es sorprendentemente útil cuando queremos imponer restricciones en tiempo de compilación. Por ejemplo, queremos asegurarnos de que solo sumamos distancias de la misma unidad.
-- Distance is an opaque type, since the module does not expose its variants.
-- This means that users may only use the functions meter, foot and add to manipulate distances.
module Distance exposing (Distance, Meter, Foot, meter, foot, add)
-- The Distance type has a phantom type 'unit'.
type Distance unit = Distance Float
-- We define two types that will be used in place
-- of the phantom type 'unit' in our constructor functions.
type Meter = Meter
type Foot = Foot
-- Constructor for Meter
meter : Distance Meter
meter = Distance 1.0
-- Constructor for Foot
foot : Distance Foot
foot = Distance 0.3048
-- The add function cannot take two parameters of different types.
-- So we cannot add meters and feet by mistake.
add : Distance unit -> Distance unit -> Distance unit
add (Distance d1) (Distance d2) = Distance (d1 + d2)
El código fuera de ese módulo no puede acceder al interior del tipo Distance.
Los usuarios pueden sumar dos Distance Meter, pero no pueden sumar un Distance Meter con un Distance Foot.
import Distance exposing (Distance)
-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter
-- Does not compile
errDist = Distance.add Distance.meter Distance.foot
No hay ninguna restricción sobre qué tipos pueden ser fantasma, y los tipos de registro también se pueden usar. Cuando se usan como tipos fantasma, los registros pueden expresar restricciones complejas que pueden transformarse a través de funciones.
Agreguemos una nueva unidad de distancia dentro del módulo Distance, llamada LegoBlock.
Los bloques físicos, al ser objetos regulados, siempre tienen dos propiedades: tienen distancias no fraccionarias y no negativas.
type LegoBlock = LegoBlock
fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0
nonFractional y nonNegative son los campos de un registro que no existe fuera de un argumento de tipo, así que es adecuado darles el tipo (), llamado el tipo unit, que no contiene información más allá del hecho de que está ahí.
Obtener distancias LegoBlock arbitrarias es, por supuesto, posible, por ejemplo después de calcular diferencias o razones de distancias.
negativeStud : Distance { properties | unit: LegoBlock, nonFractional : () }
negativeStud = Distance -1.0
threeFiddyStud : Distance { properties | unit: LegoBlock, nonNegative : () }
threeFiddyStud = Distance 3.50
crazyStud : Distance { properties | unit: LegoBlock }
crazyStud = Distance -13.37
Todos los valores anteriores son válidos y compilarán; sin embargo, tenemos un interés especial en valores como fourStuds que tienen tanto la propiedad nonFractional como la nonNegative, porque representan bloques físicos que se pueden combinar con
combineLegoBlocks
: Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add
Agreguemos algunas funciones para que los usuarios puedan crear y refinar distancias LegoBlock.
newLegoBlock : Float -> Distance { properties | unit: LegoBlock }
newLegoBlock dist = Distance dist
floorDistance : Distance properties -> Distance { properties | nonFractional : () }
floorDistance (Distance dist) = Distance (toFloat (floor dist))
ceilingDistance : Distance properties -> Distance { properties | nonFractional : () }
ceilingDistance (Distance dist) = Distance (toFloat (ceiling dist))
absDistance : Distance properties -> Distance { properties | nonNegative : () }
absDistance (Distance dist) = Distance (abs dist)
Fíjate que floorDistance, ceilingDistance y absDistance pueden trabajar con unidades distintas de LegoBlock y, en general, no hacen ninguna suposición sobre las propiedades de sus argumentos; solo garantizan que la salida tendrá una propiedad específica, ya sea nonFractional o nonNegative.
Veamos algunos resultados.
import Distance exposing (Distance)
-- Compiles
distance1 = combineLegoBlocks fourStuds fourStuds
-- Does not compile
distance2 = combineLegoBlocks fourStuds threeFiddyStud
-- Compiles
distance3 = combineLegoBlocks fourStuds (floorDistance threeFiddyStud)
-- Does not compile
distance4 = combineLegoBlocks fourStuds negativeStud
-- Compiles
distance5 = combineLegoBlocks fourStuds (absDistance negativeStud)
-- Does not compile
distance6 = combineLegoBlocks fourStuds crazyStud
-- Compiles
distance7 = combineLegoBlocks fourStuds (floorDistance (absDistance crazyStud))
-- Compiles
distance8 = combineLegoBlocks fourStuds (ceilingDistance (absDistance crazyStud))
En general, floorDistance y ceilingDistance ofrecen resultados distintos, pero las mismas garantías.
Esta es la fortaleza de la técnica del tipo fantasma: ofrecer opciones flexibles a los usuarios mientras se mantienen garantías sólidas.
Tú, el Maestro del Mal, te enorgulleces de la calidad de los TreasureChest que hay en tus mazmorras malignas por todo el mundo.
Mantener esa calidad no es fácil: los administradores de tus mazmorras no dejan de arruinar la fabricación de tesoros, así que decides ofrecer una API de Elm para doblegarlos a tu voluntad.
Hay dos condiciones en las que no vas a ceder:
TreasureChest debe estar protegido por una contraseña segura de al menos 8 caracteresTreasureChest debe guardar un tesoro únicoLos administradores de tus mazmorras propondrán una lista de sugerencias de contraseña/tesoro, y solo se crearán los TreasureChest que cumplan los requisitos.
Con estos criterios, hay dos maneras posibles de crear cofres seguros a partir de una lista de sugerencias:
Esas dos maneras podrían no dar los mismos resultados (por ejemplo, para [("strong_password", GoldStatue), ("1234", GoldStatue)]), pero a ti te da igual de una u otra forma, así que quieres dejar que los administradores de las mazmorras decidan.
¿Una API que deja a los usuarios cierta libertad, pero que aun así garantiza propiedades del resultado final? ¡Suena como el encaje perfecto para la técnica del tipo fantasma!
El tipo TreasureChest y su acompañante getTreasure ya están dados, pero necesitas idear un tipo para una sugerencia de cofre.
Implementa el tipo Chest, implementa makeChest y corrige las firmas de tipo de secureChest y uniqueTreasures.
Las firmas de tipo de makeChest y makeTreasureChest ya se proporcionan como parte de los requisitos; no las modifiques.
Un Chest debe contener los mismos datos que un TreasureChest y debe tener dos argumentos de tipo: uno para treasure y un tipo fantasma de registro para conditions.
Fíjate que, dado que Chest usa un tipo fantasma, también debería ser opaco para impedir que se use en cualquier lugar fuera del módulo TreasureFactory.
En este caso, ni siquiera se expone en absoluto.
Edita las firmas de tipo de secureChest y uniqueTreasures para añadir las restricciones usando registros extensibles como tipos fantasma.
secureChest debe recibir un Chest sin condiciones específicas y devolver un Maybe Chest, con la condición adicional securePassword : () en su registro fantasma.
uniqueTreasures debe recibir una List Chest sin condiciones específicas y devolver una List Chest, con la condición adicional uniqueTreasure : () añadida al registro fantasma.
Las pruebas de Elm tienen acceso a las funciones expuestas, pero no a las firmas de tipo, así que es imposible que las pruebas verifiquen que estás usando las firmas correctas. Por supuesto, la señal más fuerte de que tienes las firmas correctas es lograr que el módulo y las pruebas se compilen y se ejecuten, pero hemos creado una regla del analizador que comprobará tus firmas de tipo en cuanto envíes tu solución.
Una vez que tengas los cofres listos, necesitas elegir los seguros.
Implementa secureChest, que solo devuelve una variante Just para los cofres con una contraseña de 8 caracteres o más.
Los Chest que cumplan esa condición deben tener una condición adicional securePassword : () añadida a su tipo fantasma.
Solo los tesoros más raros deberían estar permitidos en una mazmorra; incluso una sola copia hace que un tesoro parezca barato.
Implementa uniqueTreasures, que recibe una lista de Chest y devuelve una lista de los Chest que tienen un tesoro único en la lista que recibe.
Si un tesoro aparece dos veces en la lista que recibe, no debería estar en el resultado.
Los Chest que se pasan no deben tener condiciones específicas, y a los que se devuelven se les debe añadir la condición adicional uniqueTreasure : ().
Disfruta de los mejores TreasureChest, que seguro atraerán a los aventureros como la miel a las moscas.
Implementa makeTreasureChest, que recibe un Chest que sea tanto seguro como único y crea un TreasureChest.
Como TreasureChest es un tipo opaco, esta será la única manera de crear uno; ni siquiera los administradores de mazmorras más incompetentes podrán arruinarlo.
Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.