Me han dicho que en la programación funcional no se debe lanzar y / u observar excepciones. En cambio, un cálculo erróneo debe evaluarse como un valor inferior. En Python (u otros lenguajes que no fomentan completamente la programación funcional) uno puede regresar None
(u otra alternativa tratada como el valor inferior, aunque None
no cumple estrictamente con la definición) cuando algo sale mal para "permanecer puro", pero para hacerlo así que uno debe observar un error en primer lugar, es decir
def fn(*args):
try:
... do something
except SomeException:
return None
¿Esto viola la pureza? Y si es así, ¿significa que es imposible manejar los errores únicamente en Python?
Actualizar
En su comentario, Eric Lippert me recordó otra forma de tratar las excepciones en FP. Aunque nunca lo había visto en Python en la práctica, jugué con él cuando estudié FP hace un año. Aquí, cualquier optional
función decorada devuelve Optional
valores, que pueden estar vacíos, para salidas normales así como para una lista específica de excepciones (las excepciones no especificadas aún pueden terminar la ejecución). Carry
crea una evaluación retrasada, donde cada paso (llamada de función retrasada) obtiene un Optional
resultado no vacío del paso anterior y simplemente lo pasa, o de lo contrario se evalúa pasando un nuevo Optional
. Al final, el valor final es normal o Empty
. Aquí el try/except
bloque está oculto detrás de un decorador, por lo que las excepciones especificadas se pueden considerar como parte de la firma de tipo de retorno.
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
fuente
Respuestas:
En primer lugar, aclaremos algunos conceptos erróneos. No hay "valor de fondo". El tipo inferior se define como un tipo que es un subtipo de cualquier otro tipo en el idioma. A partir de esto, se puede demostrar (al menos en cualquier sistema de tipos interesante), que el tipo inferior no tiene valores , está vacío . Por lo tanto, no existe un valor inferior.
¿Por qué es útil el tipo de fondo? Bueno, sabiendo que está vacío, hagamos algunas deducciones sobre el comportamiento del programa. Por ejemplo, si tenemos la función:
sabemos que
do_thing
nunca puede regresar, ya que tendría que devolver un valor de tipoBottom
. Por lo tanto, solo hay dos posibilidades:do_thing
no se detienedo_thing
lanza una excepción (en idiomas con un mecanismo de excepción)Tenga en cuenta que creé un tipo
Bottom
que en realidad no existe en el lenguaje Python.None
es un nombre inapropiado; en realidad es el valor de la unidad , el único valor del tipo de unidad , que se llamaNoneType
en Python (hazlotype(None)
por ti mismo).Ahora, otro concepto erróneo es que los lenguajes funcionales no tienen excepción. Esto tampoco es cierto. SML, por ejemplo, tiene un mecanismo de excepción muy agradable. Sin embargo, las excepciones se usan mucho más moderadamente en SML que, por ejemplo, en Python. Como ha dicho, la forma común de indicar algún tipo de falla en los lenguajes funcionales es devolviendo un
Option
tipo. Por ejemplo, crearíamos una función de división segura de la siguiente manera:Desafortunadamente, dado que Python no tiene tipos de suma, este no es un enfoque viable. Usted podría regresar
None
como un hombre pobre tipo de opción al fracaso significan, pero esto no es realmente mejor que volverNull
. No hay tipo de seguridad.Por lo tanto, recomendaría seguir las convenciones del lenguaje en este caso. Python usa excepciones idiomáticamente para manejar el flujo de control (que es un mal diseño, IMO, pero es estándar de todos modos), así que a menos que solo esté trabajando con el código que escribió usted mismo, recomendaría seguir la práctica estándar. Si esto es "puro" o no es irrelevante.
fuente
None
no cumplía con la definición. De todos modos, gracias por corregirme. ¿No cree que usar la excepción solo para detener la ejecución por completo o devolver un valor opcional está bien con los principios de Python? Quiero decir, ¿por qué es malo evitar usar excepciones para un control complicado?try/except/finally
como otra alternativaif/else
, es decirtry: var = expession1; except ...: var = expression 2; except ...: var = expression 3...
, aunque es algo común en cualquier idioma imperativo (por cierto, desaconsejo usarif/else
bloques también para esto). ¿Quiere decir que no estoy siendo razonable y debería permitir tales patrones ya que "esto es Python"?try... catch...
debe no ser utilizado para el flujo de control. Por alguna razón, así es como la comunidad Python decidió hacer las cosas. Por ejemplo, lasafe_div
función que escribí arriba generalmente se escribiríatry: result = num / div: except ArithmeticError: result = None
. Entonces, si les está enseñando principios generales de ingeniería de software, definitivamente debería desalentar esto.if ... else...
también es un olor a código, pero eso es demasiado largo para entrar aquí.Dado que ha habido tanto interés en la pureza en los últimos días, ¿por qué no examinamos cómo es una función pura?
Una función pura:
Es referencialmente transparente; es decir, para una entrada dada, siempre producirá la misma salida.
No produce efectos secundarios; no cambia las entradas, salidas o cualquier otra cosa en su entorno externo. Solo produce un valor de retorno.
Entonces pregúntate a ti mismo. ¿Su función hace algo más que aceptar una entrada y devolver una salida?
fuente
T + { Exception }
(dondeT
es el tipo de retorno declarado explícitamente), lo cual es problemático. No puede saber si una función arrojará una excepción sin mirar su código fuente, lo que hace que escribir funciones de orden superior también sea problemático.map : (A->B)->List A ->List B
dóndeA->B
puede el error. Si permitimos que f arroje una excepciónmap f L
, devolverá algo de tipoException + List<B>
. Si, en cambio, permitimos que devuelva unoptional
tipo de estilomap f L
, en su lugar devolverá Lista <Opcional <B>> `. Esta segunda opción me parece más funcional.La semántica de Haskell usa un "valor inferior" para analizar el significado del código de Haskell. No es algo que realmente uses directamente en la programación de Haskell, y regresar
None
no es para nada el mismo tipo de cosas.El valor inferior es el valor atribuido por la semántica de Haskell a cualquier cálculo que no puede evaluar un valor normalmente. ¡Una de las formas en que un cálculo de Haskell puede hacer eso es en realidad lanzando una excepción! Entonces, si intentabas usar este estilo en Python, en realidad solo deberías lanzar excepciones de manera normal.
La semántica de Haskell usa el valor inferior porque Haskell es vago; puede manipular los "valores" que devuelven los cálculos que aún no se han ejecutado. Puede pasarlos a funciones, pegarlos en estructuras de datos, etc. Tal cálculo no evaluado podría generar una excepción o bucle para siempre, pero si nunca necesitamos examinar el valor, entonces el cálculo nuncaejecutar y encontrar el error, y nuestro programa en general podría lograr hacer algo bien definido y finalizar. Entonces, sin querer explicar qué significa el código de Haskell al especificar el comportamiento operativo exacto del programa en tiempo de ejecución, en su lugar declaramos que estos cálculos erróneos producen el valor inferior y explicamos qué se comporta ese valor; Básicamente, cualquier expresión que deba depender de cualquier propiedad del valor inferior (aparte de la existente) también dará como resultado el valor inferior.
Para permanecer "puro", todas las formas posibles de generar el valor inferior deben tratarse como equivalentes. Eso incluye el "valor inferior" que representa un bucle infinito. Como no hay forma de saber que algunos bucles infinitos en realidad son infinitos (podrían terminar si los ejecuta un poco más), no puede examinar ninguna propiedad de un valor inferior. No puede probar si algo está en la parte inferior, no puede compararlo con nada más, no puede convertirlo en una cadena, nada. Todo lo que puede hacer con uno es colocarlo (parámetros de función, parte de una estructura de datos, etc.) intactos y sin examinar.
Python ya tiene este tipo de fondo; Es el "valor" que se obtiene de una expresión que arroja una excepción o no termina. Debido a que Python es estricto en lugar de perezoso, tales "fondos" no se pueden almacenar en ningún lugar y, potencialmente, no se pueden examinar. Por lo tanto, no hay necesidad real de utilizar el concepto del valor inferior para explicar cómo los cálculos que no devuelven un valor aún pueden tratarse como si tuvieran un valor. Pero tampoco hay ninguna razón por la que no pueda pensar de esta manera acerca de las excepciones si lo desea.
Lanzar excepciones se considera realmente "puro". Es la captura de excepciones que se rompe la pureza - precisamente porque le permite inspeccionar algo acerca de ciertos valores inferiores, en lugar de tratar a todos indistintamente. En Haskell solo puede detectar excepciones
IO
que permiten una interfaz impura (por lo que generalmente ocurre en una capa bastante externa). Python no exige la pureza, pero aún puede decidir por sí mismo qué funciones forman parte de su "capa impura externa" en lugar de las funciones puras, y solo se permite detectar excepciones allí.Volver en su
None
lugar es completamente diferente.None
es un valor no inferior; puede probar si algo es igual a él, y la persona que llama de la función que regresóNone
continuará ejecutándose, posiblemente usando elNone
inapropiado.Entonces, si estaba pensando en lanzar una excepción y quiere "volver al fondo" para emular el enfoque de Haskell, simplemente no haga nada en absoluto. Deje que la excepción se propague. Eso es exactamente lo que los programadores de Haskell quieren decir cuando hablan de una función que devuelve un valor inferior.
Pero eso no es lo que los programadores funcionales quieren decir cuando dicen evitar las excepciones. Los programadores funcionales prefieren "funciones totales". Estos siempre devuelven un valor no inferior válido de su tipo de retorno para cada entrada posible. Entonces, cualquier función que pueda generar una excepción no es una función total.
La razón por la que nos gustan las funciones totales es que son mucho más fáciles de tratar como "cajas negras" cuando las combinamos y manipulamos. Si tengo una función total que devuelve algo de tipo A y una función total que acepta algo de tipo A, entonces puedo llamar a la segunda en la salida de la primera, sin saber nada sobre la implementación de cualquiera; Sé que obtendré un resultado válido, sin importar cómo se actualice el código de cualquiera de las funciones en el futuro (siempre que se mantenga su totalidad y siempre que conserven la misma firma de tipo). Esta separación de preocupaciones puede ser una ayuda extremadamente poderosa para la refactorización.
También es algo necesario para funciones confiables de orden superior (funciones que manipulan otras funciones). Si quiero escribir código que recibe una función completamente arbitraria (con una interfaz conocida) como un parámetro que tengo que tratarlo como un cuadro negro porque no tengo manera de saber qué entradas podrían dar lugar a un error. Si me dan una función total, ninguna entrada causará un error. Del mismo modo, la persona que llama de mi función de orden superior no sabrá exactamente qué argumentos uso para llamar a la función que me pasan (a menos que quieran depender de los detalles de mi implementación), por lo que pasar una función total significa que no tienen que preocuparse lo que hago con eso
Por lo tanto, un programador funcional que le aconseje que evite las excepciones preferiría que en su lugar devuelva un valor que codifique el error o un valor válido, y que requiera que para usarlo esté preparado para manejar ambas posibilidades. Cosas como
Either
tipos oMaybe
/Option
tipos son algunos de los más sencillos se aproxima a ello en los idiomas más inflexible de tipos (se usa generalmente sintaxis especial o funciones de orden superior a las cosas juntos ayuda de pegamento que necesitan unaA
de las cosas que producen unaMaybe<A>
).Una función que devuelve
None
(si ocurrió un error) o algún valor (si no hubo error) no está siguiendo ninguna de las estrategias anteriores.En Python con pato escribiendo, el estilo Either / Maybe no se usa mucho, en lugar de permitir que se generen excepciones, con pruebas para validar que el código funciona en lugar de confiar en que las funciones sean totales y combinables automáticamente según sus tipos. Python no tiene facilidad para hacer cumplir ese código que usa cosas como los tipos tal vez correctamente; incluso si lo estaba utilizando como una cuestión de disciplina, necesita pruebas para ejercer realmente su código para validar eso. Por lo tanto, el enfoque de excepción / fondo probablemente sea más adecuado para la programación funcional pura en Python.
fuente
Mientras no haya efectos secundarios visibles externamente y el valor de retorno dependa exclusivamente de las entradas, entonces la función es pura, incluso si hace algunas cosas impuras internamente.
Por lo tanto, realmente depende de qué puede causar excepciones. Si está tratando de abrir un archivo dado una ruta, entonces no, no es puro, porque el archivo puede o no existir, lo que causaría que el valor de retorno varíe para la misma entrada.
Por otro lado, si está tratando de analizar un número entero de una cadena dada y arroja una excepción si falla, eso podría ser puro, siempre que no haya excepciones que salgan de su función.
En una nota al margen, los lenguajes funcionales tienden a devolver el tipo de unidad solo si hay una sola condición de error posible. Si hay varios errores posibles, tienden a devolver un tipo de error con información sobre el error.
fuente
f
es pura, esperaríaf("/path/to/file")
devolver siempre el mismo valor. ¿Qué sucede si el archivo real se elimina o cambia entre dos invocacionesf
?