Percursos
/
Elm
Elm
/
Exercícios
/
Fábrica de Tesouros
Fábrica de Tesouros

Fábrica de Tesouros

Exercício de aprendizagem

Introdução

Tipos fantasma

Os tipos de Elm permitem tipos genéricos, que geralmente acrescentam flexibilidade a uma interface. Por exemplo, o tipo Maybe a pode conter um valor de qualquer tipo.

type Maybe a = Nothing | Just a

Repara que, no exemplo acima, o parâmetro de tipo a é usado em ambos os lados do sinal de igual =. Dizemos que a está ligado a alguns dados dentro da definição do tipo.

A técnica do tipo fantasma

Em certos casos, no entanto, o parâmetro de tipo aparece apenas do lado esquerdo da equação.

type Distance unit = Distance Float

Na definição de Distance acima, unit é um parâmetro de tipo livre, não ligado a quaisquer dados do tipo. Também podemos chamar a este tipo um tipo fantasma.

É surpreendentemente útil quando queremos impor restrições em tempo de compilação. Por exemplo, queremos garantir que só somamos distâncias da mesma unidade.

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

O código fora desse módulo não pode aceder ao interior do tipo Distance. Os utilizadores podem somar dois Distance Meter, mas não podem somar um Distance Meter com um Distance Foot.

import Distance exposing (Distance)

-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter

-- Does not compile
errDist = Distance.add Distance.meter Distance.foot

Registos extensíveis como tipos fantasma

Não há restrições quanto aos tipos que podem ser fantasmas, e os tipos de registo também podem ser usados. Quando são usados como tipos fantasma, os registos conseguem expressar restrições complexas que se podem transformar através de funções.

Vamos acrescentar uma nova unidade de distância dentro do módulo Distance, chamada LegoBlock. Os blocos físicos, sendo objetos regulados, têm sempre duas propriedades: distâncias não fracionárias e não negativas.

type LegoBlock = LegoBlock

fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0

nonFractional e nonNegative são os campos de um registo que não existe fora de um argumento de tipo, por isso é adequado dar-lhes o tipo (), chamado tipo unit, que não contém mais informação do que o facto de estar lá.

Obter distâncias LegoBlock arbitrárias é, naturalmente, possível, por exemplo depois de calcular diferenças ou rácios de distâncias.

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 os valores acima são válidos e vão compilar, mas temos um interesse especial em valores como fourStuds, que têm as duas propriedades nonFractional e nonNegative, porque representam blocos físicos que podem ser combinados com

combineLegoBlocks
  :  Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
  -> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
  -> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add

Vamos acrescentar algumas funções para permitir aos utilizadores criar e refinar distâncias 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)

Repara que floorDistance, ceilingDistance e absDistance conseguem lidar com outras unidades além de LegoBlock e, de um modo geral, não fazem suposições sobre as propriedades do valor que recebem; garantem apenas que a saída terá uma propriedade específica, nonFractional ou nonNegative.

Vamos ver alguns 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))

Em geral, floorDistance e ceilingDistance fornecem resultados diferentes, mas as mesmas garantias. É esta a força da técnica do tipo fantasma: proporcionar escolhas flexíveis aos utilizadores e manter, ao mesmo tempo, garantias fortes.

Instruções

Tu, o Mestre do Mal, tens muito orgulho na qualidade dos TreasureChests que se encontram nas tuas masmorras malignas por todo o mundo. Não é fácil manter a qualidade, os teus gestores de masmorras não param de estragar a criação de tesouros, por isso decides fornecer uma API de Elm para os dobrar à tua vontade.

Há duas condições em que não cedes:

  1. um TreasureChest deve ser protegido por uma palavra-passe segura de pelo menos 8 carateres
  2. numa masmorra específica, cada TreasureChest deve guardar um tesouro único

Os teus gestores de masmorras vão apresentar uma lista de sugestões de palavra-passe/tesouro, e só serão criados os TreasureChests adequados.

