2017-08-30 3 views
5

Mit GHC Version 8.0.2 das folgende Programm:Zwei Anrufe Funktion aber nur eine Spur angezeigt

import Debug.Trace 

f=trace("f was called")$(+1) 

main = do 
    print $ f 1 
    print $ f 2 

Ausgänge:

f was called 
2 
3 

Ist es das erwartete Verhalten? Wenn ja, warum? Ich erwartete, dass die Zeichenkette f was called zweimal gedruckt wird, eine vor 2 und eine vor 3.

gleiche Ergebnis auf TIO: Try it online!

EDIT Aber dieses Programm:

import Debug.Trace 

f n=trace("f was called:"++show n)$n+1 

main = do 
    print $ f 1 
    print $ f 2 

Ausgänge:

f was called:1 
2 
f was called:2 
3 

Try it online!

Ich vermute, dass diese Verhaltensweisen etwas mit Faulheit zu tun haben, aber meine Fragen bleiben: Ist dies das erwartete Verhalten und wenn ja, warum?

Hackage behauptet das:

Die Trace-Funktion gibt die Verfolgungsnachricht als erstes Argument angegeben , bevor das zweite Argument als Ergebnis zurück.

Ich sehe es nicht im ersten Beispiel.

EDIT 2 Drittes Beispiel basierend auf @amalloy Kommentare:

import Debug.Trace 

f n=trace "f was called"$n+1 

main = do 
    print $ f 1 
    print $ f 2 

Ausgänge:

f was called 
2 
f was called 
3 
+0

Beachten Sie, dass "trace" den Typ "String -> a -> a" hat. Der Typ, der für "a" im Fall "f =" substituiert wird, unterscheidet sich von dem im Fall "f n =". Das sollte auch dazu beitragen, das unterschiedliche Verhalten zu erklären. –

Antwort

2

Es ist in der Tat ein Ergebnis der Faulheit.

Faulheit bedeutet, dass das bloße Definieren eines Werts nicht bedeutet, dass es ausgewertet wird; Das wird nur passieren, wenn es für etwas benötigt wird. Wenn es nicht benötigt wird, tut der Code, der es tatsächlich produziert, nichts "tun". Wenn ein bestimmter Wert benötigt wird, wird der Code ausgeführt, aber nur das erste Mal, es wäre erforderlich; Wenn andere Verweise auf denselben Wert vorhanden sind und erneut verwendet werden, verwenden diese Verwendungen einfach den Wert, der beim ersten Mal erstellt wurde.

Sie müssen daran denken, dass Funktionen sind Werte in jeder Hinsicht des Begriffs; alles, was für gewöhnliche Werte gilt, gilt auch für Funktionen. Ihre Definition von f schreibt einfach einen Ausdruck für einen Wert, die Auswertung des Ausdrucks wird zurückgestellt, bis der Wert f tatsächlich benötigt wird, und da der doppelte Wert (Funktion) benötigt wird, wird der Ausdruck computes gespeichert und zum zweiten Mal wiederverwendet .

Ermöglicht es genauer aussehen:

f=trace("f was called")$(+1) 

Sie definieren einen Wert f mit einer einfachen Gleichung (keinen syntaktischen Zucker unter Verwendung für das Schreiben Argument auf der linken Seite der Gleichung oder die Bereitstellung Fälle über mehrere Gleichungen). Also können wir einfach die rechte Seite als einen einzelnen Ausdruck nehmen, der den Wert f definiert. Gerade definieren es tut nichts, er sitzt dort, bis Sie anrufen:

print $ f 1 

Jetzt drucken ihr Argument ausgewertet muss, so ist dies zwingt den Ausdruck f 1. Aber wir können f nicht auf 1 anwenden, ohne vorher f zu erzwingen. Also müssen wir herausfinden, welche Funktion der Ausdruck trace "f was called" $ (+1) auswertet. So trace wird tatsächlich aufgerufen, wird seine unsichere IO-Druck und f was called erscheint am Terminal, und dann trace gibt sein zweites Argument zurück: (+1).

So jetzt wissen wir, welche Funktion f ist: (+1). f wird nun eine direkte Referenz auf diese Funktion sein, ohne den ursprünglichen Code trace("f was called")$(+1) auszuwerten, wenn f erneut aufgerufen wird. Deshalb macht das zweite print nichts.

