Eu tenho o seguinte método de extensão:
public static IEnumerable<T> Apply<T>(
[NotNull] this IEnumerable<T> source,
[NotNull] Action<T> action)
where T : class
{
source.CheckArgumentNull("source");
action.CheckArgumentNull("action");
return source.ApplyIterator(action);
}
private static IEnumerable<T> ApplyIterator<T>(this IEnumerable<T> source, Action<T> action)
where T : class
{
foreach (var item in source)
{
action(item);
yield return item;
}
}
Apenas aplica uma ação a cada item da sequência antes de devolvê-lo.
Eu queria saber se eu deveria aplicar o Pure
atributo (das anotações do Resharper) a esse método, e posso ver argumentos a favor e contra.
Prós:
- estritamente falando, é puro; apenas chamá-lo em uma sequência não altera a sequência (retorna uma nova sequência) ou faz qualquer alteração de estado observável
- chamá-lo sem usar o resultado é claramente um erro, pois não tem efeito, a menos que a sequência seja enumerada, portanto, gostaria que o Resharper me avisasse se eu fizer isso.
Contras:
- mesmo que o
Apply
método em si seja puro, enumerar a sequência resultante fará alterações de estado observáveis (que é o ponto do método). Por exemplo,items.Apply(i => i.Count++)
alterará os valores dos itens toda vez que forem enumerados. Portanto, a aplicação do atributo Pure é provavelmente enganosa ...
O que você acha? Devo aplicar o atributo ou não?
c#
pure-function
Thomas Levesque
fonte
fonte
Respostas:
Não, não é puro, porque tem efeito colateral. Concretamente, está chamando
action
cada item. Além disso, não é seguro para threads.A principal propriedade das funções puras é que ela pode ser chamada inúmeras vezes e nunca faz nada além de retornar o mesmo valor. Qual não é o seu caso? Além disso, ser puro significa que você não usa nada além dos parâmetros de entrada. Isso significa que pode ser chamado de qualquer thread a qualquer momento e não causar nenhum comportamento inesperado. Novamente, esse não é o caso de sua função.
Além disso, você pode estar enganado em uma coisa: a pureza da função não é uma questão de prós ou contras. Mesmo uma única dúvida, que pode ter efeito colateral, é suficiente para torná-lo não puro.
Eric Lippert levanta um bom ponto. Vou usar http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx como parte do meu contra-argumento. Especialmente linha
Vamos dizer que criamos um método como este:
Primeiro, isso pressupõe que isso
GetEnumerator
é puro também (eu realmente não consigo encontrar nenhuma fonte nisso). Se for, de acordo com a regra acima, podemos anotar esse método com [Pure], porque ele modifica apenas a instância que foi criada dentro do próprio corpo. Depois disso, podemos compor isso e oApplyIterator
, o que deve resultar em pura função, certo?Não. Esta composição não é pura, mesmo quando ambos
Count
eApplyIterator
são puros. Mas eu posso estar construindo esse argumento com premissa errada. Penso que a ideia de que instâncias criadas dentro do método estão isentas da regra de pureza está errada ou, pelo menos, não é específica o suficiente.fonte
where T : class
, no entanto, se o OP simplesmente o colocassewhere T : strut
, seria puro.sequence.Apply(action)
não tem efeito colateral; se houver, indique o efeito colateral que possui. Agora, chamarsequence.Apply(action).GetEnumerator().MoveNext()
tem um efeito colateral, mas já sabíamos disso; muda o enumerador! Por que devesequence.Apply(action)
ser considerado impuro porque o chamadoMoveNext
é impuro, massequence.Where(predicate)
é considerado puro?sequence.Where(predicate).GetEnumerator().MoveNext()
é tão impuro.GetEnumerator
produz, além de alocar um enumerador em seu estado inicial?Não concordo com as respostas de Eufórico e de Robert Harvey . Absolutamente essa é uma função pura; o problema é que
não está muito claro o que significa o primeiro "isso". Se "it" significa uma dessas funções, isso não está certo; nenhuma dessas funções faz isso; o
MoveNext
do enumerador da sequência faz isso e "retorna" o item por meio daCurrent
propriedade, não retornando.Essas seqüências são enumeradas preguiçosamente , não com muita ansiedade, portanto, certamente não é o caso de a ação ser aplicada antes que a sequência seja retornada
Apply
. A ação é aplicada após o retorno da sequência, seMoveNext
for chamada em um enumerador.Como você observa, essas funções executam uma ação e uma sequência e retornam uma sequência; a saída depende da entrada e nenhum efeito colateral é produzido; portanto, são funções puras.
Agora, se você criar um enumerador da sequência resultante e chamar MoveNext nesse iterador, o método MoveNext não será puro, porque chama a ação e produz um efeito colateral. Mas já sabíamos que o MoveNext não era puro porque modifica o enumerador!
Agora, quanto à sua pergunta, você deve aplicar o atributo: Eu não aplicaria o atributo porque não escreveria esse método em primeiro lugar . Se eu quiser aplicar uma ação a uma sequência, escrevo
o que é bem claro.
fonte
ForEach
método de extensão, que intencionalmente não faz parte do Linq porque o seu objetivo é produzir efeitos secundários ...Any()
longo do tempo; a ação será executada repetidamente, mas apenas no primeiro item! Uma sequência deve ser uma sequência de valores ; Se você deseja uma sequência de ações , faça umIEnumerable<Action>
.action
, então a pureza deaction
é irrelevante. Eu sei que parece que ele chamaaction
, mas esse método é um açúcar sintático para dois métodos, um que retorna um enumerador e outro que é oMoveNext
do enumerador. O primeiro é claramente puro, e o segundo claramente não é. Veja o seguinte: você diria queIEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }
é puro? Porque essa é a função que isso realmente é.ApplyIterator
método retorna imediatamente . Nenhum código no corpo deApplyIterator
é executado até a primeira chamadaMoveNext
no enumerador do objeto retornado. Agora que você sabe disso, pode deduzir a resposta para este quebra-cabeça: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… A resposta está aqui: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…Não é uma função pura, portanto, a aplicação do atributo Pure é enganosa.
As funções puras não modificam a coleção original e não importa se você está passando uma ação que não tem efeito ou não; ainda é uma função impura porque sua intenção é causar efeitos colaterais.
Se você deseja tornar a função pura, copie a coleção para uma nova coleção, aplique as alterações que a Ação realiza na nova coleção e retorne a nova coleção, mantendo a coleção original inalterada.
fonte
item
é um tipo de referência, está modificando a coleção original, mesmo que você esteja retornandoitem
em um iterador. Veja stackoverflow.com/questions/1538301action
pode ter efeitos colaterais além de modificar o item passado para ela.()=>{}
é conversível em Ação, e é uma função pura. Suas saídas dependem apenas de suas entradas e não tem efeitos colaterais observáveis.Na minha opinião, o fato de receber uma ação (e não algo como PureAction) não a torna pura.
E eu até discordo de Eric Lippert. Ele escreveu que "() => {} é conversível em Ação, e é uma função pura. Suas saídas dependem apenas de suas entradas e não têm efeitos colaterais observáveis".
Bem, imagine que, em vez de usar um delegado, o ApplyIterator estivesse invocando um método chamado Action.
Se Ação for pura, o ApplyIterator também será puro. Se a Ação não for pura, o ApplyIterator não poderá ser puro.
Considerando o tipo de delegado (não o valor real fornecido), não temos a garantia de que seja puro; portanto, o método se comportará como um método puro somente quando o delegado for puro. Portanto, para torná-lo realmente puro, ele deve receber um delegado puro (e isso existe, podemos declarar um delegado como [Puro], para que possamos ter um PureAction).
Explicando de maneira diferente, um método Pure sempre deve fornecer o mesmo resultado, com as mesmas entradas e não deve gerar mudanças observáveis. O ApplyIterator pode receber a mesma fonte e delegar duas vezes, mas, se o delegado estiver alterando um tipo de referência, a próxima execução fornecerá resultados diferentes. Exemplo: O delegado faz algo como item.Content + = "Changed";
Portanto, usando o ApplyIterator sobre uma lista de "contêineres de strings" (um objeto com uma propriedade Content do tipo string), podemos ter estes valores originais:
Após a primeira execução, a lista terá o seguinte:
E é a terceira vez:
Portanto, estamos alterando o conteúdo da lista porque o delegado não é puro e nenhuma otimização pode ser feita para evitar a execução da chamada 3 vezes se invocada 3 vezes, pois cada execução gera um resultado diferente.
fonte