Track
/
Elm
Elm
/
Esercizi
/
La fabbrica dei tesori
La fabbrica dei tesori

La fabbrica dei tesori

Esercizio di apprendimento

Introduzione

Tipi fantasma

I tipi di Elm permettono tipi generici, che generalmente aggiungono flessibilità a un'interfaccia. Ad esempio, il tipo Maybe a può contenere un valore di qualsiasi tipo.

type Maybe a = Nothing | Just a

Nota che nell'esempio sopra, il parametro di tipo a è usato su entrambi i lati del segno di uguale =. Diciamo che a è legato ad alcuni dati all'interno della definizione del tipo.

La tecnica del tipo fantasma

In alcuni casi, tuttavia, il parametro di tipo appare solo sul lato sinistro dell'equazione.

type Distance unit = Distance Float

Nella definizione di Distance sopra, unit è un parametro di tipo libero, non legato ad alcun dato nel tipo. Possiamo anche chiamare questo tipo un tipo fantasma.

È sorprendentemente utile quando vogliamo imporre vincoli al momento della compilazione. Ad esempio, vogliamo assicurarci di sommare solo distanze della stessa unità.

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

Il codice al di fuori di quel modulo non può accedere all'interno del tipo Distance. Gli utenti possono sommare due Distance Meter ma non possono sommare 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

Record estensibili come tipi fantasma

Non c'è alcuna restrizione su quali tipi possano essere fantasma, e anche i tipi record possono essere usati. Quando vengono usati come tipi fantasma, i record sono in grado di esprimere vincoli complessi che possono trasformarsi attraverso le funzioni.

Aggiungiamo una nuova unità di distanza all'interno del modulo Distance chiamata LegoBlock. I blocchi fisici, essendo oggetti regolamentati, hanno sempre due proprietà: hanno distanze non frazionarie e non negative.

type LegoBlock = LegoBlock

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

nonFractional e nonNegative sono i campi di un record che non esiste al di fuori di un argomento di tipo, quindi è appropriato dare loro il tipo (), chiamato tipo unità, che non contiene alcuna informazione oltre al fatto di esserci.

Ottenere distanze LegoBlock arbitrarie è ovviamente possibile, ad esempio dopo aver calcolato differenze o rapporti di distanze.

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

Tutti i valori sopra sono validi e verranno compilati, tuttavia abbiamo un interesse particolare per valori come fourStuds che hanno entrambe le proprietà nonFractional e nonNegative perché rappresentano blocchi fisici che possono essere combinati con

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

Aggiungiamo alcune funzioni per permettere agli utenti di creare e raffinare distanze 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)

Nota che floorDistance, ceilingDistance e absDistance possono gestire unità diverse da LegoBlock, e in generale non fanno alcuna assunzione sulle proprietà di input, garantiscono semplicemente che l'output avrà una proprietà specifica, o nonFractional o nonNegative.

Vediamo alcuni risultati.

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

In generale, floorDistance e ceilingDistance forniscono risultati diversi, ma le stesse garanzie. Questa è la forza della tecnica del tipo fantasma: offrire scelte flessibili agli utenti mantenendo garanzie forti.

Istruzioni

Tu, il Signore del Male, vai molto fiero della qualità dei TreasureChest che si trovano nei tuoi dungeon malvagi in tutto il mondo. Non è facile mantenere questa qualità: i tuoi gestori di dungeon continuano a sbagliare la produzione dei tesori, così decidi di fornire un'API in Elm per piegarli alla tua volontà.

Ci sono due condizioni su cui non transigerai:

  1. un TreasureChest deve essere protetto da una password sicura di almeno 8 caratteri
  2. in uno specifico dungeon, ogni TreasureChest deve contenere un tesoro unico

I tuoi gestori di dungeon proporranno un elenco di suggerimenti password/tesoro, e verranno creati solo i TreasureChest adatti.

