Rutas
/
Elm
Elm
/
Ejercicios
/
La fábrica de tesoros
La fábrica de tesoros

La fábrica de tesoros

Ejercicio de aprendizaje

Introducción

Tipos fantasma

Los tipos de Elm permiten tipos genéricos, que en general aportan 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 en que, en el ejemplo anterior, el parámetro de tipo a se usa a ambos lados del signo igual =. Decimos que a está ligado a algunos datos dentro de la definición del tipo.

La técnica del tipo fantasma

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 anterior, unit es un parámetro de tipo libre, no está ligado a ningún dato del tipo. También podemos llamar a este tipo un tipo fantasma.

Resulta sorprendentemente útil cuando queremos imponer restricciones en tiempo de compilación. Por ejemplo, queremos asegurarnos de sumar solo 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

Registros extensibles como tipos fantasma

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.

Vamos a añadir una nueva unidad de distancia dentro del módulo Distance llamada LegoBlock. Los bloques físicos, al ser objetos regulados, siempre tienen dos propiedades: sus distancias no son fraccionarias y no son 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 apropiado darles el tipo (), llamado el tipo unidad, que no contiene más información que el hecho de estar ahí.

Obtener distancias LegoBlock arbitrarias es, por supuesto, posible, por ejemplo tras calcular diferencias o cocientes 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, nos interesan especialmente valores como fourStuds, que tienen las propiedades nonFractional y nonNegative a la vez, 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

Vamos a añadir 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 en que floorDistance, ceilingDistance y absDistance pueden manejar unidades distintas de LegoBlock y, en general, no hacen ninguna suposición sobre las propiedades de entrada; simplemente garantizan que la salida tendrá una propiedad concreta, 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 proporcionan resultados distintos, pero las mismas garantías. Esta es la fuerza de la técnica del tipo fantasma: ofrecer opciones flexibles a los usuarios manteniendo garantías sólidas.

Instrucciones

Tú, el Amo del Mal, estás muy orgulloso de la calidad de los TreasureChest que se encuentran en tus malvadas mazmorras por todo el mundo. No es fácil mantener la calidad: tus gerentes de mazmorras no dejan de arruinar la fabricación de tesoros, así que decides proporcionar una API de Elm para doblegarlos a tu voluntad.

Hay dos condiciones en las que no cederás:

  1. un TreasureChest debe estar protegido por una contraseña segura de al menos 8 caracteres
  2. en una mazmorra concreta, cada TreasureChest debe contener un tesoro único

Tus gerentes de mazmorras propondrán un array de sugerencias de contraseña/tesoro, y solo se crearán los TreasureChest adecuados.

Con estos criterios, hay dos formas posibles de crear cofres seguros a partir de un array de sugerencias:

  1. eliminar los que tengan contraseñas poco seguras y, después, eliminar los que tengan tesoros duplicados
  2. eliminar los que tengan tesoros duplicados y, después, eliminar los que tengan contraseñas poco seguras

Es posible que esas dos formas no den el mismo resultado (por ejemplo, para [("strong_password", GoldStatue), ("1234", GoldStatue)]), pero a ti te da igual una que otra, así que quieres dejar que los gerentes de mazmorras decidan.

¿Una API que deja a los usuarios cierta libertad de elección, pero que aun así garantiza propiedades del resultado final? ¡Suena como el candidato perfecto para la técnica del tipo fantasma!

1. Proporciona un marcador de posición para los cofres del tesoro

El tipo TreasureChest y su compañera getTreasure ya están dados, pero tienes que 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 registro de tipo fantasma para conditions. Ten en cuenta que, como Chest usa un tipo fantasma, debe ser opaco para impedir su uso 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 tomar un Chest sin condiciones específicas y devolver un Maybe Chest, con la condición adicional securePassword : () en su registro fantasma. uniqueTreasures debe tomar un List Chest sin condiciones específicas y devolver un List Chest, con la condición adicional uniqueTreasure : () añadida al registro fantasma.

Note

Las pruebas de Elm tienen acceso a las funciones expuestas, pero no a las firmas de tipo, por lo que es imposible que las pruebas verifiquen que estás usando las firmas correctas. Por supuesto, la indicación más contundente de que tienes las firmas correctas es conseguir que el módulo y las pruebas compilen y se ejecuten, pero hemos creado una regla del analizador que comprobará tus firmas de tipo una vez que envíes tu solución.

2. Selecciona los cofres seguros

Una vez que tengas los cofres listos, tienes que 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.

3. Selecciona los tesoros únicos

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 toma un array de Chest y devuelve un array de los Chest que tienen un tesoro único en el array de entrada. Si un tesoro aparece dos veces en la entrada, no debería estar en la salida. Los Chest de entrada no deben tener condiciones específicas, y los de salida deben tener la condición adicional uniqueTreasure : () añadida.

4. Disfruta del producto final

Disfruta de los mejores TreasureChest, que sin duda atraerán a los aventureros como moscas a la miel.

Implementa makeTreasureChest, que toma un Chest que sea a la vez seguro y único y crea un TreasureChest. Como TreasureChest es un tipo opaco, esta será la única forma de crear uno; ni siquiera los gerentes de mazmorras más incompetentes podrán estropearlo.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Elm Exercism

¿Listo para empezar La fábrica de tesoros?

Regístrate en Exercism para aprender y dominar Elm con 28 conceptos110 ejercicios y mentoría humana real, todo gratis.