Newtypen der Großmacht

Newtype ist eine spezielle Datentypdeklaration. So dass es nur einen Konstruktor und ein Feld enthÀlt.

newtype Foo a = Bar a newtype Id = MkId Word 


Typische Fragen fĂŒr Neulinge


Was ist der Unterschied zu Datentypdaten?

 data Foo a = Bar a data Id = MkId Word 

Die HauptspezifitÀt des Newtype besteht darin, dass er aus denselben Teilen besteht wie sein einziges Feld. Genauer gesagt unterscheidet es sich vom Original auf Typebene, hat jedoch die gleiche Darstellung im Speicher und wird streng (nicht trÀge) berechnet.
Kurz gesagt - Newtype sind aufgrund ihrer PrÀsentation effektiver.

Ja, es bedeutet mir nichts ... Ich werde Daten verwenden
Nein, am Ende können Sie immer die Erweiterung -funpack-strict-fields :) fĂŒr strenge (nicht faule) Felder aktivieren oder direkt angeben

 data Id = MkId !Word 

Die Leistung des Newtype ist jedoch nicht auf die Recheneffizienz beschrÀnkt. Sie sind viel stÀrker!

3 neue Rollen




Implementierung ausblenden


 module Data.Id (Id()) where newtype Id = MkId Word 

newtype unterscheidet sich vom ursprĂŒnglichen, intern nur Word .
Wir verstecken den MkId- Konstruktor jedoch außerhalb des Moduls.

Implementierung Implementierung


 {-# LANGUAGE GeneralizedNewtypeDeriving #-} newtype Id = MkId Word deriving (Num, Eq) 

Obwohl dies nicht im Haskell2010-Standard enthalten ist, können Sie durch Erweitern der Ausgabe generischer newTypes automatisch auf das Verhalten von newtype schließen, das dem Verhalten des internen Felds entspricht. In unserem Fall ist das Verhalten von Gl. Id und Num Id dasselbe wie bei Gl. Wort und Num Wort .

Durch den Ausbau der raffinierten Zucht ( DerivingVia ) kann viel mehr erreicht werden, dazu spÀter mehr.

Umsetzung der Wahl


In einigen FÀllen können Sie trotz eines eigenen Konstruktors Ihre interne Darstellung verwenden.

Herausforderung


Es gibt eine Liste von ganzen Zahlen. Finden Sie den Maximal- und Gesamtbetrag in nur einem Durchgang auf der Liste.
Und verwenden Sie keine Foldl- und Fold- Pakete .

Typische Antwort


NatĂŒrlich falten ! :) :)

 foldr :: Foldable t => (a -> b -> b) -> b -> ta -> b {- -- instance Foldable [] foldr :: (a -> b -> b) -> b -> [a] -> b -} 

Und die endgĂŒltige Funktion wird folgendermaßen beschrieben:

 aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = foldr (\el (m, s) -> (Just el `max` m, el + s)) (Nothing, 0) {- ghci> aggregate [1, 2, 3, 4] (Just 4, 10) -} 

Wenn Sie genau hinschauen, sehen Sie Àhnliche Operationen auf beiden Seiten: Nur el `max` m und el + s . In beiden FÀllen Mapping und BinÀroperation. Und die leeren Elemente sind Nichts und 0 .

Ja, das sind Monoide!

Monoid und Halbgruppe im Detail
Eine Halbgruppe ist eine Eigenschaft einer assoziativen binÀren Operation

 x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z 

Ein Monoid ist eine Eigenschaft einer assoziativen Operation (d. H. Einer Halbgruppe).

 x ⋄ (y ⋄ z) == (x ⋄ y) ⋄ z 

Das hat ein leeres Element, das weder rechts noch links ein Element Àndert

 x ⋄ empty == x == empty ⋄ x 


Sowohl max als auch (+) sind assoziativ, beide haben leere Elemente - Nothing und 0 .

Und die Kombination der Kartierung von Monoiden zusammen mit der Faltung ist faltbar !

Faltbarer detaillierter
Erinnern Sie sich an die Definition des Faltens:

 class Foldable t where foldMap :: (Monoid m) => (a -> m) -> ta -> m ... 


