Foi-me dito que na programação funcional não se deve lançar e / ou observar exceções. Em vez disso, um cálculo incorreto deve ser avaliado como um valor inferior. No Python (ou em outras linguagens que não incentivam totalmente a programação funcional), pode-se retornar None
(ou outra alternativa tratada como o valor mais baixo, embora None
não esteja estritamente de acordo com a definição) sempre que algo der errado para "permanecer puro", mas fazer então é preciso observar um erro em primeiro lugar, ou seja,
def fn(*args):
try:
... do something
except SomeException:
return None
Isso viola a pureza? E se sim, isso significa que é impossível lidar com erros puramente em Python?
Atualizar
Em seu comentário, Eric Lippert me lembrou outra maneira de tratar exceções no PF. Embora eu nunca tenha visto isso na prática em Python, eu brinquei com ela quando estudei FP há um ano. Aqui, qualquer optional
função -decorada retorna Optional
valores, que podem estar vazios, para saídas normais e para uma lista especificada de exceções (exceções não especificadas ainda podem terminar a execução). Carry
cria uma avaliação atrasada, em que cada etapa (chamada de função atrasada) obtém uma Optional
saída não vazia da etapa anterior e simplesmente a transmite, ou avalia a si mesma passando uma nova Optional
. No final, o valor final é normal ou Empty
. Aqui o try/except
bloco fica oculto atrás de um decorador, portanto as exceções especificadas podem ser consideradas como parte da assinatura do 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
fonte
Respostas:
Primeiro de tudo, vamos esclarecer alguns equívocos. Não há "valor inferior". O tipo inferior é definido como um tipo que é um subtipo de qualquer outro tipo no idioma. A partir disso, pode-se provar (pelo menos em qualquer sistema de tipos interessantes), que o tipo inferior não tem valores - está vazio . Portanto, não existe um valor inferior.
Por que o tipo inferior é útil? Bem, sabendo que está vazio, vamos fazer algumas deduções no comportamento do programa. Por exemplo, se tivermos a função:
sabemos que
do_thing
nunca pode voltar, uma vez que teria de retornar um valor do tipoBottom
. Assim, existem apenas duas possibilidades:do_thing
não parado_thing
lança uma exceção (em idiomas com um mecanismo de exceção)Observe que eu criei um tipo
Bottom
que realmente não existe na linguagem Python.None
é um nome impróprio; na verdade, é o valor da unidade , o único valor do tipo de unidade , chamadoNoneType
em Python (façatype(None)
para confirmar por si mesmo).Agora, outro equívoco é que as linguagens funcionais não têm exceção. Isso também não é verdade. O SML, por exemplo, possui um mecanismo de exceção muito bom. No entanto, as exceções são usadas com muito mais moderação no SML do que no Python, por exemplo. Como você disse, a maneira comum de indicar algum tipo de falha nas linguagens funcionais é retornando um
Option
tipo. Por exemplo, criaríamos uma função de divisão segura da seguinte maneira:Infelizmente, como o Python não possui realmente tipos de soma, essa não é uma abordagem viável. Você pode retornar
None
como um tipo de opção de pobre para significar falha, mas isso não é nada melhor do que retornarNull
. Não há segurança de tipo.Então, eu recomendaria seguir as convenções do idioma neste caso. O Python usa exceções linguisticamente para lidar com o fluxo de controle (que é um design ruim, IMO, mas é padrão), portanto, a menos que você esteja trabalhando apenas com o código que você mesmo escreveu, recomendo seguir a prática padrão. Se isso é "puro" ou não, é irrelevante.
fonte
None
não estava de acordo com a definição. De qualquer forma, obrigado por me corrigir. Você não acha que usar exceção apenas para interromper completamente a execução ou retornar um valor opcional está de acordo com os princípios do Python? Quero dizer, por que é ruim não usar exceção para controle complicado?try/except/finally
como outra alternativaif/else
, ou sejatry: var = expession1; except ...: var = expression 2; except ...: var = expression 3...
, embora seja algo comum em qualquer linguagem imperativa (btw, eu desencorajo fortemente o uso deif/else
blocos para isso também). Você quer dizer que eu estou sendo irracional e deveria permitir esses padrões, já que "this is Python"?try... catch...
deve ser usado para controle de fluxo. Por alguma razão, foi assim que a comunidade Python decidiu fazer as coisas. Por exemplo, a função que escrevi acima normalmente seria escrita . Portanto, se você está ensinando a eles os princípios gerais de engenharia de software, você definitivamente deve desencorajar isso. também é um cheiro de código, mas é muito tempo para entrar aqui.safe_div
try: result = num / div: except ArithmeticError: result = None
if ... else...
Como houve muito interesse pela pureza nos últimos dias, por que não examinamos como é uma função pura?
Uma função pura:
É referencialmente transparente; isto é, para uma determinada entrada, ela sempre produzirá a mesma saída.
Não produz efeitos colaterais; não altera as entradas, saídas ou qualquer outra coisa em seu ambiente externo. Só produz um valor de retorno.
Então pergunte a si mesmo. Sua função faz alguma coisa além de aceitar uma entrada e retornar uma saída?
fonte
T + { Exception }
(ondeT
é o tipo de retorno declarado explicitamente), o que é problemático. Você não pode saber se uma função lançará uma exceção sem examinar seu código-fonte, o que também torna problemática a gravação de funções de ordem superior.map : (A->B)->List A ->List B
whereA->B
can error. Se permitirmos que f para lançar uma exceçãomap f L
retorne algo do tipoException + List<B>
. Se, em vez disso, permitirmos que ele retorne umoptional
tipo de estilomap f L
, retornará List <Optional <B>> `. Esta segunda opção parece mais funcional para mim.A semântica de Haskell usa um "valor inferior" para analisar o significado do código Haskell. Não é algo que você realmente use diretamente na programação do Haskell, e retornar
None
não é o mesmo tipo de coisa.O valor inferior é o valor atribuído pela semântica de Haskell a qualquer cálculo que falha ao avaliar normalmente um valor. Uma dessas maneiras que uma computação Haskell pode fazer é lançando uma exceção! Portanto, se você estava tentando usar esse estilo no Python, deveria lançar exceções normalmente.
A semântica de Haskell usa o valor mais baixo porque Haskell é preguiçoso; você pode manipular "valores" retornados por cálculos que ainda não foram executados. Você pode passá-los para funções, colá-los em estruturas de dados, etc. Essa computação não avaliada pode gerar uma exceção ou um loop para sempre, mas se nunca precisarmos realmente examinar o valor, a computação nuncaexecute e encontre o erro, e nosso programa geral poderá fazer algo bem definido e finalizado. Portanto, sem querer explicar o que significa o código Haskell, especificando o comportamento operacional exato do programa em tempo de execução, declaramos que tais cálculos errôneos produzem o valor mais baixo e explicamos o que esse valor se comporta; basicamente, qualquer expressão que precise depender de todas as propriedades do valor inferior (além da existente) também resultará no valor inferior.
Para permanecer "puro", todas as formas possíveis de gerar o valor mais baixo devem ser tratadas como equivalentes. Isso inclui o "valor inferior" que representa um loop infinito. Como não há como saber que alguns loops infinitos são realmente infinitos (eles podem terminar se você os executar por mais algum tempo), não é possível examinar nenhuma propriedade de valor inferior. Você não pode testar se algo está embaixo, não pode compará-lo a qualquer outra coisa, não pode convertê-lo em uma string, nada. Tudo o que você pode fazer com um é colocar os lugares (parâmetros de função, parte de uma estrutura de dados, etc.) intocados e não examinados.
Python já tem esse tipo de fundo; é o "valor" que você obtém de uma expressão que gera uma exceção ou não termina. Como o Python é mais rigoroso do que preguiçoso, esses "fundos" não podem ser armazenados em nenhum lugar e potencialmente deixados sem serem examinados. Portanto, não há necessidade real de usar o conceito de valor inferior para explicar como os cálculos que não retornam um valor ainda podem ser tratados como se tivessem um valor. Mas também não há razão para que você não pense dessa maneira sobre exceções, se quiser.
Lançar exceções é realmente considerado "puro". É a captura de exceções que quebra a pureza - precisamente porque permite que você inspecione algo sobre certos valores inferiores, em vez de tratá-los todos de forma intercambiável. No Haskell, você só pode capturar exceções no
IO
que permite interfaces impuras (o que geralmente acontece em uma camada bastante externa). O Python não impõe pureza, mas você ainda pode decidir por si mesmo quais funções fazem parte da sua "camada impura externa" em vez de funções puras, e apenas se permite capturar exceções lá.Retornar
None
é completamente diferente.None
é um valor não inferior; você pode testar se algo é igual a ele e o chamador da função que retornouNone
continuará sendo executado, possivelmente usando oNone
inapropriadamente.Portanto, se você estava pensando em lançar uma exceção e deseja "voltar ao fundo" para imitar a abordagem de Haskell, você simplesmente não faz nada. Deixe a exceção se propagar. É exatamente isso que os programadores Haskell querem dizer quando falam sobre uma função retornando um valor mais baixo.
Mas não é isso que os programadores funcionais querem dizer quando dizem para evitar exceções. Programadores funcionais preferem "funções totais". Eles sempre retornam um valor não inferior válido de seu tipo de retorno para todas as entradas possíveis. Portanto, qualquer função que possa gerar uma exceção não é uma função total.
A razão pela qual gostamos de funções totais é que elas são muito mais fáceis de tratar como "caixas pretas" quando as combinamos e manipulamos. Se eu tiver uma função total retornando algo do tipo A e uma função total que aceite algo do tipo A, então eu posso chamar o segundo na saída do primeiro, sem saber nada sobre a implementação de qualquer um; Eu sei que vou obter um resultado válido, não importa como o código de uma das funções seja atualizado no futuro (desde que a totalidade seja mantida e enquanto eles mantêm a mesma assinatura de tipo). Essa separação de preocupações pode ser uma ajuda extremamente poderosa para a refatoração.
Também é um pouco necessário para funções confiáveis de ordem superior (funções que manipulam outras funções). Se eu quiser escrever código que recebe uma função completamente arbitrário (com uma interface conhecida) como um parâmetro que eu tenho de tratá-lo como uma caixa preta, porque eu não tenho nenhuma maneira de saber quais entradas podem desencadear um erro. Se eu receber uma função total, nenhuma entrada causará um erro. Da mesma forma, o responsável pela chamada da minha função de ordem superior não saberá exatamente quais argumentos eu uso para chamar a função que eles passam por mim (a menos que desejem depender dos detalhes da minha implementação). Portanto, passar uma função total significa que eles não precisam se preocupar com o que eu faço com isso.
Portanto, um programador funcional que o aconselha a evitar exceções prefere que você retorne um valor que codifique o erro ou um valor válido e exija que, para usá-lo, esteja preparado para lidar com as duas possibilidades. Coisas como
Either
types ouMaybe
/Option
types são algumas das abordagens mais simples para fazer isso em linguagens mais fortemente tipadas (geralmente usadas com sintaxe especial ou funções de ordem superior para ajudar a colar coisas que precisam de umA
com coisas que produzem aMaybe<A>
).Uma função que retorna
None
(se ocorreu um erro) ou algum valor (se não houve erro) está seguindo nenhuma das estratégias acima.No Python com duck digitando, o estilo Either / Maybe não é muito usado, ao invés disso, permite que exceções sejam lançadas, com testes para validar que o código funciona, em vez de confiar que as funções sejam totais e combináveis automaticamente com base em seus tipos. O Python não tem nenhuma facilidade para impor que o código use coisas como Talvez tipos corretamente; mesmo se você o estivesse usando por uma questão de disciplina, precisará de testes para realmente exercitar seu código para validar isso. Portanto, a abordagem de exceção / parte inferior é provavelmente mais adequada à programação funcional pura em Python.
fonte
Enquanto não houver efeitos colaterais visíveis externamente e o valor de retorno depender exclusivamente das entradas, a função será pura, mesmo que faça algumas coisas impuras internamente.
Portanto, depende realmente do que exatamente pode causar exceções. Se você está tentando abrir um arquivo com um caminho, não, não é puro, porque o arquivo pode ou não existir, o que faria com que o valor de retorno variasse para a mesma entrada.
Por outro lado, se você estiver tentando analisar um número inteiro de uma determinada string e lançar uma exceção se ela falhar, isso pode ser puro, desde que nenhuma exceção possa sair de sua função.
Em uma nota lateral, os idiomas funcionais tendem a retornar o tipo de unidade apenas se houver apenas uma única condição de erro possível. Se houver vários erros possíveis, eles tendem a retornar um tipo de erro com informações sobre o erro.
fonte
f
for pura, você esperariaf("/path/to/file")
sempre retornar o mesmo valor. O que acontece se o arquivo real for excluído ou alterado entre duas invocações paraf
?