Mir wurde gesagt, dass man in der funktionalen Programmierung keine Ausnahmen werfen und / oder beobachten soll. Stattdessen sollte eine fehlerhafte Berechnung als unterer Wert gewertet werden. In Python (oder anderen Sprachen, die die funktionale Programmierung nicht vollständig unterstützen) kann man zurückgeben None
(oder eine andere Alternative, die als unterster Wert behandelt wird, obwohl None
sie der Definition nicht genau entspricht), wenn etwas schief geht, um "rein zu bleiben", aber zu tun man muss also zuerst einen Fehler beobachten, d.h.
def fn(*args):
try:
... do something
except SomeException:
return None
Verstößt dies gegen die Reinheit? Und wenn ja, heißt das, dass es unmöglich ist, Fehler nur in Python zu behandeln?
Aktualisieren
In seinem Kommentar erinnerte mich Eric Lippert an einen anderen Weg, um Ausnahmen in FP zu behandeln. Obwohl ich das in Python noch nie in der Praxis gesehen habe, habe ich vor einem Jahr, als ich FP studierte, damit gespielt. Hier gibt jede optional
-dekorierte Funktion Optional
Werte für normale Ausgaben sowie für eine angegebene Liste von Ausnahmen zurück , die leer sein können (nicht spezifizierte Ausnahmen können die Ausführung immer noch beenden). Carry
Erstellt eine verzögerte Auswertung, bei der jeder Schritt (verzögerter Funktionsaufruf) entweder eine nicht leere Optional
Ausgabe des vorherigen Schritts erhält und diese einfach weiterleitet oder sich auf andere Weise selbst bewertet, indem er eine neue weiterleitet Optional
. Am Ende ist der Endwert entweder normal oder Empty
. Hier ist der try/except
Block hinter einem Dekorator verborgen, sodass die angegebenen Ausnahmen als Teil der Rückgabetypsignatur betrachtet werden können.
class Empty:
def __repr__(self):
return "Empty"
class Optional:
def __init__(self, value=Empty):
self._value = value
@property
def value(self):
return Empty if self.isempty else self._value
@property
def isempty(self):
return isinstance(self._value, BaseException) or self._value is Empty
def __bool__(self):
raise TypeError("Optional has no boolean value")
def optional(*exception_types):
def build_wrapper(func):
def wrapper(*args, **kwargs):
try:
return Optional(func(*args, **kwargs))
except exception_types as e:
return Optional(e)
wrapper.__isoptional__ = True
return wrapper
return build_wrapper
class Carry:
"""
>>> from functools import partial
>>> @optional(ArithmeticError)
... def rdiv(a, b):
... return b // a
>>> (Carry() >> (rdiv, 0) >> (rdiv, 0) >> partial(rdiv, 1))(1)
1
>>> (Carry() >> (rdiv, 0) >> (rdiv, 1))(1)
1
>>> (Carry() >> rdiv >> rdiv)(0, 1) is Empty
True
"""
def __init__(self, steps=None):
self._steps = tuple(steps) if steps is not None else ()
def _add_step(self, step):
fn, *step_args = step if isinstance(step, Sequence) else (step, )
return type(self)(steps=self._steps + ((fn, step_args), ))
def __rshift__(self, step) -> "Carry":
return self._add_step(step)
def _evaluate(self, *args) -> Optional:
def caller(carried: Optional, step):
fn, step_args = step
return fn(*(*step_args, *args)) if carried.isempty else carried
return reduce(caller, self._steps, Optional())
def __call__(self, *args):
return self._evaluate(*args).value
quelle
Antworten:
Klären wir zunächst einige Missverständnisse auf. Es gibt keinen "unteren Wert". Der unterste Typ ist als ein Typ definiert, der ein Untertyp jedes anderen Typs in der Sprache ist. Hieraus kann man (zumindest in jedem interessanten Typensystem) beweisen, dass der unterste Typ keine Werte hat - er ist leer . Es gibt also keinen Bodenwert.
Warum ist der Bodentyp nützlich? Nun, da wir wissen, dass es leer ist, lassen Sie uns ein paar Rückschlüsse auf das Programmverhalten ziehen. Wenn wir zum Beispiel die Funktion haben:
Wir wissen, dass
do_thing
das niemals zurückkehren kann, da es einen Wert vom Typ zurückgeben müssteBottom
. Es gibt also nur zwei Möglichkeiten:do_thing
hört nicht aufdo_thing
löst eine Ausnahme aus (in Sprachen mit einem Ausnahmemechanismus)Beachten Sie, dass ich einen Typ erstellt habe,
Bottom
der in der Python-Sprache nicht vorhanden ist.None
ist eine falsche Bezeichnung; Es ist tatsächlich der Einheitenwert , der einzige Wert des Einheitentyps , derNoneType
in Python aufgerufen wirdtype(None)
.Ein weiteres Missverständnis ist, dass funktionale Sprachen keine Ausnahme haben. Das ist auch nicht wahr. SML hat zum Beispiel einen sehr schönen Ausnahmemechanismus. Ausnahmen werden in SML jedoch viel sparsamer verwendet als in z. B. Python. Wie Sie gesagt haben, besteht die übliche Möglichkeit, einen Fehler in funktionalen Sprachen anzuzeigen, darin, einen
Option
Typ zurückzugeben. Zum Beispiel würden wir eine sichere Teilungsfunktion wie folgt erstellen:Leider ist dies kein praktikabler Ansatz, da Python eigentlich keine Summentypen hat. Sie könnten zurückkehren
None
als ein Arme-Leute-Option Typen , um anzuzeigen , Ausfall, aber das ist wirklich besser nicht als RückkehrNull
. Es gibt keine Typensicherheit.Daher würde ich raten, in diesem Fall die Konventionen der Sprache zu befolgen. Python verwendet Ausnahmen idiomatisch, um den Kontrollfluss zu handhaben (was schlechtes Design ist, IMO, aber dennoch Standard). Wenn Sie also nicht nur mit selbst geschriebenem Code arbeiten, empfehle ich, die Standardpraxis zu befolgen. Ob dies "rein" ist oder nicht, spielt keine Rolle.
quelle
None
der der Definition nicht entsprach. Trotzdem danke, dass du mich korrigiert hast. Denken Sie nicht, dass es mit Pythons Prinzipien in Ordnung ist, eine Ausnahme nur zu verwenden, um die Ausführung vollständig zu stoppen oder einen optionalen Wert zurückzugeben? Ich meine, warum ist es schlecht, Ausnahmen für eine komplizierte Kontrolle zu unterbinden?try/except/finally
wie eine andere Alternative zu verwendenif/else
, dhtry: var = expession1; except ...: var = expression 2; except ...: var = expression 3...
obwohl dies in jeder imperativen Sprache üblich ist (übrigens rate ich auch dringend davon ab,if/else
Blöcke dafür zu verwenden). Meinst du, dass ich unvernünftig bin und solche Muster zulassen sollte, da "das ist Python"?try... catch...
sollte nicht für den Kontrollfluss verwendet werden. Aus irgendeinem Grund hat sich die Python-Community dazu entschlossen, Dinge zu tun. Zum Beispiel würde diesafe_div
Funktion , die ich oben geschrieben habe, normalerweise geschriebentry: result = num / div: except ArithmeticError: result = None
. Wenn Sie ihnen also allgemeine Prinzipien des Software-Engineerings beibringen, sollten Sie davon definitiv abraten.if ... else...
ist auch ein Codegeruch, aber das ist zu lang, um hier darauf einzugehen.Warum untersuchen wir nicht, wie eine reine Funktion aussieht, da in den letzten Tagen so viel Interesse an Reinheit bestand?
Eine reine Funktion:
Ist referenziell transparent; Das heißt, für eine bestimmte Eingabe wird immer dieselbe Ausgabe erstellt.
Erzeugt keine Nebenwirkungen; Es ändert weder die Eingänge noch die Ausgänge oder irgendetwas anderes in seiner externen Umgebung. Es wird nur ein Rückgabewert erzeugt.
Also frag dich. Tut Ihre Funktion etwas anderes als eine Eingabe zu akzeptieren und eine Ausgabe zurückzugeben?
quelle
T + { Exception }
(wobeiT
es sich um den explizit deklarierten Rückgabetyp handelt), was problematisch ist. Sie können nicht wissen, ob eine Funktion eine Ausnahme auslöst, ohne sich den Quellcode anzusehen, was das Schreiben von Funktionen höherer Ordnung ebenfalls problematisch macht.map : (A->B)->List A ->List B
woA->B
Fehler auftreten können. Wenn wir erlauben, dass f eine Ausnahmemap f L
auslöst, wird etwas vom Typ zurückgegebenException + List<B>
. Wenn wir stattdessen zulassen, dass einoptional
Stiltyp zurückgegebenmap f L
wird, wird stattdessen List <Optional <B >> `zurückgegeben. Diese zweite Option fühlt sich für mich funktionaler an.Die Haskell-Semantik verwendet einen "unteren Wert", um die Bedeutung des Haskell-Codes zu analysieren. Es ist nicht etwas, das Sie wirklich direkt beim Programmieren von Haskell verwenden, und das Zurückkehren
None
ist überhaupt nicht dasselbe.Der unterste Wert ist der Wert, den die Haskell-Semantik jeder Berechnung zuweist, die nicht normal bewertet werden kann. Eine solche Möglichkeit, die eine Haskell-Berechnung bietet, besteht darin, eine Ausnahme auszulösen! Wenn Sie also versuchen, diesen Stil in Python zu verwenden, sollten Sie eigentlich wie gewohnt Ausnahmen auslösen.
Die Haskell-Semantik verwendet den unteren Wert, da Haskell faul ist. Sie können "Werte" manipulieren, die von Berechnungen zurückgegeben werden, die noch nicht ausgeführt wurden. Sie können sie an Funktionen übergeben, in Datenstrukturen speichern usw. Eine solche nicht ausgewertete Berechnung kann eine Ausnahme oder eine Endlosschleife auslösen. Wenn wir den Wert jedoch nie tatsächlich untersuchen müssen, wird die Berechnung dies niemals tunFühren Sie den Befehl aus, und stellen Sie fest, dass der Fehler aufgetreten ist. Unser Gesamtprogramm schafft es möglicherweise, eine genau definierte Aufgabe zu erledigen und zu beenden. Ohne zu erklären, was Haskell-Code bedeutet, indem das genaue Betriebsverhalten des Programms zur Laufzeit angegeben wird, deklarieren wir stattdessen, dass solche fehlerhaften Berechnungen den unteren Wert erzeugen, und erklären, wie sich dieser Wert verhält. Grundsätzlich gilt, dass jeder Ausdruck, der von allen Eigenschaften des unteren Werts (der nicht vorhanden ist) abhängen muss, auch den unteren Wert ergibt.
Um "rein" zu bleiben, müssen alle Möglichkeiten zur Generierung des Grundwerts als gleichwertig behandelt werden. Das schließt den "unteren Wert" ein, der eine Endlosschleife darstellt. Da es keine Möglichkeit gibt, zu wissen, dass einige Endlosschleifen tatsächlich unendlich sind (sie werden möglicherweise beendet, wenn Sie sie nur etwas länger ausführen), können Sie keine Eigenschaft mit einem unteren Wert untersuchen. Sie können nicht testen, ob etwas unten ist, können es nicht mit etwas anderem vergleichen, können es nicht in eine Zeichenfolge konvertieren, nichts. Alles, was Sie mit einer tun können, ist, sie unberührt und ungeprüft zu platzieren (Funktionsparameter, Teil einer Datenstruktur usw.).
Python hat diese Art von Grund bereits; Es ist der "Wert", den Sie von einem Ausdruck erhalten, der eine Ausnahme auslöst oder nicht beendet. Da Python eher streng als faul ist, können solche "Unterteile" nirgendwo gespeichert und möglicherweise ungeprüft gelassen werden. Es ist also nicht wirklich notwendig, das Konzept des unteren Werts zu verwenden, um zu erklären, wie Berechnungen, die keinen Wert zurückgeben, weiterhin so behandelt werden können, als ob sie einen Wert hätten. Aber es gibt auch keinen Grund, warum Sie nicht so über Ausnahmen nachdenken könnten, wenn Sie wollten.
Das Auslösen von Ausnahmen wird tatsächlich als "rein" betrachtet. Es sind auffällige Ausnahmen, die die Reinheit brechen - gerade weil Sie damit etwas über bestimmte Grundwerte prüfen können, anstatt sie alle austauschbar zu behandeln. In Haskell können Sie nur Ausnahmen abfangen
IO
, die eine unreine Schnittstelle zulassen (dies geschieht normalerweise in einer ziemlich äußeren Schicht). Python erzwingt keine Reinheit, aber Sie können selbst entscheiden, welche Funktionen zu Ihrer "äußeren unreinen Schicht" gehören und nicht zu reinen Funktionen, und nur zulassen, dass Sie dort Ausnahmen abfangen.Die Rückkehr
None
ist völlig anders.None
ist ein nicht unterster Wert; Sie können testen, ob etwas damit übereinstimmt, und der Aufrufer der zurückgegebenen FunktionNone
wird weiterhin ausgeführt, möglicherweise unter Verwendung vonNone
.Wenn Sie also daran denken, eine Ausnahme auszulösen, und "nach unten zurückkehren" möchten, um Haskells Ansatz zu emulieren, tun Sie einfach gar nichts. Lass die Ausnahme sich verbreiten. Genau das meinen Haskell-Programmierer, wenn sie über eine Funktion sprechen, die einen unteren Wert zurückgibt.
Aber das ist nicht das, was funktionierende Programmierer meinen, wenn sie Ausnahmen vermeiden wollen. Funktionale Programmierer bevorzugen "Gesamtfunktionen". Diese geben für jede mögliche Eingabe immer einen gültigen Non-Bottom-Wert ihres Rückgabetyps zurück . Jede Funktion, die eine Ausnahme auslösen kann, ist also keine Gesamtfunktion.
Der Grund, warum wir Total Functions mögen, ist, dass sie viel einfacher als "Black Boxes" zu behandeln sind, wenn wir sie kombinieren und manipulieren. Wenn ich eine Total-Funktion habe, die etwas vom Typ A zurückgibt, und eine Total-Funktion, die etwas vom Typ A akzeptiert, dann kann ich die Sekunde am Ausgang der ersten aufrufen, ohne etwas über die Implementierung von beiden zu wissen ; Ich weiß, dass ich ein gültiges Ergebnis erhalten werde, unabhängig davon, wie der Code einer der beiden Funktionen in Zukunft aktualisiert wird (solange ihre Gesamtheit erhalten bleibt und solange sie dieselbe Typensignatur behalten). Diese Trennung von Bedenken kann ein äußerst wirksames Hilfsmittel für das Refactoring sein.
Es ist auch etwas notwendig für zuverlässige Funktionen höherer Ordnung (Funktionen, die andere Funktionen manipulieren). Wenn ich Code schreiben möchten , die eine völlig willkürliche Funktion (mit einer bekannten Schnittstelle) als Parameter erhält ich habe es als Blackbox zu behandeln , weil ich keine Möglichkeit zu wissen , haben die Eingänge möglicherweise einen Fehler auslösen. Wenn mir eine Gesamtfunktion gegeben wird, verursacht keine Eingabe einen Fehler. Ebenso weiß der Aufrufer meiner übergeordneten Funktion nicht genau, welche Argumente ich verwende, um die von ihm übergebene Funktion aufzurufen (es sei denn, er möchte von meinen Implementierungsdetails abhängen). Wenn er also eine Gesamtfunktion übergibt, muss er sich keine Sorgen machen was ich damit mache.
Ein funktionaler Programmierer, der Ihnen rät, Ausnahmen zu vermeiden, würde es vorziehen, stattdessen einen Wert zurückzugeben, der entweder den Fehler oder einen gültigen Wert codiert, und der voraussetzt, dass Sie bereit sind, beide Möglichkeiten zu nutzen. Dinge wie
Either
Typen oderMaybe
/Option
-Typen sind einige der einfachsten Ansätze, um dies in stärker typisierten Sprachen zu tun (normalerweise verwendet mit spezieller Syntax oder Funktionen höherer Ordnung, um Dinge, die ein brauchen,A
mit Dingen zusammenzukleben, die ein erzeugenMaybe<A>
).Eine Funktion, die entweder
None
(wenn ein Fehler aufgetreten ist) oder einen Wert (wenn kein Fehler aufgetreten ist) zurückgibt, folgt keiner der oben genannten Strategien.In Python mit Duck-Typisierung wird der Either / Maybe-Stil nicht sehr häufig verwendet. Stattdessen werden Ausnahmen ausgelöst, um zu überprüfen, ob der Code funktioniert, und nicht um sicherzustellen, dass Funktionen vollständig sind und basierend auf ihren Typen automatisch kombiniert werden können. Python hat keine Möglichkeit, zu erzwingen, dass Code Dinge wie Vielleicht Typen richtig verwendet. Selbst wenn Sie es aus Gründen der Disziplin einsetzen, benötigen Sie Tests, um Ihren Code tatsächlich auszuüben und dies zu überprüfen. Daher ist der Ausnahmen- / Bottom-Ansatz wahrscheinlich eher für die reine Funktionsprogrammierung in Python geeignet.
quelle
Solange es keine äußerlich sichtbaren Nebenwirkungen gibt und der Rückgabewert ausschließlich von den Eingaben abhängt, ist die Funktion rein, auch wenn sie intern einige eher unreine Dinge tut.
Es kommt also wirklich darauf an, was genau dazu führen kann, dass Ausnahmen ausgelöst werden. Wenn Sie versuchen, eine Datei mit einem angegebenen Pfad zu öffnen, ist dies nicht der Fall, da die Datei möglicherweise vorhanden ist oder nicht, was dazu führen würde, dass der Rückgabewert für dieselbe Eingabe variiert.
Wenn Sie andererseits versuchen, eine Ganzzahl aus einer bestimmten Zeichenfolge zu analysieren und eine Ausnahme auszulösen, wenn sie fehlschlägt, kann dies rein sein, solange keine Ausnahmen aus Ihrer Funktion herausspringen können.
Auf einer Seite zur Kenntnis, funktionale Sprachen neigen dazu , den zurückEinheit Typen nur , wenn es nur ein einziger möglicher Fehlerzustand ist. Wenn mehrere mögliche Fehler vorliegen, geben sie in der Regel einen Fehlertyp mit Informationen zum Fehler zurück.
quelle
f
rein ist, wird erwartetf("/path/to/file")
, dass immer derselbe Wert zurückgegeben wird. Was passiert, wenn die aktuelle Datei zwischen zwei Aufrufen von gelöscht oder geändert wirdf
?