Um die Antwort von @ KarlBielefeldt zu erweitern, finden Sie hier ein vollständiges Beispiel für die Implementierung von Vektoren - Listen mit einer statisch bekannten Anzahl von Elementen - in Haskell. Halte an deinem Hut fest ...
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeFamilies #-}
import Prelude hiding (foldr, zipWith)
import qualified Prelude
import Data.Type.Equality
import Data.Foldable
import Data.Traversable
Wie Sie der langen Liste der LANGUAGE
Direktiven entnehmen können, funktioniert dies nur mit einer neueren Version von GHC.
Wir brauchen eine Möglichkeit, Längen innerhalb des Typensystems darzustellen. Per Definition ist eine natürliche Zahl entweder Null ( Z
) oder der Nachfolger einer anderen natürlichen Zahl ( S n
). So würde zum Beispiel die Nummer 3 geschrieben werden S (S (S Z))
.
data Nat = Z | S Nat
Mit der DataKinds Erweiterung , diese data
Erklärung stellt eine Art genannt Nat
und zwei Typ Bauer genannt S
und Z
- in anderen Worten , wir haben Typ-Ebene natürliche Zahlen. Beachten Sie, dass die Typen S
und Z
keine Mitgliedswerte haben - nur Arten von Typen *
werden von Werten bewohnt.
Nun führen wir eine GADT ein, die Vektoren mit bekannter Länge darstellt. Beachten Sie die Art-Signatur: Vec
Erfordert eine ArtNat
(dh eine Z
oder eine S
Art), um ihre Länge darzustellen.
data Vec :: Nat -> * -> * where
VNil :: Vec Z a
VCons :: a -> Vec n a -> Vec (S n) a
deriving instance (Show a) => Show (Vec n a)
deriving instance Functor (Vec n)
deriving instance Foldable (Vec n)
deriving instance Traversable (Vec n)
Die Definition von Vektoren ähnelt der von verknüpften Listen mit einigen zusätzlichen Informationen zur Länge auf Typebene. Ein Vektor ist entweder VNil
, in welchem Fall er eine Länge von Z
(ero) hat, oder es ist eine VCons
Zelle, die ein Element zu einem anderen Vektor hinzufügt. In diesem Fall ist seine Länge eins länger als der andere Vektor ( S n
). Beachten Sie, dass es kein Konstruktorargument vom Typ gibt n
. Es wird nur zur Kompilierungszeit verwendet, um Längen zu verfolgen. Es wird gelöscht, bevor der Compiler Maschinencode generiert.
Wir haben einen Vektortyp definiert, der statisches Wissen über seine Länge enthält. Fragen wir die Art einiger Vec
Sekunden ab, um ein Gefühl dafür zu bekommen, wie sie funktionieren:
ghci> :t (VCons 'a' (VCons 'b' VNil))
(VCons 'a' (VCons 'b' VNil)) :: Vec ('S ('S 'Z)) Char -- (S (S Z)) means 2
ghci> :t (VCons 13 (VCons 11 (VCons 3 VNil)))
(VCons 13 (VCons 11 (VCons 3 VNil))) :: Num a => Vec ('S ('S ('S 'Z))) a -- (S (S (S Z))) means 3
Das Skalarprodukt verhält sich wie bei einer Liste:
-- note that the two Vec arguments are declared to have the same length
vap :: Vec n (a -> b) -> Vec n a -> Vec n b
vap VNil VNil = VNil
vap (VCons f fs) (VCons x xs) = VCons (f x) (vap fs xs)
zipWith :: (a -> b -> c) -> Vec n a -> Vec n b -> Vec n c
zipWith f xs ys = fmap f xs `vap` ys
dot :: Num a => Vec n a -> Vec n a -> a
dot xs ys = foldr (+) 0 $ zipWith (*) xs ys
vap
, der 'flink' einen Vektor von Funktionen auf einen Vektor von Argumenten anwendet, ist Vec
anwendbar <*>
; Ich habe es nicht in eine Applicative
Instanz gestellt, weil es chaotisch wird . Beachten Sie auch, dass ich die foldr
vom Compiler generierte Instanz von verwende Foldable
.
Probieren wir es aus:
ghci> let v1 = VCons 2 (VCons 1 VNil)
ghci> let v2 = VCons 4 (VCons 5 VNil)
ghci> v1 `dot` v2
13
ghci> let v3 = VCons 8 (VCons 6 (VCons 1 VNil))
ghci> v1 `dot` v3
<interactive>:20:10:
Couldn't match type ‘'S 'Z’ with ‘'Z’
Expected type: Vec ('S ('S 'Z)) a
Actual type: Vec ('S ('S ('S 'Z))) a
In the second argument of ‘dot’, namely ‘v3’
In the expression: v1 `dot` v3
Groß! Beim Versuch, dot
Vektoren zu erstellen, deren Länge nicht übereinstimmt , wird ein Kompilierungsfehler angezeigt.
Hier ist ein Versuch einer Funktion, Vektoren miteinander zu verketten:
-- This won't compile because the type checker can't deduce the length of the returned vector
-- VNil +++ ys = ys
-- (VCons x xs) +++ ys = VCons x (concat xs ys)
Die Länge des Ausgangsvektors wäre die Summe der Längen der beiden Eingangsvektoren. Wir müssen dem Typprüfer beibringen, wie man Nat
s addiert . Hierfür verwenden wir eine Funktion auf Typebene :
type family (n :: Nat) :+: (m :: Nat) :: Nat where
Z :+: m = m
(S n) :+: m = S (n :+: m)
Diese type family
Deklaration führt eine Funktion für Typen ein, die aufgerufen werden. Mit :+:
anderen Worten, es ist ein Rezept für die Typprüfung, um die Summe zweier natürlicher Zahlen zu berechnen. Es wird rekursiv definiert - immer wenn der linke Operand größer als Z
ero ist, fügen wir dem Ausgang einen hinzu und reduzieren ihn im rekursiven Aufruf um eins. (Es ist eine gute Übung, eine Typfunktion zu schreiben, die zwei Nat
Sekunden multipliziert .) Nun können wir +++
kompilieren:
infixr 5 +++
(+++) :: Vec n a -> Vec m a -> Vec (n :+: m) a
VNil +++ ys = ys
(VCons x xs) +++ ys = VCons x (concat xs ys)
So verwenden Sie es:
ghci> VCons 1 (VCons 2 VNil) +++ VCons 3 (VCons 4 VNil)
VCons 1 (VCons 2 (VCons 3 (VCons 4 VNil)))
So weit so einfach. Was ist, wenn wir das Gegenteil von Verkettung machen und einen Vektor in zwei Teile teilen wollen? Die Längen der Ausgabevektoren hängen vom Laufzeitwert der Argumente ab. Wir möchten so etwas schreiben:
-- this won't work because there aren't any values of type `S` and `Z`
-- split :: (n :: Nat) -> Vec (n :+: m) a -> (Vec n a, Vec m a)
aber leider lässt Haskell uns das nicht zu. Wenn der Wert des n
Arguments im Rückgabetyp angezeigt werden soll (dies wird im Allgemeinen als abhängige Funktion oder Pi-Typ bezeichnet ), sind abhängige Typen mit "vollem Spektrum" erforderlich, wohingegen DataKinds
uns nur hochgestufte Typkonstruktoren zur Verfügung stehen. Anders ausgedrückt, die Typkonstruktoren S
und werden Z
nicht auf der Wertebene angezeigt. Wir müssen uns mit Singleton-Werten begnügen, um eine bestimmte Darstellung zur Laufzeit zu erhalten Nat
. *
data Natty (n :: Nat) where
Zy :: Natty Z -- pronounced 'zed-y'
Sy :: Natty n -> Natty (S n) -- pronounced 'ess-y'
deriving instance Show (Natty n)
Für einen bestimmten Typ n
(mit Art Nat
) gibt es genau einen Typbegriff Natty n
. Wir können den Singleton-Wert als Laufzeitzeugen für Folgendes verwenden n
: Wenn Sie etwas über a lernen, lernen Sie Natty
etwas über dessen Wert n
und umgekehrt.
split :: Natty n ->
Vec (n :+: m) a -> -- the input Vec has to be at least as long as the input Natty
(Vec n a, Vec m a)
split Zy xs = (Nil, xs)
split (Sy n) (Cons x xs) = let (ys, zs) = split n xs
in (Cons x ys, zs)
Lass es uns mal ausprobieren:
ghci> split (Sy (Sy Zy)) (VCons 1 (VCons 2 (VCons 3 VNil)))
(VCons 1 (VCons 2 VNil), VCons 3 VNil)
ghci> split (Sy (Sy Zy)) (VCons 3 VNil)
<interactive>:116:21:
Couldn't match type ‘'S ('Z :+: m)’ with ‘'Z’
Expected type: Vec ('S ('S 'Z) :+: m) a
Actual type: Vec ('S 'Z) a
Relevant bindings include
it :: (Vec ('S ('S 'Z)) a, Vec m a) (bound at <interactive>:116:1)
In the second argument of ‘split’, namely ‘(VCons 3 VNil)’
In the expression: split (Sy (Sy Zy)) (VCons 3 VNil)
Im ersten Beispiel haben wir einen Vektor mit drei Elementen an Position 2 erfolgreich aufgeteilt. Dann ist ein Tippfehler aufgetreten, als wir versucht haben, einen Vektor an einer Position nach dem Ende zu teilen. Singletons sind die Standardtechnik, um einen Typ von einem Wert in Haskell abhängig zu machen.
* Die singletons
Bibliothek enthält einige Template Haskell-Helfer, um Singleton-Werte wie Natty
für Sie zu generieren .
Letztes Beispiel. Was ist, wenn Sie die Dimensionalität Ihres Vektors nicht statisch kennen? Was ist zum Beispiel, wenn wir versuchen, einen Vektor aus Laufzeitdaten in Form einer Liste zu erstellen? Der Typ des Vektors hängt von der Länge der Eingabeliste ab. Anders ausgedrückt, wir können keinen foldr VCons VNil
Vektor erstellen, da sich der Typ des Ausgabevektors mit jeder Iteration der Falte ändert. Wir müssen die Länge des Vektors vor dem Compiler geheim halten.
data AVec a = forall n. AVec (Natty n) (Vec n a)
deriving instance (Show a) => Show (AVec a)
fromList :: [a] -> AVec a
fromList = Prelude.foldr cons nil
where cons x (AVec n xs) = AVec (Sy n) (VCons x xs)
nil = AVec Zy VNil
AVec
ist ein existentieller Typ : Die Typvariable n
erscheint nicht im Rückgabetyp des AVec
Datenkonstruktors. Wir können es verwenden eine zu simulieren abhängig Paar : fromList
Sie können nicht sagen , der Länge des Vektors statisch, aber es kann Ihnen etwas zurückgeben können Muster-Match auf lernen die Länge des Vektors - die Natty n
im ersten Element des Tupels . Wie Conor McBride es in einer verwandten Antwort formuliert : "Sie betrachten eine Sache und lernen dabei etwas über eine andere".
Dies ist eine übliche Technik für existenziell quantifizierte Typen. Da Sie mit Daten, für die Sie den Typ nicht kennen, eigentlich nichts data Something = forall a. Sth a
anfangen können - versuchen Sie, eine Funktion zu schreiben -, werden Existenzdaten häufig mit GADT-Beweisen gebündelt, die es Ihnen ermöglichen, den ursprünglichen Typ durch Durchführung von Pattern-Matching-Tests wiederherzustellen. Andere gebräuchliche Muster für Existentials sind das Packen von Funktionen zum Verarbeiten Ihres Typs ( data AWayToGetTo b = forall a. HeresHow a (a -> b)
), was eine gute Möglichkeit ist, erstklassige Module zu erstellen, oder das Einrichten eines Typklassenwörterbuchs ( data AnOrd = forall a. Ord a => AnOrd a
), mit dessen Hilfe der Polymorphismus von Subtypen emuliert werden kann.
ghci> fromList [1,2,3]
AVec (Sy (Sy (Sy Zy))) (VCons 1 (VCons 2 (VCons 3 Nil)))
Abhängige Paare sind nützlich, wenn die statischen Eigenschaften von Daten von dynamischen Informationen abhängen, die zur Kompilierungszeit nicht verfügbar sind. Hier ist filter
für Vektoren:
filter :: (a -> Bool) -> Vec n a -> AVec a
filter f = foldr (\x (AVec n xs) -> if f x
then AVec (Sy n) (VCons x xs)
else AVec n xs) (AVec Zy VNil)
Zu dot
zweit AVec
müssen wir GHC beweisen, dass ihre Längen gleich sind. Data.Type.Equality
definiert eine GADT, die nur erstellt werden kann, wenn ihre Typargumente identisch sind:
data (a :: k) :~: (b :: k) where
Refl :: a :~: a -- short for 'reflexivity'
Wenn Sie die Musterübereinstimmung Refl
aktivieren, weiß GHC das a ~ b
. Es gibt auch einige Funktionen, die Ihnen bei der Arbeit mit diesem Typ helfen: Wir werden gcastWith
zwischen äquivalenten Typen konvertieren und TestEquality
feststellen, ob zwei Natty
s gleich sind.
Um die Gleichheit zweier zu testen Natty
s, sind wir nach Bedarf zu nutzen die Tatsache geht , dass , wenn zwei Zahlen gleich sind, dann ihre Nachfolger auch gleich sind ( :~:
ist deckungsgleich über S
):
congSuc :: (n :~: m) -> (S n :~: S m)
congSuc Refl = Refl
Der Mustervergleich Refl
auf der linken Seite zeigt dies GHC an n ~ m
. Mit diesem Wissen ist es trivial S n ~ S m
, also lässt uns GHC Refl
sofort ein neues zurückgeben .
Jetzt können wir TestEquality
durch einfache Rekursion eine Instanz von schreiben . Wenn beide Zahlen Null sind, sind sie gleich. Wenn beide Zahlen Vorgänger haben, sind sie gleich, wenn die Vorgänger gleich sind. (Wenn sie nicht gleich sind, kehren Sie einfach zurück Nothing
.)
instance TestEquality Natty where
-- testEquality :: Natty n -> Natty m -> Maybe (n :~: m)
testEquality Zy Zy = Just Refl
testEquality (Sy n) (Sy m) = fmap congSuc (testEquality n m) -- check whether the predecessors are equal, then make use of congruence
testEquality Zy _ = Nothing
testEquality _ Zy = Nothing
Jetzt können wir die Teile zu dot
einem Paar AVec
unbekannter Länge zusammenfügen.
dot' :: Num a => AVec a -> AVec a -> Maybe a
dot' (AVec n u) (AVec m v) = fmap (\proof -> gcastWith proof (dot u v)) (testEquality n m)
Zuerst wird eine Musterübereinstimmung im AVec
Konstruktor durchgeführt, um eine Laufzeitdarstellung der Längen der Vektoren zu erhalten. Stellen Sie testEquality
nun fest, ob diese Längen gleich sind. Wenn ja, werden wir haben Just Refl
; gcastWith
wird diesen Gleichheitsnachweis verwenden, um sicherzustellen, dass er dot u v
gut typisiert ist, indem er seine implizite n ~ m
Annahme erfüllt .
ghci> let v1 = fromList [1,2,3]
ghci> let v2 = fromList [4,5,6]
ghci> let v3 = fromList [7,8]
ghci> dot' v1 v2
Just 32
ghci> dot' v1 v3
Nothing -- they weren't the same length
Beachten Sie, dass wir die Listenversion von effektiv neu implementiert haben, da ein Vektor ohne statische Kenntnis seiner Länge im Grunde genommen eine Liste ist dot :: Num a => [a] -> [a] -> Maybe a
. Der Unterschied besteht darin, dass diese Version in Bezug auf die Vektoren implementiert ist dot
. Hier ist der Punkt: vor dem Typ - Checker Sie können anrufen dot
, Sie getestet haben müssen , ob die Eingangslisten haben die gleiche Länge verwenden testEquality
. Ich if
neige dazu, -Anweisungen falsch herum zu bekommen , aber nicht in einer von der Schreibmaschine abhängigen Umgebung!
Sie können es nicht vermeiden, existenzielle Wrapper an den Rändern Ihres Systems zu verwenden, wenn Sie mit Laufzeitdaten arbeiten, aber Sie können abhängige Typen überall in Ihrem System verwenden und die existenziellen Wrapper an den Rändern belassen, wenn Sie eine Eingabevalidierung durchführen.
Da dies Nothing
nicht sehr informativ ist, können Sie die Art der dot'
Rückgabe eines Beweises, dass die Längen nicht gleich sind (in Form eines Beweises, dass ihre Differenz nicht 0 ist), im Fehlerfall weiter verfeinern . Dies ist ziemlich ähnlich der Standard-Haskell-Technik, mit Either String a
der möglicherweise eine Fehlermeldung zurückgegeben wird, obwohl ein Beweisbegriff weitaus rechenintensiver ist als eine Zeichenfolge!
Damit endet diese Pfeifentour mit einigen der Techniken, die bei der Programmierung mit Haskell-Typen üblich sind. Das Programmieren mit solchen Typen in Haskell ist wirklich cool, aber gleichzeitig sehr umständlich. Das Aufteilen all Ihrer abhängigen Daten in viele Darstellungen, die dasselbe bedeuten - Nat
Typ, Nat
Art, Natty n
Singleton - ist sehr umständlich, obwohl Code-Generatoren vorhanden sind, die bei der Erstellung des Boilerplates helfen. Derzeit gibt es auch Einschränkungen, was auf die Typstufe heraufgestuft werden kann. Es ist zwar verlockend! Der Verstand wundert sich über die Möglichkeiten - in der Literatur gibt es in Haskell Beispiele für stark typisierte printf
Datenbankschnittstellen, UI-Layout-Engines ...
Wenn Sie mehr darüber lesen möchten, gibt es eine wachsende Zahl von Literatur zu Haskell, die abhängig von der Schreibweise geschrieben wurde, sowohl veröffentlicht als auch auf Websites wie Stack Overflow. Ein guter Ausgangspunkt ist das Hasochismus- Papier - das Papier geht genau dieses Beispiel durch (unter anderem) und erörtert die schmerzhaften Teile ausführlich. Das Singleton- Paper demonstriert die Technik von Singleton-Werten (wie z. B. Natty
). Weitere Informationen zum abhängigen Tippen im Allgemeinen finden Sie im Agda- Lernprogramm. auch, Idris ist eine Sprache , in der Entwicklung , die (grob) entwickelt , um „Haskell mit abhängigen Arten“ ist.