Con questi criteri, ci sono due modi possibili di creare scrigni sicuri a partire da un elenco di suggerimenti:

  1. rimuovere quelli con password non sicure, poi rimuovere quelli con tesori duplicati
  2. rimuovere quelli con tesori duplicati, poi rimuovere quelli con password non sicure

Queste due strade potrebbero non dare gli stessi risultati (per esempio per [("strong_password", GoldStatue), ("1234", GoldStatue)]), ma per te va bene in entrambi i casi, quindi vuoi lasciare decidere ai gestori di dungeon.

Un'API che lascia agli utenti un po' di scelta, ma garantisce comunque alcune proprietà del risultato finale? Sembra il caso perfetto per la tecnica del tipo fantasma!

1. Fornisci un segnaposto per gli scrigni del tesoro

Il tipo TreasureChest e il suo compagno getTreasure sono già forniti, ma devi ideare un tipo per un suggerimento di scrigno.

Implementa il tipo Chest, implementa makeChest e correggi le firme di tipo di secureChest e uniqueTreasures. Le firme di tipo di makeChest e makeTreasureChest sono già fornite come parte dei requisiti: non modificarle.

Un Chest deve contenere gli stessi dati di un TreasureChest e deve avere due argomenti di tipo: uno per treasure e un tipo fantasma record per conditions. Nota che, poiché Chest usa un tipo fantasma, dovrebbe anche essere opaco per impedirne l'uso al di fuori del modulo TreasureFactory. In questo caso, non viene nemmeno esposto affatto.

Modifica le firme di tipo di secureChest e uniqueTreasures per aggiungere i vincoli usando record estensibili come tipi fantasma. secureChest deve prendere un Chest senza condizioni specifiche e restituire un Maybe Chest, con la condizione extra securePassword : () nel suo record fantasma. uniqueTreasures deve prendere una List Chest senza condizioni specifiche e restituire una List Chest, con la condizione extra uniqueTreasure : () aggiunta al record fantasma.

Note

I test di Elm hanno accesso alle funzioni esposte, ma non alle firme di tipo: è quindi impossibile per i test verificare che tu stia usando le firme giuste. Certo, l'indizio più forte che hai le firme giuste è riuscire a far compilare ed eseguire il modulo e i test, ma abbiamo creato una regola dell'analizzatore che controllerà le tue firme di tipo una volta che invii la soluzione.

2. Seleziona gli scrigni sicuri

Una volta che hai degli scrigni pronti, devi scegliere quelli sicuri.

Implementa secureChest in modo che restituisca una variante Just solo per gli scrigni con una password di 8 o più caratteri. I Chest che soddisfano quella condizione devono avere una condizione extra securePassword : () aggiunta al loro tipo fantasma.

3. Seleziona i tesori unici

In un dungeon dovrebbero essere ammessi solo i tesori più rari: anche una sola copia fa sembrare un tesoro dozzinale.

Implementa uniqueTreasures, che prende una lista di Chest e restituisce una lista dei Chest che hanno un tesoro unico nella lista di input. Se un tesoro compare due volte nell'input, non deve essere nell'output. I Chest in input non devono avere condizioni specifiche, mentre quelli in output devono avere la condizione extra uniqueTreasure : () aggiunta.

4. Goditi il prodotto finale

Goditi i migliori TreasureChest, che attrarranno sicuramente gli avventurieri come il miele attira le mosche.

Implementa makeTreasureChest, che prende un Chest sicuro e unico e crea un TreasureChest. Dato che TreasureChest è un tipo opaco, questo sarà l'unico modo per crearne uno: nemmeno i gestori di dungeon più scadenti riusciranno a rovinarlo.

Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
Elm Exercism

Vuoi iniziare La fabbrica dei tesori?

Iscriviti a Exercism per imparare e padroneggiare Elm con 28 concetti110 esercizi e il mentoring di persone reali, tutto gratis.