Com estes critérios, há duas formas possíveis de criar baús seguros a partir de uma lista de sugestões:

  1. remover os que têm palavras-passe inseguras e, depois, remover os que têm tesouros duplicados
  2. remover os que têm tesouros duplicados e, depois, remover os que têm palavras-passe inseguras

Estas duas podem não dar os mesmos resultados (por exemplo, para [("strong_password", GoldStatue), ("1234", GoldStatue)]), mas tanto te faz, por isso queres deixar que os gestores de masmorras decidam.

Uma API que deixa alguma escolha aos utilizadores, mas que ainda garante propriedades do resultado final? Parece um encaixe perfeito para a técnica do tipo fantasma!

1. Fornece um espaço reservado para baús do tesouro

O tipo TreasureChest e a função getTreasure que o acompanha já estão definidos, mas precisas de criar um tipo para uma sugestão de baú.

Implementa o tipo Chest, implementa makeChest e corrige as assinaturas de tipo de secureChest e uniqueTreasures. As assinaturas de tipo de makeChest e makeTreasureChest já são fornecidas como parte dos requisitos; não as modifiques.

Um Chest deve guardar os mesmos dados que um TreasureChest e deve ter dois argumentos de tipo, um para treasure e um tipo fantasma de registo para conditions. Repara que, como Chest usa um tipo fantasma, deve por isso ser opaco para impedir a sua utilização em qualquer lugar fora do módulo TreasureFactory. Neste caso, não é sequer exposto.

Edita as assinaturas de tipo de secureChest e uniqueTreasures para adicionar as restrições usando registos extensíveis como tipos fantasma. secureChest deve receber um Chest sem condições específicas e devolver um Maybe Chest, com a condição extra securePassword : () no seu registo fantasma. uniqueTreasures deve receber uma List Chest sem condições específicas e devolver uma List Chest, com a condição extra uniqueTreasure : () adicionada ao registo fantasma.

Note

Os testes de Elm têm acesso às funções expostas, mas não às assinaturas de tipo; por isso, é impossível que os testes verifiquem se estás a usar as assinaturas corretas. Claro que a indicação mais forte de que tens as assinaturas corretas é conseguires compilar e executar o módulo e os testes, mas criámos uma regra do analisador que vai verificar as tuas assinaturas de tipo assim que submeteres a tua solução.

2. Seleciona baús seguros

Assim que tiveres os baús prontos, precisas de escolher os seguros.

Implementa secureChest que só devolve uma variante Just para baús com uma palavra-passe de 8 ou mais carateres. Os Chest que cumprem essas condições devem ter uma condição extra securePassword : () adicionada ao seu tipo fantasma.

3. Seleciona tesouros únicos

Só os tesouros mais raros devem ser permitidos numa masmorra; até uma única cópia faz um tesouro parecer barato.

Implementa uniqueTreasures que recebe uma lista de Chest e devolve uma lista dos Chest que têm um tesouro único na lista de entrada. Se um tesouro aparecer duas vezes na entrada, não deve estar na saída. Os Chest de entrada não devem ter condições específicas, e os de saída devem ter a condição extra uniqueTreasure : () adicionada.

4. Aproveita o produto final

Aproveita o melhor dos TreasureChests, que vão de certeza atrair aventureiros como moscas ao mel.

Implementa makeTreasureChest que recebe um Chest que é ao mesmo tempo seguro e único e cria um TreasureChest. Como TreasureChest é um tipo opaco, esta será a única forma de criar um; nem os gestores de masmorras mais incompetentes o conseguirão estragar.

Editar via GitHub A ligação abre numa nova janela ou separador
Elm Exercism

Estás pronto para começar Fábrica de Tesouros?

Inscreve-te no Exercism para aprenderes e dominares Elm com 28 conceitos110 exercícios, e mentoria humana real, tudo grátis.