Wenden wir das Faltverhalten auf max und (+) an . Wir können nicht mehr als eine Implementierung des Word- Monoids organisieren. Es ist Zeit, die Implementierung der Newtype- Auswahl zu nutzen!

 {-# LANGUAGE GeneralizedNewtypeDeriving #-} -- already in Data.Semigroup & Data.Monoid newtype Sum a = Sum {getSum :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Sum a) where (<>) = (+) instance (Num a, Ord a) => Monoid (Sum a) where mempty = Sum 0 newtype Max a = Max {getMax :: a} deriving (Num, Eq, Ord) instance (Num a, Ord a) => Semigroup (Max a) where (<>) = max 

Es ist notwendig, eine Bemerkung zu machen.

Tatsache ist, dass wir, um ein Monoid fĂŒr den Datentyp Max a zu sein , ein minimales Element benötigen, dh, dass ein leeres Element existiert. So kann nur ein begrenzter Max a ein Monoid sein.

Theoretisch korrektes maximales Elementmonoid
 newtype Max a = Max a instance Ord a => Semigroup (Max a) instance Bounded a => Monoid (Max a) 


Irgendwie mĂŒssen wir also unseren Datentyp konvertieren, damit ein leeres Element erscheint und wir die Koagulation verwenden können.

 -- already in Prelude data Maybe a = Nothing | Just a instance Semigroup a => Semigroup (Maybe a) where Nothing <> b = b b <> Nothing = b (Just a) <> (Just b) = Just (a <> b) instance Semigroup a => Monoid (Maybe a) where mempty = Nothing -- ------ instance Functor Maybe where fmap _ Nothing = Nothing fmap f (Just b) = Just (fb) 

Das konjugierte Vielleicht- Element verwandelt eine Halbgruppe in ein Monoid!

Liberalisierung von BeschrÀnkungen in neueren Versionen von GHC
ZurĂŒck in GHC 8.2 war eine Monoid-Typ-EinschrĂ€nkung erforderlich

 instance Monoid a => Monoid (Maybe a) 

was bedeutet, wir brauchten einen anderen neuen Typ:

 -- already in Data.Semigroup & Data.Monoid newtype Option a = Option {getOption :: Maybe a} deriving (Eq, Ord, Semigroup) instance (Ord a, Semigroup a) => Monoid (Option a) where mempty = Option Nothing 

Und es ist bereits in GHC 8.4 viel einfacher, wo nur eine Halbgruppe mit TypbeschrÀnkung erforderlich ist und selbst kein Optionstyp erstellt werden muss.

 instance Semigroup a => Monoid (Maybe a) 


Faltreaktion


Nun aktualisieren Sie den Code mithilfe von Reduzierbarkeit und Pfeilen.
Wir erinnern uns, dass (.) Nur eine funktionale Zusammensetzung ist:

  (.) :: (b -> c) -> (a -> b) -> a -> c f . g = \x -> f (gx) 

Und denken Sie daran, dass fmap ein Funktor ist:

 fmap :: Functor f => (a -> b) -> fa -> fb 

und seine Implementierung fĂŒr Vielleicht wird etwas höher beschrieben.

Pfeil detaillierter
Pfeile sind Eigenschaften einiger Funktionen, mit denen Sie in einem Blockdiagramm damit arbeiten können.
Weitere Details finden Sie hier: Pfeile: Eine allgemeine Schnittstelle zur Berechnung
In unserem Fall verwenden wir die Pfeilfunktion
Also

 instance Arrow (->) 

Wir werden die Funktionen verwenden:

 (***) :: Arrow a => abc -> ab' c' -> a (b, b') (c, c') (&&&) :: Arrow a => abc -> abc' -> ab (c, c') 

FĂŒr unseren Fall
 abc == (->) bc == b -> c 

Dementsprechend reduziert sich die Signatur unserer Funktionen auf:

 (***) :: (b -> c) -> (b' -> c') -> ((b, b') -> (c, c')) (&&&) :: (b -> c) -> (b -> c') -> (b -> (c, c')) 

Oder in ganz einfachen Worten, die Funktion (***) kombiniert zwei Funktionen mit einem Argument (und einem Ausgabetyp) zu einer Funktion mit der Arbeit eines Paares von Argumenten am Eingang bzw. am Ausgang, einem Paar von Ausgabetypen.

Die Funktion (&&&) ist eine abgespeckte Version (***) , bei der der Typ der Eingabeargumente der beiden Funktionen gleich ist und bei der Eingabe nicht zwei Argumente, sondern ein Argument vorhanden sind.

Insgesamt hat die Vereinigungsfunktion die Form erhalten:

 import Data.Semigroup import Data.Monoid import Control.Arrow aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = (fmap getMax *** getSum) . (foldMap (Just . Max &&& Sum)) {- -- for GHC 8.2 aggregate = (fmap getMax . getOption *** getSum) . (foldMap (Option . Just . Max &&& Sum)) -} 

Es stellte sich sehr kurz heraus!

Es ist jedoch immer noch anstrengend, Daten von verschachtelten Typen zu verpacken und zu invertieren!
Sie können es weiter reduzieren, und eine ressourcenfreie erzwungene Konvertierung hilft uns!

Sichere, ressourcenfreie erzwungene Konvertierung und Typrollen


Es gibt eine Funktion aus dem Unsafe.Coerce- Paket - unsafeCoerce

 import Unsafe.Coerce(unsafeCoerce) unsafeCoerce :: a -> b 

Die Funktion zwangsweise unsicher konvertiert den Typ: von a nach b .
TatsĂ€chlich ist die Funktion magisch und weist den Compiler an, Daten vom Typ a als Typ b zu betrachten , ohne die Konsequenzen dieses Schritts zu berĂŒcksichtigen.

Es kann verwendet werden, um verschachtelte Typen zu konvertieren, aber Sie mĂŒssen sehr vorsichtig sein.

