Kurzlebige Memoisierung in Haskell?

8

In einer objektorientierten Sprache, wenn ich die Ergebnisse einer Funktion für eine bekannte Lebensdauer cache / memoisieren möchte, folge ich im Allgemeinen diesem Muster:

  1. Erstellen Sie eine neue Klasse
  2. Fügen Sie der Klasse einen Datenmember und eine Methode für jedes Funktionsergebnis hinzu, das ich cachen möchte
  3. Implementieren Sie die Methode, um zuerst zu überprüfen, ob das Ergebnis im Datenelement gespeichert wurde. Wenn ja, gib diesen Wert zurück; Andernfalls rufen Sie die Funktion (mit den entsprechenden Argumenten) auf und speichern das zurückgegebene Ergebnis im Datenelement.
  4. Objekte dieser Klasse werden mit Werten initialisiert, die für die verschiedenen Funktionsaufrufe benötigt werden.

Dieser objektbasierte Ansatz ist dem hier beschriebenen funktionsbasierten Memoisierungsmuster sehr ähnlich: Ссылка

Der Hauptvorteil dieses Ansatzes besteht darin, dass die Ergebnisse nur für die Lebensdauer des Cache-Objekts beibehalten werden. Ein häufiger Anwendungsfall ist die Verarbeitung einer Liste von Arbeitselementen. Für jedes Arbeitselement erstellt man das Cache-Objekt für dieses Element, verarbeitet das Arbeitselement mit diesem Cache-Objekt und verwirft dann das Arbeitselement und das Cache-Objekt, bevor mit dem nächsten Arbeitselement fortgefahren wird.

Was sind gute Möglichkeiten, kurzlebige Memoization in Haskell zu implementieren? Und hängt die Antwort davon ab, ob die zu cachenden Funktionen rein sind oder IO beinhalten?

Nur um es noch einmal zu wiederholen - es wäre schön, Lösungen für Funktionen zu sehen, die IO betreffen.

    
ErikR 24.02.2012, 20:46
quelle

4 Antworten

12

Lasst uns Luke Palmers Memo-Bibliothek verwenden: Daten. MemoCombinators

%Vor%

Ich werde die Dinge etwas anders definieren als seine Bibliothek, aber es ist im Grunde dasselbe (und außerdem kompatibel). Ein "memoizable" Ding nimmt sich selbst als Eingabe und produziert das "echte" Ding.

%Vor%

Ein "Memoizer" übernimmt eine Funktion und erzeugt die Memo-Version davon.

%Vor%

Lassen Sie uns eine kleine Funktion schreiben, um diese beiden Dinge zusammenzufügen. Bei einer Memoizable -Funktion und einer Memoizer wollen wir die resultierende Memo-Funktion.

%Vor%

Dies ist ein wenig Magie mit dem Fixpunkt-Kombinator ( fix ). Vergiss das nicht; Sie können es googeln, wenn Sie interessiert sind.

Lassen Sie uns eine Memoizable Version des klassischen fib Beispiels schreiben:

%Vor%

Die Verwendung einer self Konvention macht den Code einfach. Denken Sie daran, self ist die erwartete Version dieser Funktion, daher sollten rekursive Aufrufe auf self stehen. Jetzt feuern ghci.

%Vor%

Nun, das Coole an runMemo ist, dass Sie mehr als eine frisch gemouncte Version derselben Funktion erstellen können, und sie nicht Speicherbänke teilen. Das bedeutet, dass ich eine Funktion schreiben kann, die fib' lokal erstellt und verwendet, aber sobald fib' außerhalb des Gültigkeitsbereichs liegt (oder früher, abhängig von der Intelligenz des Compilers), kann Garbage Collected sein Es muss nicht auf oberster Ebene gespeichert werden . Dies kann oder kann nicht gut mit Memo-Techniken funktionieren, die auf unsafePerformIO angewiesen sind. Data.MemoCombinators verwendet eine reine, faule Trie, die perfekt zu runMemo passt. Anstatt ein Objekt zu erstellen, das im Wesentlichen zu einem Memo-Manager wird, können Sie einfach Memo-Funktionen auf Anforderung erstellen. Wenn Ihre Funktion rekursiv ist, muss sie als Memoizable geschrieben werden. Die gute Nachricht ist, dass Sie beliebig Memoizer anschließen können. Sie könnten sogar verwenden:

%Vor%     
Dan Burton 24.02.2012, 23:12
quelle
4

Die Lazy-Haskell-Programmierung ist gewissermaßen das Paradigma der Memoisierung. Was auch immer du in einer imperativen Sprache machst, ist in Haskell möglich, entweder mit IO-Monade, der ST-Monade, Monaden-Transformatoren, Pfeilen oder du nennst was.

Das einzige Problem ist, dass diese Abstraktionsgeräte viel komplizierter sind als das Imperativ, das Sie erwähnten, und sie brauchen eine ziemlich tiefe Umverdrahtung.

    
dsign 24.02.2012 20:53
quelle
2

Ich glaube, die obigen Antworten sind komplexer als notwendig, obwohl sie tragbarer sind als das, was ich beschreiben werde.

Wie ich es verstehe, gibt es in ghc eine Regel, nach der jeder Wert genau einmal berechnet wird, wenn ein Lambda-Ausdruck eingegeben wird. So können Sie genau Ihr kurzlebiges Memo-Objekt wie folgt erstellen:

%Vor%

Was macht das? Es gruppiert alle Elemente in Data.Vector t , die übergeben wurden, als zweites Argument vec gemäß der Ganzzahl, die durch das erste Argument idx berechnet wurde, wobei ihre Gruppierung als Data.Vector [t] beibehalten wird. Sie gibt eine Funktion vom Typ Int -> [t] zurück, die diese Gruppierung anhand dieses vorberechneten Indexwerts nachschlägt.

Unser Compiler ghc hat versprochen, dass tbl nur einmal angegeben wird, wenn wir indexerVector aufrufen. Wir können daher den von \e -> tbl ! e zurückgegebenen Lambda-Ausdruck indexVector einem anderen Wert zuweisen, den wir wiederholt verwenden können, ohne befürchten zu müssen, dass tbl jemals neu berechnet wird. Sie können dies überprüfen, indem Sie trace in tbl einfügen.

Kurz gesagt, Ihr Caching-Objekt ist genau dieser Lambda-Ausdruck.

Ich habe festgestellt, dass fast alles, was Sie mit einem Kurzzeitobjekt erreichen können, besser erreicht werden kann, indem Sie einen Lambda-Ausdruck wie diesen zurückgeben.

    
Jeff Burdges 06.04.2012 20:34
quelle
1

Sie können das gleiche Muster auch in Haskell verwenden. Die Lazy-Auswertung prüft, ob der Wert bereits ausgewertet wurde. Es wurde bereits mehrfach erwähnt, aber Codebeispiel könnte nützlich sein. Im Beispiel unten wird memoedValue nur einmal berechnet, wenn es angefordert wird.

%Vor%

Noch besser: Sie können Werte memoisieren, die von anderen memoisierten Werten abhängen. Sie sollten Abhängigkeiten vermeiden. Sie können zur Nichtbeendigung führen.

%Vor%     
Shimuuar 25.02.2012 19:19
quelle

Tags und Links