Opaque Types sind eine Kapselungstechnik in Elm.
Manche Leute nennen das auch Informationsverbergung oder das Verbergen von Implementierungsdetails.
Sie werden außerdem häufig verwendet, um Validierung zu erzwingen, und zwar mit dem Muster Parse, dont validate.
Zum Beispiel könntest du einen Opaque Type EmailAddress haben, der nur gültige E-Mail-Adressen enthalten kann (im Gegensatz zu einem String, der alles enthalten kann).
Custom Types haben eine oder mehrere Varianten, und der Compiler erzeugt automatisch eine Funktion, um jede Variante zu erstellen, wobei er den Namen der Variante verwendet. Das ist die einzige Möglichkeit, wie Custom Types erstellt werden können. Der Compiler erlaubt außerdem, diese Varianten zu destrukturieren.
type Maybe a
= Nothing --> Variant implies a 'Nothing' function, with type 'Maybe a', and destructuring of 'Nothing'
| Just a --> Variant implies a 'Just' function, with type 'a -> Maybe a', and destructuring of 'Just _'
Standardmäßig ist all das intern für die Datei bzw. das Modul, und wir verwenden das Schlüsselwort exposing, um auszuwählen, was exportiert wird.
Das Modul Maybe exportiert den Typ Maybe (damit andere Module vorhandene Maybe-Werte verwenden können) und exportiert außerdem die Varianten Nothing und Just (damit andere Module Maybe-Werte erstellen und destrukturieren können).
Dazu verwendet es die folgende Syntax.
Der Teil Maybe(..) weist Elm an, alle Varianten zu exportieren, ebenso wie die Custom-Type-Definition.
module Maybe exposing
( Maybe(..)
-- ...
)
Um einen Opaque Type zu erstellen, entscheidest du dich einfach dafür, die Varianten nicht zu exportieren.
Um das für den Typ Maybe zu tun, würdest du die folgende Syntax verwenden:
module Maybe exposing
( Maybe
-- ...
)
Wenn du das tust, musst du fast immer eine andere Möglichkeit bereitstellen, um die Varianten zu erstellen und abzurufen, indem du eigene Funktionen exportierst. Diese eigenen Funktionen erlauben es dir, etwas Logik anzuwenden, wenn du die Varianten erstellst und abrufst. Das verbirgt die internen Implementierungsdetails des Moduls und bedeutet, dass du die Implementierungsdetails beliebig ändern kannst, solange die exportierten Funktionen gleich bleiben. Es erlaubt dir außerdem, Invarianten aufrechtzuerhalten, wenn du den Typ erstellst und verwendest.
Der wohl häufigste Einsatz von Opaque Types ist das Erzwingen von Validierung mit dem Muster Parse, dont validate.
Bei diesem Muster exportierst du eine Funktion, die eine Eingabe mit weniger Struktur bzw. Typsicherheit entgegennimmt, als du möchtest, zum Beispiel einen String, und parst diese dann in einen stärker strukturierten Typ wie EmailAddress, wobei sie entweder den Typ oder einen Fehler zurückgibt (meist Maybe oder Result).
Im folgenden Code zum Beispiel können externe Module einen EmailAddress-Typ nur mit der Funktion parseEmailAddress erstellen. Wenn du also mit dem Typ EmailAddress arbeitest, kannst du sicher sein, dass er gültig ist.
module EmailAddress exposing ( EmailAddress, parseEmailAddress, getEmailAddress )
-- The `EmailAddress` custom type has one variant, also called `EmailAddress`
-- When a custom type has one variant, it is idiomatic to use the same name for both.
type EmailAddress =
EmailAddress String
-- create an EmailAddress variant
parseEmailAddress: String -> Maybe EmailAddress
parseEmailAddress candidateEmailAddress =
if isValidEmailAddress candidateEmailAddress then
Just (EmailAddress candidateEmailAddress)
else
Nothing
-- get an EmailAddress variant
getEmailAddress: EmailAddress -> String
getEmailAddress ( EmailAddress emailAdress ) =
emailAdress
isValidEmailAddress: String -> bool
-- ...