Capturar / lançar exceções tornam impuro um método puro?

27

Os exemplos de código a seguir fornecem contexto para minha pergunta.

A classe Room é inicializada com um delegado. Na primeira implementação da classe Room, não há guardas contra delegados que lançam exceções. Essas exceções aparecerão na propriedade North, onde o delegado é avaliado (nota: o método Main () demonstra como uma instância de Room é usada no código do cliente):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Sendo que prefiro falhar na criação do objeto do que ao ler a propriedade North, altero o construtor para private e apresento um método de fábrica estático chamado Create (). Este método captura a exceção lançada pelo delegado e lança uma exceção do wrapper, com uma mensagem de exceção significativa:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

O bloco try-catch torna impuro o método Create ()?

Rock Anthony Johnson
fonte
1
Qual é o benefício da função Criar quarto; se você estiver usando um delegado, por que chamá-lo antes que o cliente chame 'Norte'?
SH-
1
@ SH Se o delegado lança uma exceção, quero descobrir sobre a criação do objeto Room. Não quero que o cliente encontre a exceção no uso, mas na criação. O método Create é o local perfeito para expor delegados malignos.
Rock Anthony Johnson
3
@RockAnthonyJohnson, Sim. O que você faz ao executar o delegado só pode funcionar na primeira vez ou retornar uma sala diferente na segunda chamada? Ligar para o delegado pode ser considerado / causar um efeito colateral em si?
precisa
4
Uma função que chama uma função impura é uma função impura; portanto, se o delegado é impuro Createtambém é impuro, porque chama.
Idan Arye
2
Sua Createfunção não o protege de obter uma exceção ao obter a propriedade. Se o seu delegado jogar, na vida real, é muito provável que ele seja jogado apenas sob algumas condições. As chances são de que as condições de lançamento não estejam presentes durante a construção, mas estão presentes ao obter a propriedade.
Bart van Ingen Schenau 27/10/16

Respostas:

26

Sim. Essa é efetivamente uma função impura. Isso cria um efeito colateral: a execução do programa continua em algum lugar que não seja o local para o qual a função deve retornar.

Para torná-lo uma função pura, returnum objeto real que encapsula o valor esperado da função e um valor indicando uma possível condição de erro, como um Maybeobjeto ou um objeto de Unidade de Trabalho.

Robert Harvey
fonte
16
Lembre-se, "puro" é apenas uma definição. Se você precisa de uma função que lança, mas de qualquer outra maneira é referencialmente transparente, é isso que você escreve.
Robert Harvey
1
No haskell, pelo menos qualquer função pode ser lançada, mas você precisa estar no IO para capturar, uma função pura que às vezes lança normalmente seria chamada de parcial
jk.
4
Eu tive uma epifania por causa do seu comentário. Embora eu esteja intelectualmente intrigado com a pureza, o que eu realmente preciso na praticidade é determinismo e transparência reverencial. Se eu conseguir pureza, isso é apenas um bônus a mais. Obrigado. Para sua informação, o que me levou a seguir esse caminho é o fato de o Microsoft IntelliTest ser eficaz apenas contra códigos determinísticos.
Rock Anthony Johnson
4
@RockAnthonyJohnson: Bem, todo código deve ser determinístico, ou pelo menos o mais determinístico possível. A transparência referencial é apenas uma maneira de obter esse determinismo. Algumas técnicas, como threads, tendem a trabalhar contra esse determinismo; portanto, existem outras técnicas, como Tarefas, que melhoram as tendências não determinísticas do modelo de encadeamento. Coisas como geradores de números aleatórios e simuladores de Monte-Carlo também são determinísticos, mas de uma maneira diferente com base nas probabilidades.
Robert Harvey
3
@zzzzBov: Não considero a definição real e estrita de "puro" tão importante. Veja o segundo comentário, acima.
Robert Harvey
12

Bem, sim ... e não.

Uma função pura deve ter Transparência Referencial - ou seja, você deve poder substituir qualquer chamada possível para uma função pura pelo valor retornado, sem alterar o comportamento do programa. * É garantido que sua função sempre lança para determinados argumentos; portanto, não há valor de retorno para substituir a chamada de função; portanto, vamos ignorá- lo. Considere este código:

{
    var ignoreThis = func(arg);
}

Se funcfor puro, um otimizador pode decidir que func(arg)pode ser substituído por seu resultado. Ainda não sabe qual é o resultado, mas pode dizer que não está sendo usado - portanto, é possível deduzir que essa declaração não tem efeito e removê-la.

Mas se for func(arg)lançado, essa declaração faz alguma coisa - lança uma exceção! Portanto, o otimizador não pode removê-lo - importa se a função é chamada ou não.

Mas...

