Tracks
/
Elm
Elm
/
Übungen
/
Schatzfabrik
Schatzfabrik

Schatzfabrik

Lernübung

Einführung

Phantomtypen

Elm-Typen erlauben generische Typen, die einer Schnittstelle im Allgemeinen mehr Flexibilität verleihen. Zum Beispiel kann der Typ Maybe a einen Wert beliebigen Typs enthalten.

type Maybe a = Nothing | Just a

Beachte, dass im obigen Beispiel der Typparameter a auf beiden Seiten des Gleichheitszeichens = verwendet wird. Wir sagen, dass a innerhalb der Typdefinition an Daten gebunden ist.

Die Phantomtyp-Technik

In bestimmten Fällen erscheint der Typparameter jedoch nur auf der linken Seite der Gleichung.

type Distance unit = Distance Float

In der obigen Definition von Distance ist unit ein freier Typparameter, der nicht an Daten im Typ gebunden ist. Wir können diesen Typ auch Phantomtyp nennen.

Er ist überraschend nützlich, wenn wir Einschränkungen zur Kompilierzeit erzwingen wollen. Zum Beispiel wollen wir sicherstellen, dass wir nur Entfernungen derselben Einheit addieren.

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

Code außerhalb dieses Moduls kann nicht auf das Innere des Typs Distance zugreifen. Nutzer können zwei Distance Meter addieren, aber nicht ein Distance Meter mit einem Distance Foot.

import Distance exposing (Distance)

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

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

Erweiterbare Records als Phantomtypen

Es gibt keine Einschränkung, welche Typen Phantomtypen sein können, und auch Record-Typen können verwendet werden. Wenn Records als Phantomtypen verwendet werden, können sie komplexe Einschränkungen ausdrücken, die sich durch Funktionen transformieren lassen.

Fügen wir eine neue Entfernungseinheit im Modul Distance namens LegoBlock hinzu. Physische Bausteine sind regulierte Objekte und haben immer zwei Eigenschaften: Sie haben nicht-fraktionale und nicht-negative Entfernungen.

type LegoBlock = LegoBlock

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

nonFractional und nonNegative sind Felder eines Records, der außerhalb eines Typarguments nicht existiert. Deshalb ist es naheliegend, ihnen den Typ () zu geben, den Unit-Typ, der keine Information enthält außer der Tatsache, dass er da ist.

Es ist natürlich möglich, beliebige LegoBlock-Entfernungen zu erhalten, zum Beispiel nach der Berechnung von Entfernungsdifferenzen oder -verhältnissen.

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

Alle oben genannten Werte sind gültig und werden kompiliert. Wir haben jedoch ein besonderes Interesse an Werten wie fourStuds, die sowohl die Eigenschaft nonFractional als auch nonNegative haben, weil sie physische Bausteine darstellen, die kombiniert werden können mit

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

Fügen wir einige Funktionen hinzu, mit denen Nutzer LegoBlock-Entfernungen erstellen und verfeinern können.

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)

Beachte, dass floorDistance, ceilingDistance und absDistance mit anderen Einheiten als LegoBlock umgehen können und im Allgemeinen keine Annahmen über die Eigenschaften der Eingabe treffen. Sie garantieren lediglich, dass die Ausgabe eine bestimmte Eigenschaft hat, entweder nonFractional oder nonNegative.

Schauen wir uns einige Ergebnisse an.

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

Im Allgemeinen liefern floorDistance und ceilingDistance unterschiedliche Ergebnisse, aber dieselben Garantien. Das ist die Stärke der Phantomtyp-Technik: Sie bietet Nutzern flexible Wahlmöglichkeiten und behält gleichzeitig starke Garantien bei.

Anleitung

Du, der Meister des Bösen, bist mächtig stolz auf die Qualität der TreasureChests in deinen finsteren Verliesen auf der ganzen Welt. Es ist nicht leicht, diese Qualität zu halten, denn deine Verliesverwalter vermasseln die Schatzherstellung immer wieder. Also beschließt du, eine Elm-API bereitzustellen, um sie deinem Willen zu unterwerfen.

Es gibt zwei Bedingungen, bei denen du nicht nachgibst:

  1. eine TreasureChest sollte durch ein sicheres Passwort mit mindestens 8 Zeichen geschützt sein
  2. in einem bestimmten Verlies sollte jede TreasureChest einen einzigartigen Schatz enthalten

Deine Verliesverwalter werden eine Liste von Passwort-/Schatzvorschlägen erstellen, und nur geeignete TreasureChests werden erstellt.