Im Jahr 2014 gab es eine Revolution mit Newtype , nÀmlich eine sichere, ressourcenlose erzwungene Konvertierung!

 import Data.Coerce(coerce) coerce :: Coercible ab => a -> b 

Diese Funktion hat eine neue Ära in der Arbeit mit Newtype eingeleitet .

Coercible Forced Converter arbeitet mit Typen, die im Speicher dieselbe Struktur haben. Es sieht aus wie eine Typklasse, aber tatsÀchlich konvertiert der GHC Typen zur Kompilierungszeit und es ist nicht möglich, Instanzen selbst zu definieren.
Mit der Funktion Data.Coerce.coerce können Sie Typen ohne Ressourcen konvertieren. Dazu benötigen wir jedoch Zugriff auf die Typkonstruktoren .

Vereinfachen Sie nun unsere Funktion:

 import Data.Semigroup import Data.Monoid import Control.Arrow import Data.Coerce aggregate :: [Integer] -> (Maybe Integer, Integer) aggregate = coerce . (foldMap (Just . Max &&& Sum)) -- coerce :: (Maybe (Max Integer), Sum Integer) -> (Maybe Integer, Integer) 

Wir haben die Routine vermieden, verschachtelte Typen abzurufen, ohne Ressourcen mit nur einer Funktion zu verschwenden.

Rollen verschachtelter Datentypen


Mit der Coerce- Funktion können wir die Konvertierung verschachtelter Typen erzwingen.
Aber ist es notwendig, diese Funktion so weit zu nutzen?

 -- already in Data.Ord -- Down a - reversed order newtype Down a = Down a deriving (Eq, Show) instance Ord a => Ord (Down a) where compare (Down x) (Down y) = y `compare` x import Data.List(sort) -- Sorted data Sorted a = Sorted [a] deriving (Show, Eq, Ord) fromList2Sorted :: Ord a => [a] -> Sorted a fromList2Sorted = Sorted . sort -- minimum: O(1) ! minView :: Sorted a -> Maybe a minView (Sorted []) = Nothing minView (Sorted (a : _)) = Just a 

Semantisch ist es absurd, von Sorted (Down a) nach Sorted a zu konvertieren.
Sie können jedoch versuchen:

 ghci> let h = fromList2Sorted [1,2,3] :: Sorted Int ghci> let hDown = fromList2Sorted $ fmap Down [1,2,3] :: Sorted (Down Int) ghci> minView h Just (Down 1) ghci> minView (coerce h :: Sorted (Down Int)) Just (Down 1) ghci> minView hDown Just (Down 3) 

Alles wÀre in Ordnung, aber die richtige Antwort ist Just (Down 3) .
Um falsches Verhalten auszuschließen, wurden nĂ€mlich Typrollen eingefĂŒhrt.

 {-# LANGUAGE RoleAnnotations #-} type role Sorted nominal 

Versuchen wir es jetzt:

 ghci> minView (coerce h :: Sorted (Down Int)) error: Couldn't match type 'Int' with 'Down Int' arising from a use of 'coerce' 

Deutlich besser!

Insgesamt gibt es 3 Rollen ( Typ Rolle ):

  • reprĂ€sentativ - Ă€quivalent bei gleicher Darstellung
  • nominal - muss genau den gleichen Typ haben
  • Phantom - unabhĂ€ngig von echten Inhalten. Entspricht allem

In den meisten FĂ€llen ist der Compiler intelligent genug, um die Rolle des Typs aufzudecken, aber es kann geholfen werden.

Verfeinertes Ableiten des Ableitungsverhaltens


Dank der Erweiterung der DerivingVia- Sprache hat sich die Verteilungsrolle von newtype verbessert.

Beginnend mit GHC 8.6, das kĂŒrzlich veröffentlicht wurde, ist diese neue Erweiterung erschienen.

 {-# LANGUAGE DerivingVia #-} newtype Id = MkId Word deriving (Semigroup, Monoid) via Max Word 

Wie Sie sehen können, wird das Typverhalten aufgrund der Verfeinerung der Ausgabe automatisch abgeleitet.
DerivingVia kann auf jeden Typ angewendet werden, der Coercible unterstĂŒtzt und vor allem - ganz ohne Ressourcenverbrauch!

DarĂŒber hinaus kann DerivingVia nicht nur auf neue Typen angewendet werden , sondern auch auf alle isomorphen Typen, wenn sie Generika- Generika und Zwangskonvertierung mit Zwang unterstĂŒtzen.

Schlussfolgerungen


Types newtype ist eine leistungsstarke Kraft, die Code erheblich vereinfacht und verbessert, Routine eliminiert und den Ressourcenverbrauch reduziert.

OriginalĂŒbersetzung : Die Großmacht der Newtypes (Hiromi Ishii)

PS Ich denke, nach diesem Artikel, der vor mehr als einem Jahr [nicht von meinem] Artikel veröffentlicht wurde, wird die Magie des Newtype in Haskell ĂŒber neue Typen etwas klarer!

Source: https://habr.com/ru/post/de425405/


All Articles