Ist ein übergeordneter parametrischer Polymorphismus nützlich?

16

Ich bin mir ziemlich sicher, dass jeder mit allgemeinen Methoden der Form vertraut ist:

T DoSomething<T>(T item)

Diese Funktion wird auch als parametrisch polymorph (PP) bezeichnet, insbesondere als PP mit Rang 1 .

Nehmen wir an, diese Methode kann mit einem Funktionsobjekt der Form dargestellt werden:

<T> : T -> T

Dies <T>bedeutet, dass ein Typparameter verwendet wird und T -> Tdass ein Typparameter verwendet wird Tund ein Wert desselben Typs zurückgegeben wird.

Dann wäre das Folgende eine Rang-2-PP-Funktion:

(<T> : T -> T) -> int 

Die Funktion akzeptiert selbst keine Typparameter, sondern eine Funktion, die einen Typparameter akzeptiert. Sie können dies iterativ fortsetzen, indem Sie die Verschachtelung immer tiefer machen und PP von immer höherem Rang erhalten.

Diese Funktion ist unter Programmiersprachen sehr selten. Sogar Haskell lässt dies standardmäßig nicht zu.

Ist es nützlich? Kann es Verhalten beschreiben, die sonst schwer zu beschreiben sind?

Was bedeutet es auch, dass etwas anregend ist ? (in diesem Kontext)

GregRos
quelle
1
Interessanterweise ist TypeScript eine der Hauptsprachen mit vollständiger Unterstützung für PP. Das Folgende ist beispielsweise ein gültiger TypeScript-Code:let sdff = (g : (f : <T> (e : T) => void) => void) => {}
GregRos

Antworten:

11

Im Allgemeinen verwenden Sie einen höherrangigen Polymorphismus, wenn der Angerufene den Wert eines Typparameters anstelle des Aufrufers auswählen soll . Beispielsweise:

f :: (forall a. Show a => a -> Int) -> (Int, Int)
f g = (g "one", g 2)

Jede Funktion g, die ich an diese übergebe, fmuss in der Lage sein, mir Inteinen Wert von einem Typ zu geben, von dem das einzige, was güber diesen Typ weiß, ist, dass er eine Instanz von hat Show. Das sind also koschere:

f (length . show)
f (const 42)

Das sind aber nicht:

f length
f succ

Eine besonders nützliche Anwendung besteht darin, das Scoping von Typen zu verwenden, um das Scoping von Werten zu erzwingen . Angenommen, wir haben ein Objekt vom Typ Action<T>, das eine Aktion darstellt, die wir ausführen können, um ein Ergebnis vom Typ zu erzeugen T, z. B. eine Zukunft oder einen Rückruf.

T runAction<T>(Action<T>)

runAction :: forall a. Action a -> a

Angenommen, wir haben auch eine Action, die Resource<T>Objekte zuordnen kann:

Action<Resource<T>> newResource<T>(T)

newResource :: forall a. a -> Action (Resource a)

Wir möchten sicherstellen, dass diese Ressourcen nur in dem Bereich verwendet werden, in dem Actionsie erstellt wurden, und nicht zwischen verschiedenen Aktionen oder verschiedenen Läufen derselben Aktion aufgeteilt werden, damit Aktionen deterministisch und wiederholbar sind.

Um dies zu erreichen, können wir höherrangige Typen verwenden, indem wir Sden Resourceund Action-Typen einen Parameter hinzufügen , der völlig abstrakt ist - er repräsentiert den „Geltungsbereich“ der Action. Jetzt sind unsere Unterschriften:

T run<T>(<S> Action<S, T>)
Action<S, Resource<S, T>> newResource<T>(T)

runAction :: forall a. (forall s. Action s a) -> a
newResource :: forall s a. a -> Action s (Resource s a)

Nun , wenn wir geben runActionein Action<S, T>, sind wir sicher sein , dass , weil die „scope“ Parameter Svollständig polymorph sind, ist es nicht den Körper entweichen kann runAction-so jeden Wert eines Typs , dass Anwendungen Swie Resource<S, int>ebenfalls nicht entziehen können!

(In Haskell ist dies als die STMonade bekannt, wo runActionheißt runST, Resourceheißt STRefund newResourceheißt newSTRef.)

Jon Purdy
quelle
Die STMonade ist ein sehr interessantes Beispiel. Können Sie noch einige Beispiele nennen, wann ein höherrangiger Polymorphismus sinnvoll wäre?
GregRos
@ GregRos: Es ist auch praktisch mit existentials. In Haxl hatten wir ein existenzielles Like data Fetch d = forall a. Fetch (d a) (MVar a), das ein Paar aus einer Anforderung an eine Datenquelle dund einem Slot ist, in dem das Ergebnis gespeichert werden kann. Das Ergebnis und der Slot müssen übereinstimmende Typen haben, dieser Typ ist jedoch ausgeblendet, sodass Sie eine heterogene Liste von Anforderungen an dieselbe Datenquelle haben können. Jetzt können Sie höherrangigen Polymorphismus verwenden , um eine Funktion zu schreiben , die alle Anforderungen holt, da eine Funktion , die man holt: fetch :: (forall a. d a -> IO a) -> [Fetch d] -> IO ().
Jon Purdy
8