Mit diesen Kriterien gibt es zwei mögliche Wege, aus einer Liste von Vorschlägen sichere Truhen zu erstellen:

  1. die mit unsicheren Passwörtern entfernen, dann die mit doppelten Schätzen entfernen
  2. die mit doppelten Schätzen entfernen, dann die mit unsicheren Passwörtern entfernen

Diese beiden könnten unterschiedliche Ergebnisse liefern (zum Beispiel für [("strong_password", GoldStatue), ("1234", GoldStatue)]), aber dir ist beides recht, also möchtest du die Verliesverwalter entscheiden lassen.

Eine API, die den Nutzern etwas Wahlfreiheit lässt, aber trotzdem Eigenschaften des Endergebnisses garantiert? Das klingt nach einem perfekten Einsatz für die Phantomtyp-Technik!

1. Einen Platzhalter für Schatztruhen bereitstellen

Der Typ TreasureChest und sein Begleiter getTreasure sind bereits gegeben, aber du musst dir einen Typ für einen Truhenvorschlag ausdenken.

Implementiere den Typ Chest, implementiere makeChest und korrigiere die Typsignaturen von secureChest und uniqueTreasures. Die Typsignaturen von makeChest und makeTreasureChest sind bereits als Teil der Anforderungen vorgegeben, ändere sie nicht.

Eine Chest sollte dieselben Daten wie eine TreasureChest enthalten und zwei Typargumente haben, eines für treasure und einen Record-Phantomtyp für conditions. Beachte, dass Chest einen Phantomtyp verwendet und deshalb opak sein sollte, um seine Verwendung außerhalb des Moduls TreasureFactory zu verhindern. In diesem Fall wird er nicht einmal exportiert.

Ändere die Typsignaturen von secureChest und uniqueTreasures, um die Einschränkungen mithilfe erweiterbarer Records als Phantomtypen hinzuzufügen. secureChest sollte eine Chest ohne spezifische Bedingungen entgegennehmen und ein Maybe Chest zurückgeben, mit der zusätzlichen Bedingung securePassword : () in seinem Phantom-Record. uniqueTreasures sollte eine List Chest ohne spezifische Bedingungen entgegennehmen und eine List Chest zurückgeben, wobei die zusätzliche Bedingung uniqueTreasure : () zum Phantom-Record hinzugefügt wird.

Note

Elm-Tests haben Zugriff auf exportierte Funktionen, aber nicht auf Typsignaturen. Daher ist es unmöglich, dass Tests überprüfen, ob du die richtigen Signaturen verwendest. Der stärkste Hinweis darauf, dass du die richtigen Signaturen hast, ist natürlich, dass Modul und Tests kompilieren und laufen. Wir haben jedoch eine Analyzer-Regel erstellt, die deine Typsignaturen prüft, sobald du deine Lösung einreichst.

2. Sichere Truhen auswählen

Sobald deine Truhen bereit sind, musst du die sicheren herausfiltern.

Implementiere secureChest, das nur für Truhen mit einem Passwort aus 8 oder mehr Zeichen eine Just-Variante zurückgibt. Die Chests, die diese Bedingungen erfüllen, sollten eine zusätzliche Bedingung securePassword : () zu ihrem Phantomtyp hinzugefügt bekommen.

3. Einzigartige Schätze auswählen

Nur die seltensten Schätze sollten in einem Verlies erlaubt sein, schon ein einziges Exemplar lässt einen Schatz billig aussehen.

Implementiere uniqueTreasures, das eine Liste von Chest entgegennimmt und eine Liste der Chest zurückgibt, die in der Eingabeliste einen einzigartigen Schatz haben. Wenn ein Schatz in der Eingabe zweimal vorkommt, sollte er nicht in der Ausgabe sein. Die Eingabe-Chests sollten keine spezifischen Bedingungen haben, und die ausgegebenen sollten die zusätzliche Bedingung uniqueTreasure : () hinzugefügt bekommen.

4. Genieße das Endergebnis

Genieße die besten TreasureChests, die Abenteurer sicher anziehen wie Fliegen den Honig.

Implementiere makeTreasureChest, das eine Chest entgegennimmt, die sowohl sicher als auch einzigartig ist, und eine TreasureChest erstellt. Da TreasureChest ein opaker Typ ist, wird dies die einzige Möglichkeit sein, eine zu erstellen. Sogar miserable Verliesverwalter werden es nicht vermasseln können.

Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Elm Exercism

Bereit, mit Schatzfabrik zu starten?

Melde dich bei Exercism an, um Elm mit 28 Konzepte110 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.