Os tipos de Elm permitem tipos genéricos, que em geral adicionam flexibilidade a uma interface.
Por exemplo, o tipo Maybe a pode conter um valor de qualquer tipo.
type Maybe a = Nothing | Just a
Repare que, no exemplo acima, o parâmetro de tipo a é usado nos dois lados do sinal de igual =.
Dizemos que a está ligado a algum dado dentro da definição do tipo.
Em certos casos, porém, o parâmetro de tipo aparece apenas no 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 nenhum dado do tipo.
Também podemos chamar esse tipo de tipo fantasma.
Ele é 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 consegue acessar o interior do tipo Distance.
Usuários 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
Não há restrição sobre quais tipos podem ser fantasma, e tipos de registro também podem ser usados. Quando usados como tipos fantasma, os registros conseguem expressar restrições complexas que podem se transformar por meio de funções.
Vamos adicionar uma nova unidade de distância dentro do módulo Distance, chamada LegoBlock.
Blocos físicos, por serem objetos regulamentados, sempre têm 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 campos de um registro que não existe fora de um argumento de tipo, então é adequado dar a eles o tipo (), chamado de tipo unit, que não carrega nenhuma informação além do fato de estar ali.
Obter distâncias LegoBlock arbitrárias é, claro, possível, por exemplo depois de calcular diferenças ou razões entre 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 tanto a propriedade nonFractional quanto a 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 adicionar algumas funções para permitir que os usuários criem e refinem 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)
Repare que floorDistance, ceilingDistance e absDistance conseguem lidar com unidades diferentes de LegoBlock e, em geral, não fazem nenhuma suposição sobre as propriedades de entrada: elas apenas garantem 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.
É aí que está a força da técnica do tipo fantasma: oferecer escolhas flexíveis aos usuários sem abrir mão de garantias fortes.
Você, o Mestre do Mal, tem muito orgulho da qualidade dos TreasureChests que existem nas suas masmorras malignas espalhadas pelo mundo.
Não é fácil manter essa qualidade: os gerentes de masmorra vivem errando na hora de fazer tesouros, então você decide fornecer uma API em Elm para dobrá-los à sua vontade.
Há duas condições das quais você não abre mão:
TreasureChest deve ser protegido por uma senha segura de pelo menos 8 caracteresTreasureChest deve guardar um tesouro únicoOs gerentes de masmorra vão apresentar uma lista de sugestões de senha/tesouro, e só os TreasureChests adequados serão criados.
Com esses critérios, há duas formas possíveis de criar baús seguros a partir de uma lista de sugestões:
Essas duas formas podem não dar os mesmos resultados (por exemplo, para [("strong_password", GoldStatue), ("1234", GoldStatue)]), mas para você tanto faz, então você quer deixar os gerentes de masmorra decidirem.
Uma API que deixa alguma escolha para quem usa, mas ainda garante propriedades do resultado final? Parece o encaixe perfeito para a técnica de tipos fantasma!
O tipo TreasureChest e seu companheiro getTreasure já foram dados, mas você precisa criar um tipo para uma sugestão de baú.
Implemente o tipo Chest, implemente makeChest e corrija as assinaturas de tipo de secureChest e uniqueTreasures.
As assinaturas de tipo de makeChest e makeTreasureChest já foram fornecidas como parte dos requisitos; não as modifique.
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 registro para conditions.
Observe que, como Chest usa um tipo fantasma, ele deve ser opaco para impedir seu uso em qualquer lugar fora do módulo TreasureFactory.
Neste caso, ele nem sequer é exposto.
Edite as assinaturas de tipo de secureChest e uniqueTreasures para adicionar as restrições usando registros extensíveis como tipos fantasma.
secureChest deve receber um Chest sem condições específicas e retornar um Maybe Chest, com a condição extra securePassword : () em seu registro fantasma.
uniqueTreasures deve receber uma List Chest sem condições específicas e retornar uma List Chest, com a condição extra uniqueTreasure : () adicionada ao registro fantasma.
Os testes em Elm têm acesso a funções expostas, mas não às assinaturas de tipo; por isso, é impossível que os testes verifiquem se você está usando as assinaturas corretas. É claro que a indicação mais forte de que você tem as assinaturas certas é conseguir compilar e rodar o módulo e os testes, mas criamos uma regra de analisador que vai verificar suas assinaturas de tipo quando você enviar sua solução.
Depois que você tiver os baús prontos, precisa escolher os seguros.
Implemente secureChest, que só retorna a variante Just para baús com uma senha de 8 caracteres ou mais.
Os Chest que atendem a essa condição devem ter uma condição extra securePassword : () adicionada ao seu tipo fantasma.
Só os tesouros mais raros devem ser permitidos em uma masmorra: até uma única cópia faz um tesouro parecer barato.
Implemente uniqueTreasures, que recebe uma lista de Chest e retorna uma lista dos Chest que têm um tesouro único na lista de entrada.
Se um tesouro aparece duas vezes na entrada, ele 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.
Aproveite os melhores TreasureChests, que com certeza vão atrair aventureiros como moscas ao mel.
Implemente makeTreasureChest, que recebe um Chest que é seguro e único e cria um TreasureChest.
Como TreasureChest é um tipo opaco, essa será a única forma de criar um; nem os gerentes de masmorra mais ruins vão conseguir estragar tudo.
Crie sua conta no Exercism para aprender e dominar Elm com 28 conceitos110 exercícios e mentoria humana de verdade, tudo de graça.