Polymorphismus mit höherem Rang ist äußerst nützlich. In System F (der Kernsprache typisierter FP-Sprachen, mit der Sie vertraut sind) ist dies wesentlich, um "getippte Kirchenkodierungen" zuzulassen, wie dies in System F der Fall ist. Ohne diese ist System F völlig unbrauchbar.

In System F definieren wir Zahlen als

Nat = forall c. (c -> c) -> c -> c

Zusatz hat den Typ

plus : Nat -> Nat -> Nat
plus l r = Λ t. λ (s : t -> t). λ (z : t). l s (r s z)

Dies ist ein Typ mit höherem Rang (der forall c.in diesen Pfeilen angezeigt wird).

Dies kommt auch an anderen Stellen vor. Wenn Sie beispielsweise angeben möchten, dass eine Berechnung eine ordnungsgemäße Art der Weitergabe darstellt (google "codensity haskell"), können Sie dies als korrigieren

type CPSed A = forall c. (A -> c) -> c

Selbst wenn in System F von einem unbewohnten Typ gesprochen wird, ist ein Polymorphismus mit höherem Rang erforderlich

type Void = forall a. a 

Das lange und kurze Schreiben einer Funktion in einem reinen Typensystem (System F, CoC) erfordert einen höherrangigen Polymorphismus, wenn wir mit interessanten Daten umgehen wollen.

Insbesondere in System F müssen diese Codierungen "anregend" sein. Dies bedeutet, dass a forall a.über absolut alle Typen quantifiziert . Dies schließt genau den Typ ein, den wir definieren. In forall a. adas akönnte tatsächlich stehen forall a. awieder! In Sprachen wie ML ist dies nicht der Fall, sie werden als "prädikativ" bezeichnet, da eine Typvariable nur über den Satz von Typen ohne Quantifizierer (sogenannte Monotypen) quantifiziert. Unsere Definition von pluserforderlichen impredicativity auch , weil wir instanziiert die cin l : Natsein Nat!

Abschließend möchte ich einen letzten Grund erwähnen, aus dem Sie sowohl Impredikativität als auch höherrangigen Polymorphismus wünschen, selbst in einer Sprache mit willkürlich rekursiven Typen (im Gegensatz zu System F). In Haskell gibt es eine Monade für Effekte, die als "State Thread Monad" bezeichnet wird. Die Idee ist, dass die Status-Thread-Monade Sie Dinge mutieren lässt, aber es erfordert, dass Ihr Ergebnis nicht von etwas Mutierbarem abhängt. Dies bedeutet, dass ST-Berechnungen beobachtbar rein sind. Um diese Anforderung durchzusetzen, verwenden wir einen höherrangigen Polymorphismus

runST :: forall a. (forall s. ST s a) -> a

Indem awir sicherstellen, dass dies außerhalb des Bereichs liegt, in dem wir es einführen s, wissen wir, dass es asich um einen wohlgeformten Typ handelt, der nicht auf etwas angewiesen ist s. Wir verwenden s, um alle veränderlichen Dinge in diesem bestimmten Status-Thread zu paramerisieren, damit wir wissen, dass dies aunabhängig von veränderlichen Dingen ist und daher nichts dem Umfang dieser STBerechnung entgeht ! Ein wunderbares Beispiel für die Verwendung von Typen, um schlecht geformte Programme auszuschließen.

Übrigens, wenn Sie etwas über Typentheorie lernen möchten, würde ich vorschlagen, in ein oder zwei gute Bücher zu investieren. Es ist schwer, dieses Zeug in Einzelteilen zu lernen. Ich würde eines von Pierces oder Harpers Büchern über PL-Theorie im Allgemeinen (und einige Elemente der Typentheorie) vorschlagen. Das Buch "Fortgeschrittene Themen in Typen und Programmiersprachen" behandelt auch ein gutes Maß an Typentheorie. Schließlich ist "Programmieren in der Typentheorie von Martin Lof" eine sehr gute Darstellung der umrissenen intensionalen Typentheorie von Martin Lof.

Daniel Gratzer
quelle
Vielen Dank für Ihre Empfehlungen. Ich werde sie nachschlagen. Das Thema ist wirklich interessant, und ich wünschte, einige fortgeschrittenere Typsystemkonzepte würden von mehr Programmiersprachen übernommen. Sie geben Ihnen viel mehr Ausdruckskraft.
GregRos