Ich schreibe ein Programm, das aus einer Liste von Dateien liest. Jede Datei enthält entweder eine Verknüpfung zur nächsten Datei oder markiert, dass es das Ende der Kette ist.
Da es für Haskell neu ist, schien es, als wäre die idiomatische Art, dies zu handhaben, eine faule Liste möglicher Dateien zu diesem Zweck, die ich habe.
%Vor%So weit, so gut. Das einzige Problem ist, dass, da getFirstFile und getNextFile beide Dateien öffnen müssen, ihre Ergebnisse in der IO-Monade liegen müssen. Dies gibt die modifizierte Form von
%Vor%Das Problem dabei ist, dass, da iterate eine unendliche Liste zurückgibt, die Sequenz zu einer Endlosschleife wird. Ich bin mir nicht sicher, wie ich von hier aus vorgehen soll. Gibt es eine faule Form der Sequenz, die nicht alle Listenelemente trifft? Sollte ich die Karte neu justieren und takeWhile innerhalb der IO-Monade für jedes Listenelement arbeiten? Oder muss ich den gesamten unendlichen Listenprozess löschen und eine rekursive Funktion schreiben, um die Liste manuell zu beenden?
Ein Schritt in die richtige Richtung
Was mich verwirrt, ist getNextFile
. Begib dich mit mir in eine vereinfachte Welt, in der es noch kein IO gibt. Der Typ ist Maybe DataFile -> Maybe DataFile
. Meiner Meinung nach sollte dies einfach DataFile -> Maybe DataFile
sein, und ich gehe davon aus, dass diese Anpassung möglich ist. Und das sieht wie ein guter Kandidat für unfoldr
aus. Das erste, was ich tun werde, ist meine eigene vereinfachte Version von unfoldr, die weniger allgemein aber einfacher zu verwenden ist.
Jetzt stimmt der Typ f :: a -> Maybe a
mit getNextFile :: DataFile -> Maybe DataFile
Schön, oder? unfoldr
ist sehr ähnlich wie iterate
, außer wenn es Nothing
erreicht, beendet es die Liste.
Jetzt haben wir ein Problem. %Code%. Wie können wir das Gleiche mit IO
tun, das dort hineingeworfen wird? Denken Sie nicht einmal an die Funktion, die nicht benannt werden soll. Wir brauchen einen aufgepeppten Unfold, um damit fertig zu werden. Glücklicherweise ist die Quelle für unvoldr verfügbar zu uns.
Was brauchen wir jetzt? Eine gesunde Dosis von IO
. IO
fast bringt uns den richtigen Typ, wird ihn diesmal aber nicht ganz schneiden.
Eine tatsächliche Lösung
%Vor%Es ist eine ziemlich direkte Umwandlung; Ich frage mich, ob es einen Kombinator gibt, der das Gleiche erreichen könnte.
Fun fact: Wir können nun liftM2 unfoldr
Lassen Sie uns noch einmal ein vereinfachtes unfoldr f b = runIdentity $ unfoldrM (return . f) b
definieren, wir müssen nur ein myUnfoldrM
darin einstreuen:
Und jetzt sind wir fertig, genau wie vorher.
%Vor% Übrigens habe ich all diese Elemente mit liftM
und den Typ-Signaturen für data DataFile = NoClueWhatGoesHere
und getFirstFile
überprüft, wobei ihre Definitionen auf getNextFile
gesetzt wurden.
[edit] hat undefined
und myUnfoldr
geändert, um sich mehr wie myUnfoldrM
zu verhalten, einschließlich des Anfangswerts in der Ergebnisliste.
[edit] Zusätzliche Einblicke zu entfaltet:
Wenn es Ihnen schwer fällt, sich zu entfalten, ist die Collatz-Sequenz möglicherweise eines der einfachsten Beispiele.
%Vor% Denken Sie daran, iterate
ist eine vereinfachte Entfaltung für die Fälle, in denen "next seed" und "current output value" identisch sind, wie es bei collatz der Fall ist. Dieses Verhalten sollte bei der einfachen Definition von myUnfoldr
in myUnfoldr
und unfoldr
leicht zu erkennen sein.
Mehr, meist nicht verwandte Gedanken
Der Rest hat absolut nichts mit der Frage zu tun, aber ich konnte einfach nicht darüber nachdenken. Wir können tuplefy x = (x,x)
in myUnfoldr
definieren:
Vertraut? Wir können dieses Muster sogar abstrahieren:
%Vor% myUnfoldrM
sollte funktionieren, um jede Funktion der Form
sinkM
.
, da die Monad m => (a -> m b) -> a -> m c
in diesen Funktionen mit der Monad m
-Monad-Einschränkung von Identity
vereinheitlicht werden können. Allerdings ich nicht alles sehen , dass sinkM
tatsächlich nützlich wäre.
Wie Sie bemerkt haben, können IO-Ergebnisse nicht träge sein, so dass Sie (einfach) keine unendliche Liste mit IO erstellen können. Es gibt jedoch einen Ausweg in unsafeInterleaveIO
; Mit diesem können Sie etwas tun wie:
Es ist jedoch wichtig, hier vorsichtig zu sein - Sie haben die Ergebnisse von ioList
auf eine unvorhersehbare Zeit in der Zukunft verschoben. Es wird vielleicht überhaupt nie ausgeführt werden. Also sei sehr vorsichtig, wenn du Clever ™ bist.
Persönlich würde ich nur eine manuelle rekursive Funktion erstellen.
Faulheit und I / O sind eine knifflige Kombination. Die Verwendung von unsafeInterleaveIO
ist eine Möglichkeit, faule Listen in der IO-Monade zu erzeugen (und dies ist die Technik, die von den Standard getContents
, readFile
und Freunden verwendet wird). So praktisch dies jedoch ist, macht es reinen Code möglichen E / A-Fehlern zugänglich und macht die Freigabe von Ressourcen (wie Dateihandles) nicht deterministisch. Aus diesem Grund verwenden die meisten "ernsthaften" Haskell-Anwendungen (insbesondere diejenigen, die sich mit Effizienz befassen) heutzutage sogenannte Enumeratoren und Iteraten für das Streaming von I / O. Eine Bibliothek in Hackage, die dieses Konzept implementiert, ist enumerator
.
Sie sind wahrscheinlich in der Lage, lazy I / O in Ihrer Anwendung zu verwenden, aber ich dachte, ich würde dies immer noch als Beispiel für eine andere Art der Annäherung an diese Art von Problemen geben. Sie können weitere ausführliche Tutorials über iteratees hier und hier .
Beispielsweise könnte Ihr Stream von DataFiles als Enumerator wie folgt implementiert werden:
%Vor%Tags und Links haskell io infinite-loop monads lazy-evaluation