Frage

Was ich tue: Ich schreibe einen kleinen Dolmetscher-System, das eine Datei analysieren kann, schalten Sie ihn in eine Folge von Operationen, und dann füttern Tausende von Datensätzen in dieser Sequenz zu extrahieren einige Endwert von jedem. Ein kompilierten Interpretierer besteht aus einer Liste von Funktionen, die reinen zwei Argumente: ein Datensatz, und einer Ausführungskontext. Jede Funktion gibt den modifizierten Ausführungskontext:

  

type ('data, 'context) interpreter = ('data -> 'context -> 'context) list

Der Compiler ist im Wesentlichen ein tokenizer mit einem letzten token-to-Befehlszuordnungsschritt, der verwendet eine Karte Beschreibung wie folgt definiert:

  

type ('data, 'context) map = (string * ('data -> 'context -> 'context)) list

Typische Interpreter Nutzung sieht wie folgt aus:

let pocket_calc = 
  let map = [ "add", (fun d c -> c # add d) ;
              "sub", (fun d c -> c # sub d) ;
              "mul", (fun d c -> c # mul d) ]
  in 
  Interpreter.parse map "path/to/file.txt"

let new_context = Interpreter.run pocket_calc data old_context

Das Problem: Ich würde meine pocket_calc Dolmetscher mit jeder Klasse arbeiten, dass Stützen add, sub und mul Methoden und der entsprechende data Typ (könnte ganze Zahlen für eine Kontextklasse sein und Fließ- Punktzahlen für die anderen).

Allerdings pocket_calc als ein Wert definiert ist und nicht eine Funktion, so das Typsystem macht nicht seine Art generic: das erste Mal, es verwendet wird, werden die 'data und 'context Typen auf die Arten von was auch immer Daten und Kontext gebunden ich zum ersten Mal bieten, und der Dolmetscher wird für immer unvereinbar mit anderen Daten und Kontexttypen.

Eine tragfähige Lösung für eta-erweitern die Definition des Dolmetschers ist seine Art Parameter erlauben generisch zu sein:

let pocket_calc data context = 
  let map = [ "add", (fun d c -> c # add d) ;
              "sub", (fun d c -> c # sub d) ;
              "mul", (fun d c -> c # mul d) ]
  in 
  let interpreter = Interpreter.parse map "path/to/file.txt" in
  Interpreter.run interpreter data context

Allerdings ist diese Lösung nicht akzeptabel aus mehreren Gründen:

  • Es neu kompiliert den Interpreter jedes Mal, es heißt, die Leistung erheblich verschlechtert. Auch der Abbildungsschritt führt zu einer spürbaren Verlangsamung (eine Token-Liste in einen Interpreter mit der Kartenliste Drehen).

  • Ihr Design stützt sich auf alle Dolmetscher bei der Initialisierung geladen werden, da der Compiler Warnungen, wenn ein Token in der geladenen Datei, keine Zeile in der Kartenliste übereinstimmen und ich möchte alle diese Warnungen sehen, wenn die Software startet (nicht, wenn einzelne Dolmetscher sind schließlich laufen).

  • Ich möchte manchmal einen bestimmten Kartenliste in mehrere Dolmetscher zur Wiederverwendung, ob auf eigene oder durch das Voranstellen zusätzliche Anweisungen (zum Beispiel "div").

Die Fragen: ist es eine Möglichkeit, den Typ parametrischer andere als eta-Expansion zu machen? Vielleicht ein cleverer Trick beteiligt Modul Signaturen oder Vererbung? Wenn das nicht möglich ist, ist es eine Möglichkeit, die drei Punkte zu lindern ich oben erwähnt, um habe eta-Expansion eine akzeptable Lösung zu machen? Vielen Dank!

War es hilfreich?

Lösung

  

Eine tragfähige Lösung ist eta-erweitern die   Definition des Interpreters zu ermöglichen   seine Art Parameter sein generic:

 let pocket_calc data context = 
   let map = [ "add", (fun d c -> c # add d) ;
               "sub", (fun d c -> c # sub d) ;
               "mul", (fun d c -> c # mul d) ]
   in 
   let interpreter = Interpreter.parse map "path/to/file.txt" in
   Interpreter.run interpreter data context
  

Allerdings ist diese Lösung nicht akzeptabel   aus mehreren Gründen:

     
      
  • Sie neu kompiliert der Interpreter jedes Mal, es heißt, die   erheblich beeinträchtigt die Leistung.   Auch der Abbildungsschritt (Drehen eines Tokens   Liste in einen Dolmetscher mit Hilfe der Karte   Liste) führt zu einer spürbaren Verlangsamung.
  •   

Es rekompiliert die jedes Mal, Dolmetscher, weil Sie es falsch machen. Die richtige Form mehr so ??etwas wie diese (und technisch, wenn die teilweise Interpretation von Interpreter.run zu interpreter einigen Berechnungen tun kann, sollten Sie es auch von den fun ausziehen).

 let pocket_calc = 
   let map = [ "add", (fun d c -> c # add d) ;
               "sub", (fun d c -> c # sub d) ;
               "mul", (fun d c -> c # mul d) ]
   in 
   let interpreter = Interpreter.parse map "path/to/file.txt" in
   fun data context -> Interpreter.run interpreter data context

Andere Tipps

Ich denke, das Problem liegt in einem Mangel an Polymorphismus in Ihrem Betrieb, die Sie mögen, dass einen geschlossenen Parametertyp haben (Arbeiten für alle Daten, die die folgenden arithmetische Primitiven unterstützt) stattdessen einen Typparameter mit einem festen Datentyp darstellt. Allerdings ist es ein bisschen schwierig, sicherzustellen, es ist genau das, weil Ihr Code nicht in sich geschlossen genug ist, um es zu testen.

Unter der Annahme der gegebenen Art für Primitive:

type 'a primitives = <
  add : 'a -> 'a;
  mul : 'a -> 'a; 
  sub : 'a -> 'a;
>

Sie können die ersten Ordnung Polymorphismus von Strukturen und Objekte zur Verfügung gestellt verwenden:

type op = { op : 'a . 'a -> 'a primitives -> 'a }

let map = [ "add", { op = fun d c -> c # add d } ;
            "sub", { op = fun d c -> c # sub d } ;
            "mul", { op = fun d c -> c # mul d } ];;

Sie erhalten wieder die folgenden Daten-agnostischen Typ:

 val map : (string * op) list

Edit: in Bezug auf Ihre Kommentare über verschiedene Betriebsarten, ich bin nicht sicher, welches Maß an Flexibilität, die Sie wünschen. Ich glaube nicht, Sie Operationen über verschiedene Primitive in der gleichen Liste mischen könnten, und nach wie vor von den Spezifitäten jeder profitieren: am besten, können Sie nur einen „Betrieb über Add / sub / mul“ in einen „Betrieb über Add-Transformation / sub / mul / div“(wie wir sind kontra in der Primitive Art), aber sicher nicht viel.

Auf einer pragmatischen Ebene, es ist wahr, dass mit diesem Design, benötigen Sie einen anderen „Betrieb“ Typen für jeden Typen Primitiven. Sie könnte leicht, jedoch baut einen Funktor von dem Primitive Art parametrisiert und Rückführen des Betriebstypen.

Ich weiß nicht, wie man eine direkte Untertypisierungsbeziehung zwischen den verschiedenen Grundtypen aussetzen würde. Das Problem ist, dass dies eine Untertypisierungsbeziehung auf der Funktors Ebene benötigen würde, was ich nicht glaube, wir in Caml haben. Sie könnte jedoch eine einfachere Form von explizitem Subtyping mit (statt a :> b von Gießen, eine Funktion a -> b verwenden), baut zweiten Funktors, kontra, dass eine Karte von einem primitiven Typ zu den andere gegeben, würde eine Karte von einer Operation bauen geben Sie auf der anderen Seite.

Es ist durchaus möglich, dass mit einer anderen und geschickten Darstellung des Typs entwickelt, eine viel einfachere Lösung möglich ist. First-Class-Module von 3.12 könnte auch in Spiel kommen, aber sie neigen dazu, für erstklassige existentielle Typen nützlich zu sein, während hier rhater wir Universaltypen verwenden.

interpretierender Aufwand und Betrieb Verdinglichungen

Neben dem lokalen Typisierung Problem, ich bin nicht sicher, dass Sie den richtigen Weg gehen. Sie versuchen, interpretierenden Aufwand durch Gebäude zu beseitigen „vor der Zeit“ (vor den Operationen verwendet wird), einen Verschluss eine in-Sprache Darstellung Ihres Betriebes entspricht.

Nach meiner Erfahrung ist dieser Ansatz nicht allgemein von interpretierende Aufwand loszuwerden, es bewegt sich eher es auf eine andere Ebene. Wenn Sie Ihre Verschlüsse naiverweise erstellen, werden Sie die Parsen Steuerfluss haben an der Verschlussschicht wiedergegeben: der Verschluß andere Verschlüsse rufen usw. als Parsing-Code den Eingang „interpretiert“, wenn die Schließung zu schaffen. Sie eliminiert die Kosten für die Analyse, aber die möglicherweise suboptimal Ablauf der Steuerung ist immer noch die gleichen. Additionnaly, Verschlüsse sind in der Regel ein Schmerz zu sein, direkt zu manipulieren. Sie sehr vorsichtig Vergleichsoperationen zum Beispiel sein müssen, Serialisierung, usw.

Ich glaube, Sie auf langer Sicht in einem Zwischen interessiert sein „verdinglicht“ Sprache darstellt, Ihre Operationen: ein einfachen algebraischen Datentyp für arithmetische Operationen, die Sie von Ihrer Textdarstellung aufbauen würden. Sie können nach wie vor versuchen, Schließungen „vor der Zeit“ zu bauen, von ihm, obwohl ich nicht sicher, ob die Leistungen sind viel besser, als es direkt zu interpretieren, wenn die In-Memory-Darstellung anständig. Darüber hinaus wird es viel einfacher sein, in Intermediär-Analysatoren / Transformatoren zu stopfen Ihren Betrieb zu optimieren, zum Beispiel von einem „assoziativen Binäroperationen“ -Modell zu einem „n-stufigen Operationen“ -Modell geht, die effizienten ausgewertet werden kann.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top