Na prática, isso importa muito pouco. Uma exceção - pelo menos em C # - é algo excepcional. Você não deve usá-lo como parte do seu fluxo de controle regular - você deve tentar capturá-lo e, se você capturar algo, lide com o erro para reverter o que estava fazendo ou de alguma forma ainda realizá-lo. Se o seu programa não funcionar corretamente porque um código que teria falhado foi otimizado, você está usando exceções erradas (a menos que seja um código de teste e quando você cria para testes, as exceções não devem ser otimizadas).

Dito isto ...

Não jogue exceções de funções puras com a intenção de que elas sejam capturadas - há um bom motivo para as linguagens funcionais preferirem usar mônadas em vez de exceções de desenrolamento de pilha.

Se o C # tivesse uma Errorclasse como Java (e muitas outras linguagens), eu teria sugerido lançar um em Errorvez de um Exception. Indica que o usuário da função fez algo errado (passou por uma função que lança) e tais coisas são permitidas em funções puras. Mas o C # não tem uma Errorclasse e as exceções de erro de uso parecem derivar Exception. Minha sugestão é lançar um ArgumentException, deixando claro que a função foi chamada com um argumento ruim.


* Falando computacionalmente. Uma função de Fibonacci implementada usando recursão ingênua levará muito tempo para grandes números e poderá esgotar os recursos da máquina, mas estes, com tempo e memória ilimitados, a função sempre retornará o mesmo valor e não terá efeitos colaterais (exceto alocar memória e alterá-la) - ainda é considerado puro.

Idan Arye
fonte
1
+1 pelo uso sugerido de ArgumentException e por sua recomendação de não "lançar exceções de funções puras com a intenção de que elas sejam capturadas".
Rock Anthony Johnson
Seu primeiro argumento (até o "Mas ...") me parece doentio. A exceção faz parte do contrato da função. Conceitualmente, você pode reescrever qualquer função (value, exception) = func(arg)e remover a possibilidade de gerar uma exceção (em alguns idiomas e para alguns tipos de exceções, você realmente precisa listá-la com a definição, o mesmo que argumentos ou valor de retorno). Agora, seria tão puro quanto antes (desde que sempre retorne aka lança uma exceção, dado o mesmo argumento). Lançar uma exceção não é um efeito colateral, se você usar essa interpretação.
AnoE
@AnoE Se você tivesse escrito funcdessa maneira, o bloco que eu dei poderia ter sido otimizado, porque, em vez de desenrolar a pilha, a exceção lançada seria codificada ignoreThis, o que ignoramos. Diferentemente das exceções que desenrolam a pilha, a exceção codificada no tipo de retorno não viola a pureza - e é por isso que muitas linguagens funcionais preferem usá-las.
Idan Arye
Exatamente ... você não teria ignorado a exceção para essa transformação imaginada. Eu acho que é tudo uma questão de semântica / definição, eu só queria ressaltar que a existência ou ocorrência de exceções não necessariamente anula todas as noções de pureza.
AnoE 27/10
1
@AnoE Mas o código que eu writted iria ignorar a exceção para esta transformação imaginado. func(arg)retornaria a tupla (<junk>, TheException())e, portanto ignoreThis, conterá a tupla (<junk>, TheException()), mas como nunca usamos ignoreThisa exceção, isso não terá efeito sobre nada.
Idan Arye
1

Uma consideração é que o bloco try-catch não é o problema. (Com base nos meus comentários para a pergunta acima).

O principal problema é que a propriedade do Norte é uma chamada de I / O .

Nesse ponto da execução do código, o programa precisa verificar a E / S fornecida pelo código do cliente. (Não seria relevante que a entrada esteja na forma de um delegado ou que a entrada já tenha sido nominalmente passada).

Depois de perder o controle da entrada, você não pode garantir que a função seja pura. (Especialmente se a função puder ser lançada).


Não sei ao certo por que você não deseja verificar a chamada para mover a sala [Verificar]? Conforme meu comentário à pergunta:

Sim. O que você faz ao executar o delegado só pode funcionar na primeira vez ou retornar uma sala diferente na segunda chamada? Ligar para o delegado pode ser considerado / causar um efeito colateral em si?

Como Bart van Ingen Schenau disse acima,

Sua função Criar não o protege de obter uma exceção ao obter a propriedade. Se o seu delegado jogar, na vida real, é muito provável que ele seja jogado apenas sob algumas condições. As chances são de que as condições de lançamento não estejam presentes durante a construção, mas estão presentes ao obter a propriedade.

Em geral, qualquer tipo de carregamento lento adia implicitamente os erros até esse ponto.


Eu sugeriria o uso de um método Move [Check] Room. Isso permitiria separar os aspectos impuros de E / S em um só lugar.

Semelhante à resposta de Robert Harvey :

Para torná-la uma função pura, retorne um objeto real que encapsule o valor esperado da função e um valor indicando uma possível condição de erro, como um objeto Talvez ou um objeto Unidade de Trabalho.

