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 ()?
c#
functional-programming
pure-function
Rock Anthony Johnson
fonte
fonte
Create
também é impuro, porque chama.Create
funçã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.Respostas:
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,
return
um objeto real que encapsula o valor esperado da função e um valor indicando uma possível condição de erro, como umMaybe
objeto ou um objeto de Unidade de Trabalho.fonte
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:
Se
func
for puro, um otimizador pode decidir quefunc(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
Error
classe como Java (e muitas outras linguagens), eu teria sugerido lançar um emError
vez de umException
. 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 umaError
classe e as exceções de erro de uso parecem derivarException
. Minha sugestão é lançar umArgumentException
, 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.
fonte
(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.func
dessa maneira, o bloco que eu dei poderia ter sido otimizado, porque, em vez de desenrolar a pilha, a exceção lançada seria codificadaignoreThis
, 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.func(arg)
retornaria a tupla(<junk>, TheException())
e, portantoignoreThis
, conterá a tupla(<junk>, TheException())
, mas como nunca usamosignoreThis
a exceção, isso não terá efeito sobre nada.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:
Como Bart van Ingen Schenau disse acima,
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 :
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:
fonte
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).
fonte
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.
fonte
f
eg
são ambas funções puras,f(x) + g(x)
deve ter o mesmo resultado, independentemente da ordem em que você avaliaf(x)
eg(x)
. Mas sef(x)
jogaF
eg(x)
jogaG
, a exceção lançada dessa expressão seriaF
sef(x)
for avaliada primeiro eG
seg(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 algoError(F) + Error(G)
independente da ordem de avaliação.