Dieser Fall ist ganz anders, obwohl es ähnlich aussehen könnte: durch Schreiben Argumente auf der linken Seite

f n=trace("f was called:"++show n)$n+1 

Hier stellen wir sind den syntaktischen Zucker unter Verwendung von Funktionen für die Definition.Lassen Sie uns desugar dass Notation Lambda deutlicher, was der tatsächliche Wert um zu sehen, zu f gebunden zu sein ist:

f = \n -> trace ("f was called:" ++ show n) $ n + 1 

Hier haben wir einen Funktionswert geschrieben haben direkt, eher als ein Ausdruck, der ausgewertet werden können, um zur Folge eine Funktion. Also, wenn f ausgewertet werden muss, bevor es auf 1 aufgerufen werden kann, ist der Wert von f diese ganze Funktion; Der trace Aufruf ist innerhalb die Funktion anstelle der Sache, die Ergebnis in einer Funktion aufgerufen wird. So wird trace nicht als Teil der Bewertung f aufgerufen, es wird als Teil der Auswertung der Anwendung f 1 aufgerufen. Wenn Sie das Ergebnis davon gespeichert haben (z. B. indem Sie let x = f 1 getan haben) und es dann mehrfach gedruckt haben, sehen Sie nur die eine Ablaufverfolgung. Aber wenn wir kommen f 2, trace Aufruf ist immer noch dort in der Funktion, die der Wert f ist, so wenn f heißt wieder so ist trace.

4

Ihre Spur druckt, wenn f definiert, nicht, wenn es aufgerufen wird. Wenn Sie die Spur passieren als Teil des Aufrufs wollen, sollten Sie sicherstellen, dass es nicht ausgewertet, bis ein Parameter empfangen wird:

f x = trace "f was called" $ x + 1 

Auch, wenn ich Ihre TIO betreibe ich nicht die Spur sehen erscheinen an alle. trace ist nicht wirklich eine zuverlässige Möglichkeit, Dinge zu drucken, weil es das IO-Modell betrügt, auf dem die Sprache aufgebaut ist. Die geringsten Änderungen in der Bewertungsreihenfolge können dies stören. Natürlich kann man sie zum Debuggen verwenden, aber wie auch dieses einfache Beispiel zeigt, ist es nicht garantiert, dass es viel hilft.

In Ihrem bearbeiten, geben Sie die Dokumentation von trace:

Die Trace-Funktion gibt die Verfolgungsnachricht als erstes Argument gegeben, bevor das zweite Argument als Ergebnis zurück.

Und tatsächlich passiert genau das in Ihrem Programm!Bei der Definition ,

muss ausgewertet werden. Zuerst wird "f wurde aufgerufen" gedruckt. Dann wird trace ausgewertet und (+ 1) zurückgegeben. Dies ist der Endwert des Ausdrucks trace, und daher ist (+ 1)f definiert als. Die trace ist verschwunden, siehe?

+0

Nicht wirklich beim Definieren: Wenn Sie es nicht nennen, werden Sie keine Spur sehen. In TIO erscheint es einmal in der "Debug" -Box (stderr?). Siehe meine Bearbeitung für ein anderes Beispiel. – jferard

+0

Nein, Sie müssen die Funktion nicht aufrufen, um dies zu drucken: Sie müssen es lediglich auswerten. Zum Beispiel gibt 'main = seq f $ return()' auch genau einmal aus. Das 'seq' ist notwendig, um zu erzwingen, dass' f' ausgewertet wird, obwohl es nicht verwendet wird. Wie bei den meisten Dingen in Haskell hat es keine Nebenwirkungen, wenn man es definiert, aber nicht benutzt. Aber die Nebenwirkungen auf dein 'f' sind in der Tat daran gebunden, es zu definieren, es nicht zu nennen; Sie müssen nur sicherstellen, dass die Nebenwirkungen seiner Definition erzwungen werden. – amalloy

+0

@ammaloy Ich habe die Bearbeitung nach dem Kommentar vorgenommen. Ok für "evaluate" vs "call". – jferard

Verwandte Themen