Este método é puro?

9

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 Pureatributo (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 Applymé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?

Thomas Levesque
fonte

Respostas:

15

Não, não é puro, porque tem efeito colateral. Concretamente, está chamando actioncada 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

Um método puro pode modificar objetos que foram criados após a entrada no método puro.

Vamos dizer que criamos um método como este:

int Count<T>(IEnumerable<T> e)
{
    var enumerator = e.GetEnumerator();
    int count = 0;
    while (enumerator.MoveNext()) count ++;
    return count;
}

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 o ApplyIterator, o que deve resultar em pura função, certo?

Count(ApplyIterator(source, action));

Não. Esta composição não é pura, mesmo quando ambos Counte ApplyIteratorsã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.

Eufórico
fonte
11
A pureza da função +1 não é uma questão de prós ou contras. A pureza da função é uma dica sobre uso e segurança. Estranhamente, o OP colocou where T : class, no entanto, se o OP simplesmente o colocasse where T : strut, seria puro.
ArTs
4
Eu discordo desta resposta. Chamar sequence.Apply(action)não tem efeito colateral; se houver, indique o efeito colateral que possui. Agora, chamar sequence.Apply(action).GetEnumerator().MoveNext()tem um efeito colateral, mas já sabíamos disso; muda o enumerador! Por que deve sequence.Apply(action)ser considerado impuro porque o chamado MoveNexté impuro, mas sequence.Where(predicate)é considerado puro? sequence.Where(predicate).GetEnumerator().MoveNext()é tão impuro.
Eric Lippert
@EricLippert Você levanta um bom argumento. Mas, não seria suficiente apenas chamar GetEnumerator? Podemos considerar isso puro?
Euphoric
@Euphoric: Que efeito colateral observável a chamada GetEnumeratorproduz, além de alocar um enumerador em seu estado inicial?
Eric Lippert
11
@EricLippert Então, por que Enumerable.Count é considerado puro pelos contratos de código do .NET? Não tenho link, mas quando toco no visual studio, recebo um aviso quando uso a contagem não pura personalizada, mas o contrato funciona muito bem com Enumerable.Count.
Euphoric
18

Não concordo com as respostas de Eufórico e de Robert Harvey . Absolutamente essa é uma função pura; o problema é que

Apenas aplica uma ação a cada item da sequência antes de devolvê-lo.

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 MoveNextdo enumerador da sequência faz isso e "retorna" o item por meio da Currentpropriedade, 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, se MoveNextfor 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

foreach(var item in sequence) action(item);

o que é bem claro.

Eric Lippert
fonte
2
Eu acho que este método cai no mesmo saco como o ForEachmétodo de extensão, que intencionalmente não faz parte do Linq porque o seu objetivo é produzir efeitos secundários ...
Thomas Levesque
11
@ ThomasLevesque: Meu conselho é nunca fazer isso . Uma consulta deve responder a uma pergunta , não alterar uma sequência ; é por isso que eles são chamados de consultas . A mutação da sequência conforme é consultada é extraordinariamente perigosa . Considere, por exemplo, o que acontece se essa consulta for sujeita a várias chamadas ao 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 um IEnumerable<Action>.
Eric Lippert
2
Essa resposta confunde as águas mais do que ilumina. Embora tudo o que você diga seja inquestionavelmente verdadeiro, os princípios de imutabilidade e pureza são princípios de linguagem de programação de alto nível, não detalhes de implementação de baixo nível. Os programadores que trabalham no nível funcional estão interessados ​​em como seu código se comporta no nível funcional, não se o seu funcionamento interno é puro ou não . Eles quase certamente não são puros sob o capô, se você for baixo o suficiente. Todos nós geralmente executamos essas coisas na arquitetura Von Neumann, que certamente não é pura.
Robert Harvey
2
@ Thomashoding: O método não chama action, então a pureza de actioné irrelevante. Eu sei que parece que ele chama action, mas esse método é um açúcar sintático para dois métodos, um que retorna um enumerador e outro que é o MoveNextdo enumerador. O primeiro é claramente puro, e o segundo claramente não é. Veja o seguinte: você diria que IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }é puro? Porque essa é a função que isso realmente é.
precisa saber é o seguinte
11
@ Thomashoding: Está faltando alguma coisa; não é assim que os iteradores funcionam. O ApplyIteratormétodo retorna imediatamente . Nenhum código no corpo de ApplyIteratoré executado até a primeira chamada MoveNextno 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 /…
Eric Lippert
3

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.

Robert Harvey
fonte
Bem, ele não modifica a coleção original, pois apenas retorna uma nova sequência com os mesmos itens; é por isso que eu estava pensando em torná-lo puro. Mas isso pode alterar o estado dos itens quando você enumera o resultado.
Thomas Levesque
Se itemé um tipo de referência, está modificando a coleção original, mesmo que você esteja retornando itemem um iterador. Veja stackoverflow.com/questions/1538301
Robert Harvey
11
Mesmo se ele copiasse a coleção em profundidade, ela ainda não seria pura, pois actionpode ter efeitos colaterais além de modificar o item passado para ela.
Idan Arye
@IdanArye: É verdade que a ação também teria que ser pura.
Robert Harvey
11
@IdanArye: ()=>{}é 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.
Eric Lippert 06/06
0

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:

Test

Test2

Após a primeira execução, a lista terá o seguinte:

Test Changed

Test2 Changed

E é a terceira vez:

Test Changed Changed

Test2 Changed Changed

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.

Paulo Zemek
fonte