On m'a dit qu'en programmation fonctionnelle, on n'est pas censé lever et / ou observer des exceptions. Au lieu de cela, un calcul erroné doit être évalué comme une valeur inférieure. En Python (ou dans d'autres langages qui n'encouragent pas pleinement la programmation fonctionnelle), on peut retourner None
(ou une autre alternative traitée comme la valeur la plus basse, bien qu'elle None
ne soit pas strictement conforme à la définition) chaque fois que quelque chose se passe mal pour "rester pur", mais pour le faire il faut donc observer une erreur en premier lieu, c'est-à-dire
def fn(*args):
try:
... do something
except SomeException:
return None
Est-ce que cela viole la pureté? Et si oui, cela signifie-t-il qu'il est impossible de gérer les erreurs uniquement en Python?
Mise à jour
Dans son commentaire, Eric Lippert m'a rappelé une autre façon de traiter les exceptions en PF. Bien que je n'ai jamais vu cela en Python en pratique, j'ai joué avec quand j'ai étudié la PF il y a un an. Ici, toute optional
fonction -décorée renvoie des Optional
valeurs, qui peuvent être vides, pour les sorties normales ainsi que pour une liste d'exceptions spécifiée (les exceptions non spécifiées peuvent toujours mettre fin à l'exécution). Carry
crée une évaluation retardée, où chaque étape (appel de fonction retardée) obtient une Optional
sortie non vide de l'étape précédente et la transmet simplement, ou s'évalue autrement en passant une nouvelle Optional
. Au final, la valeur finale est soit normale, soit Empty
. Ici, le try/except
bloc est caché derrière un décorateur, de sorte que les exceptions spécifiées peuvent être considérées comme faisant partie de la signature de type de retour.
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
la source
Réponses:
Tout d'abord, clarifions certaines idées fausses. Il n'y a pas de "valeur inférieure". Le type inférieur est défini comme un type qui est un sous-type de tous les autres types dans la langue. De cela, on peut prouver (dans tout système de type intéressant au moins), que le type inférieur n'a pas de valeurs - il est vide . Il n'y a donc pas de valeur minimale.
Pourquoi le type inférieur est-il utile? Eh bien, sachant qu'il est vide, faisons des déductions sur le comportement du programme. Par exemple, si nous avons la fonction:
nous savons que
do_thing
cela ne peut jamais retourner, car il devrait retourner une valeur de typeBottom
. Ainsi, il n'y a que deux possibilités:do_thing
ne s'arrête pasdo_thing
lève une exception (dans les langues avec un mécanisme d'exception)Notez que j'ai créé un type
Bottom
qui n'existe pas réellement dans le langage Python.None
est un terme impropre; c'est en fait la valeur unitaire , la seule valeur du type d'unité , qui est appeléeNoneType
en Python (faites-letype(None)
pour vous-même).Maintenant, une autre idée fausse est que les langages fonctionnels n'ont pas d'exception. Ce n'est pas vrai non plus. SML par exemple a un très bon mécanisme d'exception. Cependant, les exceptions sont utilisées beaucoup plus avec parcimonie en SML qu'en Python par exemple. Comme vous l'avez dit, la manière courante d'indiquer une sorte d'échec dans les langages fonctionnels est de renvoyer un
Option
type. Par exemple, nous créerions une fonction de division sûre comme suit:Malheureusement, puisque Python n'a pas réellement de types de somme, ce n'est pas une approche viable. Vous pouvez revenir en
None
tant que type d'option de pauvre pour signifier l'échec, mais ce n'est vraiment pas mieux que de revenirNull
. Il n'y a pas de sécurité de type.Je conseillerais donc de suivre les conventions de la langue dans ce cas. Python utilise idiomatiquement les exceptions pour gérer le flux de contrôle (ce qui est une mauvaise conception, IMO, mais c'est néanmoins standard), donc à moins que vous ne travailliez qu'avec du code que vous avez écrit vous-même, je vous recommande de suivre la pratique standard. Que ce soit "pur" ou non n'a pas d'importance.
la source
None
n'était pas conforme à la définition. Quoi qu'il en soit, merci de me corriger. Ne pensez-vous pas que l'utilisation d'exception uniquement pour arrêter complètement l'exécution ou pour renvoyer une valeur facultative est conforme aux principes de Python? Je veux dire, pourquoi est-il mauvais de ne pas utiliser d'exception pour un contrôle compliqué?try/except/finally
comme une autre alternative àif/else
, c'est- à -diretry: var = expession1; except ...: var = expression 2; except ...: var = expression 3...
, bien que ce soit une chose courante à faire dans n'importe quel langage impératif (btw, je déconseille fortement d'utiliser desif/else
blocs pour cela également). Voulez-vous dire que je suis déraisonnable et que je devrais autoriser de tels modèles puisque "c'est Python"?try... catch...
doit pas être utilisé pour contrôler le débit. Pour une raison quelconque, c'est ainsi que la communauté Python a décidé de faire les choses. Par exemple, lasafe_div
fonction que j'ai écrite ci-dessus serait généralement écritetry: result = num / div: except ArithmeticError: result = None
. Donc, si vous leur enseignez les principes généraux de l'ingénierie logicielle, vous devriez certainement décourager cela.if ... else...
est aussi une odeur de code, mais c'est trop long pour entrer ici.Puisqu'il y a eu tellement d'intérêt pour la pureté au cours des derniers jours, pourquoi n'examinons-nous pas à quoi ressemble une fonction pure.
Une fonction pure:
Est référentiellement transparent; c'est-à-dire que pour une entrée donnée, elle produira toujours la même sortie.
Ne produit pas d'effets secondaires; il ne modifie pas les entrées, les sorties ou quoi que ce soit d'autre dans son environnement externe. Il ne produit qu'une valeur de retour.
Alors demandez-vous. Votre fonction fait-elle autre chose que d'accepter une entrée et de renvoyer une sortie?
la source
T + { Exception }
(oùT
est le type de retour explicitement déclaré), ce qui est problématique. Vous ne pouvez pas savoir si une fonction lèvera une exception sans regarder son code source, ce qui rend également difficile l'écriture de fonctions d'ordre supérieur.map : (A->B)->List A ->List B
endroit où l'A->B
erreur peut se produire. Si nous permettons à f de lever une exception,map f L
nous retournerons quelque chose de typeException + List<B>
. Si, au contraire, nous lui permettons de renvoyer unoptional
type de stylemap f L
, il renvoie à la place List <Facultatif <B>> `. Cette deuxième option me semble plus fonctionnelle.La sémantique Haskell utilise une "valeur inférieure" pour analyser la signification du code Haskell. Ce n'est pas quelque chose que vous utilisez vraiment directement dans la programmation de Haskell, et le retour
None
n'est pas du tout le même genre de chose.La valeur inférieure est la valeur attribuée par la sémantique de Haskell à tout calcul qui ne parvient pas à évaluer normalement une valeur. Un tel moyen qu'un calcul Haskell peut faire est en fait de lever une exception! Donc, si vous essayez d'utiliser ce style en Python, vous devriez en fait simplement lever les exceptions comme d'habitude.
La sémantique de Haskell utilise la valeur inférieure car Haskell est paresseux; vous êtes capable de manipuler des "valeurs" qui sont retournées par des calculs qui n'ont pas encore été exécutés. Vous pouvez les passer à des fonctions, les coller dans des structures de données, etc. Un tel calcul non évalué peut lever une exception ou une boucle pour toujours, mais si nous n'avons jamais réellement besoin d'examiner la valeur, le calcul ne sera jamaisexécutez et rencontrez l'erreur, et notre programme global pourrait réussir à faire quelque chose de bien défini et à terminer. Donc, sans vouloir expliquer ce que signifie le code Haskell en spécifiant le comportement opérationnel exact du programme au moment de l'exécution, nous déclarons plutôt que de tels calculs erronés produisent la valeur inférieure et expliquons ce que cette valeur se comporte; fondamentalement, toute expression qui doit dépendre de toutes les propriétés de la valeur inférieure (autre qu'elle existe) entraînera également la valeur inférieure.
Pour rester «pur», toutes les façons possibles de générer la valeur inférieure doivent être traitées comme équivalentes. Cela inclut la "valeur inférieure" qui représente une boucle infinie. Puisqu'il n'y a aucun moyen de savoir que certaines boucles infinies sont en fait infinies (elles pourraient se terminer si vous les exécutez juste un peu plus longtemps), vous ne pouvez pas examiner aucune propriété d'une valeur inférieure. Vous ne pouvez pas tester si quelque chose est en bas, ne pouvez pas le comparer à autre chose, ne pouvez pas le convertir en chaîne, rien. Tout ce que vous pouvez faire avec un, c'est de le mettre en place (paramètres de fonction, partie d'une structure de données, etc.) intact et non examiné.
Python a déjà ce type de fond; c'est la "valeur" que vous obtenez d'une expression qui lève une exception ou ne se termine pas. Parce que Python est strict plutôt que paresseux, ces "fonds" ne peuvent pas être stockés n'importe où et potentiellement non examinés. Il n'est donc pas vraiment nécessaire d'utiliser le concept de la valeur inférieure pour expliquer comment les calculs qui ne renvoient pas de valeur peuvent toujours être traités comme s'ils avaient une valeur. Mais il n'y a également aucune raison de ne pas penser de cette façon aux exceptions si vous le vouliez.
Le fait de lever des exceptions est en fait considéré comme "pur". C'est intercepter des exceptions qui brise la pureté - précisément parce qu'il vous permet d'inspecter quelque chose sur certaines valeurs inférieures, au lieu de les traiter toutes de manière interchangeable. Dans Haskell, vous ne pouvez attraper que des exceptions dans le
IO
qui permet une interface impure (donc cela se produit généralement sur une couche assez externe). Python n'applique pas la pureté, mais vous pouvez toujours décider par vous-même quelles fonctions font partie de votre "couche impure externe" plutôt que des fonctions pures, et ne vous autorisez qu'à y détecter des exceptions.Le retour
None
est complètement différent.None
est une valeur non inférieure; vous pouvez tester si quelque chose lui est égal et l'appelant de la fonction retournéeNone
continuera à s'exécuter, en utilisant peut-être deNone
manière inappropriée.Donc, si vous envisagez de lever une exception et que vous souhaitez «revenir en bas» pour imiter l'approche de Haskell, vous ne faites rien du tout. Laissez l'exception se propager. C'est exactement ce que les programmeurs Haskell veulent dire quand ils parlent d'une fonction renvoyant une valeur inférieure.
Mais ce n'est pas ce que les programmeurs fonctionnels veulent dire quand ils disent d'éviter les exceptions. Les programmeurs fonctionnels préfèrent les «fonctions totales». Ceux-ci renvoient toujours une valeur non inférieure valide de leur type de retour pour chaque entrée possible. Donc, toute fonction qui peut lever une exception n'est pas une fonction totale.
La raison pour laquelle nous aimons les fonctions totales est qu'elles sont beaucoup plus faciles à traiter comme des "boîtes noires" lorsque nous les combinons et les manipulons. Si j'ai une fonction totale renvoyant quelque chose de type A et une fonction totale qui accepte quelque chose de type A, alors je peux appeler la seconde à la sortie de la première, sans rien savoir de l'implémentation de l'une ou l'autre; Je sais que j'obtiendrai un résultat valide, peu importe la façon dont le code de l'une ou l'autre fonction sera mis à jour à l'avenir (tant que leur totalité est conservée et tant qu'ils conservent la même signature de type). Cette séparation des préoccupations peut être une aide extrêmement puissante pour la refactorisation.
Il est également quelque peu nécessaire pour des fonctions fiables d'ordre supérieur (fonctions qui manipulent d'autres fonctions). Si je veux écrire du code qui reçoit une fonction complètement arbitraire (avec une interface connue) comme paramètre, je dois le traiter comme une boîte noire parce que je n'ai aucun moyen de savoir quelles entrées peuvent déclencher une erreur. Si on me donne une fonction totale, aucune entrée ne provoquera une erreur. De même, l'appelant de ma fonction d'ordre supérieur ne saura pas exactement quels arguments j'utilise pour appeler la fonction qu'il me passe (sauf s'il veut dépendre de mes détails d'implémentation), donc passer une fonction totale signifie qu'il n'a pas à s'inquiéter ce que j'en fais.
Ainsi, un programmeur fonctionnel qui vous conseille d'éviter les exceptions préférerait que vous retourniez plutôt une valeur qui code l'erreur ou une valeur valide, et exige que pour l'utiliser, vous êtes prêt à gérer les deux possibilités. Des choses comme
Either
types ouMaybe
/Option
types sont quelques-unes des approches les plus simples pour le faire dans des langages plus fortement typés (généralement utilisés avec une syntaxe spéciale ou des fonctions d'ordre supérieur pour aider à coller ensemble les choses qui ont besoin d'uneA
avec les choses qui produisent uneMaybe<A>
).Une fonction qui retourne
None
(si une erreur s'est produite) ou une certaine valeur (s'il n'y a pas eu d'erreur) ne suit aucune des stratégies ci-dessus.En Python avec typage canard, le style Soit / Peut-être n'est pas beaucoup utilisé, laissant plutôt des exceptions levées, avec des tests pour valider que le code fonctionne plutôt que de faire confiance aux fonctions pour être totales et automatiquement combinables en fonction de leurs types. Python n'a aucune facilité pour faire appliquer ce code utilise des choses comme les types peut-être correctement; même si vous l'utilisiez comme une question de discipline, vous avez besoin de tests pour exercer réellement votre code pour le valider. Ainsi, l'approche exception / bas est probablement plus adaptée à la programmation fonctionnelle pure en Python.
la source
Tant qu'il n'y a pas d'effets secondaires visibles de l'extérieur et que la valeur de retour dépend exclusivement des entrées, la fonction est pure, même si elle fait des choses plutôt impures en interne.
Cela dépend donc vraiment de ce qui peut provoquer le déclenchement d'exceptions. Si vous essayez d'ouvrir un fichier avec un chemin d'accès, alors non, ce n'est pas pur, car le fichier peut ou non exister, ce qui entraînerait une variation de la valeur de retour pour la même entrée.
D'un autre côté, si vous essayez d'analyser un entier à partir d'une chaîne donnée et de lever une exception s'il échoue, cela pourrait être pur, tant qu'aucune exception ne peut sortir de votre fonction.
De plus, les langages fonctionnels ont tendance à renvoyer le type d' unité uniquement s'il n'y a qu'une seule condition d'erreur possible. S'il y a plusieurs erreurs possibles, elles ont tendance à renvoyer un type d'erreur avec des informations sur l'erreur.
la source
f
est pure, vous vous attendezf("/path/to/file")
à toujours renvoyer la même valeur. Que se passe-t-il si le fichier réel est supprimé ou modifié entre deux appels àf
?