Az Elm típusai lehetővé teszik a generikus típusokat, amelyek általában rugalmasabbá teszik a felületet.
Például a Maybe a típus bármilyen típusú értéket tárolhat.
type Maybe a = Nothing | Just a
Vegyük észre, hogy a fenti példában az a típusparaméter az egyenlőségjel (=) mindkét oldalán szerepel.
Azt mondjuk, hogy az a a típusdefiníción belül adathoz van kötve.
Bizonyos esetekben azonban a típusparaméter csak az egyenlet bal oldalán jelenik meg.
type Distance unit = Distance Float
A fenti Distance definíciójában a unit szabad típusparaméter, amely nincs a típuson belüli adathoz kötve.
Ezt a típust fantomtípusnak is nevezhetjük.
Meglepően hasznos, amikor fordítási időben szeretnénk megkötéseket érvényesíteni. Tegyük fel például, hogy csak azonos egységű távolságokat akarunk összeadni.
-- 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)
A modulon kívüli kód nem fér hozzá a Distance típus belsejéhez.
A felhasználók összeadhatnak két Distance Meter értéket, de egy Distance Meter és egy Distance Foot értéket nem.
import Distance exposing (Distance)
-- Compiles
twoMeters = Distance.add Distance.meter Distance.meter
-- Does not compile
errDist = Distance.add Distance.meter Distance.foot
Nincs semmilyen megkötés arra nézve, hogy mely típusok lehetnek fantomok, és rekordtípusok is használhatók. Fantomtípusként használva a rekordok olyan összetett megkötéseket tudnak kifejezni, amelyek függvényeken keresztül átalakíthatók.
Vegyünk fel egy új távolságegységet a Distance modulon belül, LegoBlock néven.
A fizikai építőkockák, mivel szabályozott tárgyak, mindig két tulajdonsággal bírnak: a távolságuk nem tört és nem negatív.
type LegoBlock = LegoBlock
fourStuds : Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
fourStuds = Distance 4.0
A nonFractional és a nonNegative egy olyan rekord mezői, amely csak típusargumentumban létezik, ezért kézenfekvő a () típust adni nekik, amelyet egységtípusnak hívunk, és amely nem hordoz több információt annál, mint hogy ott van.
Természetesen tetszőleges LegoBlock távolságot is kaphatunk, például távolságkülönbségek vagy arányok kiszámítása után.
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
A fenti értékek mind érvényesek és lefordulnak, viszont különösen érdekelnek minket az olyan értékek, mint a fourStuds, amelyek egyszerre rendelkeznek a nonFractional és a nonNegative tulajdonsággal, mert olyan fizikai építőkockákat képviselnek, amelyek összekapcsolhatók a következővel:
combineLegoBlocks
: Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
-> Distance { properties | unit: LegoBlock, nonFractional : (), nonNegative: () }
combineLegoBlocks = add
Adjunk hozzá néhány függvényt, amelyekkel a felhasználók létrehozhatnak és finomíthatnak LegoBlock távolságokat.
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)
Vegyük észre, hogy a floorDistance, a ceilingDistance és az absDistance a LegoBlock-tól eltérő egységgel is megbirkózik, és általában semmilyen feltevéssel nem él a bemeneti tulajdonságokkal kapcsolatban, csupán garantálják, hogy a kimenet rendelkezik egy adott tulajdonsággal, a nonFractional vagy a nonNegative tulajdonsággal.
Nézzünk meg néhány eredményt.
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))
Általában a floorDistance és a ceilingDistance más eredményt ad, de ugyanazokat a garanciákat nyújtja.
Ez a fantomtípus-technika ereje: rugalmas választási lehetőségeket kínál a felhasználóknak, miközben erős garanciákat tart fenn.
Te, a Gonosz Ura, nagyon büszke vagy a világ minden táján lévő gonosz kazamataidban található TreasureChest-ek minőségére.
Nem könnyű tartani a színvonalat: a kazamatafelügyelőid folyton elrontják a kincskészítést, ezért úgy döntesz, hogy adsz nekik egy Elm API-t, amellyel az akaratodnak vetheted alá őket.
Két feltétel van, amiben nem engedsz:
TreasureChest-et legalább 8 karakter hosszú, biztonságos jelszó védjeTreasureChest egyedi kincset őrizzenA kazamatafelügyelőid összeállítanak egy listát jelszó- és kincsjavaslatokból, és csak a megfelelő TreasureChest-ek fognak létrejönni.
Ezekkel a feltételekkel kétféleképpen hozhatók létre biztonságos ládák egy javaslatlistából:
Lehet, hogy ez a kettő nem ugyanarra az eredményre vezet (például a [("strong_password", GoldStatue), ("1234", GoldStatue)] esetében), de neked mindegy, melyik, úgyhogy a kazamatafelügyelőkre bíznád a döntést.
Egy API, amely némi választási szabadságot hagy a felhasználóknak, mégis garantálja a végeredmény bizonyos tulajdonságait? Ez pontosan a fantomtípus-technika esete!
A TreasureChest típus és a hozzá tartozó getTreasure már adott, neked viszont ki kell találnod egy típust a ládajavaslathoz.
Valósítsd meg a Chest típust, valósítsd meg a makeChest-et, és javítsd ki a secureChest és a uniqueTreasures típusaláírását.
A makeChest és a makeTreasureChest típusaláírása már adott a követelmények részeként, ezeket ne módosítsd.
A Chest ugyanazokat az adatokat tárolja, mint a TreasureChest, és két típusargumentuma legyen: az egyik a treasure, a másik pedig egy rekord fantomtípus a conditions számára.
Vedd figyelembe, hogy mivel a Chest fantomtípust használ, ezért átlátszatlannak kell lennie, hogy megakadályozzuk a használatát a TreasureFactory modulon kívül.
Ebben az esetben ugyanis egyáltalán nincs is exportálva.
Módosítsd a secureChest és a uniqueTreasures típusaláírását úgy, hogy bővíthető rekordokkal, fantomtípusként add hozzá a megkötéseket.
A secureChest egy olyan Chest-et kapjon, amelynek nincsenek konkrét feltételei, és egy Maybe Chest-et adjon vissza, amelynek fantomrekordjában ott van a securePassword : () extra feltétel.
A uniqueTreasures egy olyan List Chest-et kapjon, amelynek nincsenek konkrét feltételei, és egy olyan List Chest-et adjon vissza, amelynek fantomrekordjához hozzáadtuk a uniqueTreasure : () extra feltételt.
Az Elm tesztek hozzáférnek az exportált függvényekhez, a típusaláírásokhoz viszont nem, ezért a tesztek nem tudják ellenőrizni, hogy a megfelelő aláírásokat használod-e. Persze a legjobb jele annak, hogy jó aláírásaid vannak, az, ha sikerül lefordítani és futtatni a modult és a teszteket, de készítettünk egy elemzőszabályt, amely a megoldásod beküldése után ellenőrzi a típusaláírásaidat.
Ha készen vannak a ládák, ki kell válogatnod a biztonságosakat.
Valósítsd meg a secureChest-et, amely csak azokra a ládákra ad vissza Just változatot, amelyek jelszava legalább 8 karakter hosszú.
Azok a Chest-ek, amelyek teljesítik ezt a feltételt, kapjanak egy extra securePassword : () feltételt a fantomtípusukban.
Egy kazamatába csak a legritkább kincsek kerülhetnek: már egyetlen másolat is olcsóvá teszi a kincset.
Valósítsd meg a uniqueTreasures-t, amely egy Chest-ekből álló listát kap, és azoknak a Chest-eknek a listáját adja vissza, amelyeknek egyedi a kincse a bemeneti listában.
Ha egy kincs kétszer szerepel a bemenetben, akkor nem kerülhet a kimenetbe.
A bemeneti Chest-eknek nem szabad konkrét feltételekkel rendelkezniük, a kimenetieknek pedig hozzá kell adni a uniqueTreasure : () extra feltételt.
Élvezd a legjobb TreasureChest-eket, amelyek biztos, hogy úgy vonzzák a kalandorokat, mint a méz a legyeket.
Valósítsd meg a makeTreasureChest-et, amely egy olyan Chest-et kap, amely egyszerre biztonságos és egyedi, és létrehoz egy TreasureChest-et.
Mivel a TreasureChest átlátszatlan típus, ez lesz az egyetlen módja létrehozni egyet, így még a gyatra kazamatafelügyelők sem tudják elrontani.
Iratkozz fel az Exercism-re, hogy megtanuld és elsajátítsd a(z) Elm nyelvet 28 fogalom110 feladat segítségével, valódi emberi mentorálással, mindez ingyen.