Caberia ao gravador de código determinar como lidar com a (possível) exceção da entrada. Em seguida, o método pode retornar um objeto Room, ou um objeto Null Room, ou talvez eliminar a exceção.

É neste ponto que depende:

  • O domínio da sala trata as exceções da sala como nulas ou como algo pior.
  • Como notificar o código do cliente que chama o Norte em uma Sala Nula / Exceção. (Fiança / Status variável / Estado global / Devolver uma mônada / Tanto faz; algumas são mais puras do que outras :)).
SH-
fonte
-1

Hmm ... eu não me sentia bem com isso. Eu acho que o que você está tentando fazer é garantir que outras pessoas façam seu trabalho corretamente, sem saber o que estão fazendo.

Uma vez que a função é passada pelo consumidor, o que pode ser escrito por você ou por outra pessoa.

E se a passagem da função não for válida para ser executada no momento em que o objeto Room for criado?

Pelo código, eu não podia ter certeza do que o Norte está fazendo.

Digamos que, se a função é reservar o quarto e precisar do horário e do período da reserva, você receberá uma exceção se não tiver as informações prontas.

E se for uma função de longa duração? Você não deseja bloquear seu programa ao criar um objeto de ambiente, e se precisar criar um objeto de 1000 ambientes?

Se você fosse a pessoa que escreveria o consumidor que consome essa classe, garantiria que a função passada fosse escrita corretamente, não é?

Se houver outra pessoa que escreva o consumidor, ele poderá não conhecer a condição "especial" de criar um objeto Room, o que poderia causar execuções e eles iriam coçar a cabeça tentando descobrir o porquê.

Outra coisa é que nem sempre é bom lançar uma nova exceção. A razão é que não conterá as informações da exceção original. Você sabe que obtém um erro (uma mesa amigável para exceção genérica), mas não saberia qual é o erro (referência nula, índice fora do limite, desvio zero etc). Essa informação é muito importante quando precisamos investigar.

Você deve lidar com a exceção somente quando souber o que e como lidar com ela.

Eu acho que o seu código original é bom o suficiente, a única coisa é que você pode querer lidar com a exceção no "Main" (ou qualquer camada que saiba como lidar com isso).

user251630
fonte
1
Veja novamente o segundo exemplo de código; Eu inicializo a nova exceção com a exceção mais baixa. Todo o rastreamento da pilha é preservado.
Rock Anthony Johnson
Opa Meu erro.
user251630
-1

Discordo das outras respostas e direi não . Exceções não fazem com que uma função pura se torne impura.

Qualquer uso de exceções pode ser reescrito para usar verificações explícitas para obter resultados de erro. Além disso, as exceções podem ser consideradas açúcar sintático. O açúcar sintático conveniente não torna impuro o código puro.

JacquesB
fonte
1
O fato de você poder reescrever a impureza não torna a função pura. Haskell é uma prova de que o uso de funções impuras pode ser reescrito com funções puras - ele usa mônadas para codificar qualquer comportamento impuro nos tipos de retorno funções puras puras. A existência de mônadas de IO não implica que IO regular seja pura - por que a existência de mônadas de exceção implica que exceções regulares são puras?
Idan Arye
@IdanArye: Estou argumentando que não há impureza em primeiro lugar. O exemplo de reescrita foi apenas para demonstrar isso. A E / S regular é impura porque causa efeitos colaterais observáveis ​​ou pode retornar valores diferentes com a mesma entrada devido a alguma entrada externa. Lançar uma exceção não faz nada disso.
JacquesB
O código livre de efeitos colaterais não é suficiente. A transparência referencial também é um requisito para a pureza da função.
Idan Arye
1
O determinismo não é suficiente para a transparência referencial - os efeitos colaterais também podem ser determinísticos. Transparência referencial significa que a expressão pode ser substituída por seus resultados - portanto, não importa quantas vezes você avalie expressões transparentes referenciais, em que ordem e se você as avalia ou não - o resultado deve ser o mesmo. Se uma exceção é lançada deterministicamente, somos bons com os critérios "não importa quantas vezes", mas não com os outros. Eu argumentei sobre os "ou não" critérios em minha própria resposta, (cont ...)
Idan Arye
1
(... cont) e quanto ao "em que ordem" - se fe gsão ambas funções puras, f(x) + g(x)deve ter o mesmo resultado, independentemente da ordem em que você avalia f(x)e g(x). Mas se f(x)joga Fe g(x)joga G, a exceção lançada dessa expressão seria Fse f(x)for avaliada primeiro e Gse g(x)for avaliada primeiro - não é assim que as funções puras devem se comportar! Quando a exceção é codificada no tipo de retorno, esse resultado acabaria como algo Error(F) + Error(G)independente da ordem de avaliação.
Idan Arye