¿Es puro este método?

9

Tengo el siguiente método de extensión:

    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;
        }
    }

Simplemente aplica una acción a cada elemento de la secuencia antes de devolverlo.

Me preguntaba si debería aplicar el Pureatributo (de las anotaciones de Resharper) a este método, y puedo ver argumentos a favor y en contra.

Pros:

  • estrictamente hablando, es puro; simplemente llamarlo en una secuencia no altera la secuencia (devuelve una nueva secuencia) ni realiza ningún cambio de estado observable
  • llamarlo sin usar el resultado es claramente un error, ya que no tiene ningún efecto a menos que se enumere la secuencia, por lo que me gustaría que Resharper me avise si hago eso.

Contras:

  • a pesar de que el Applymétodo en sí es puro, enumerar la secuencia resultante hará cambios de estado observables (que es el punto del método). Por ejemplo, items.Apply(i => i.Count++)cambiará los valores de los elementos cada vez que se enumeren. Entonces, aplicar el atributo Pure es probablemente engañoso ...

¿Qué piensas? ¿Debo aplicar el atributo o no?

Thomas Levesque
fuente

Respuestas:

15

No, no es puro, porque tiene efectos secundarios. Concretamente está llamando actiona cada elemento. Además, no es seguro para subprocesos.

La propiedad principal de las funciones puras es que se puede llamar cualquier número de veces y nunca hace nada más que devolver el mismo valor. Que no es tu caso. Además, ser puro significa que no usa nada más que los parámetros de entrada. Esto significa que se puede invocar desde cualquier subproceso en cualquier momento y no causar ningún comportamiento inesperado. De nuevo, ese no es el caso de su función.

Además, puede estar equivocado en una cosa: la pureza de la función no es una cuestión de pros o contras. Incluso una sola duda, que puede tener efectos secundarios, es suficiente para que no sea pura.

Eric Lippert plantea un buen punto. Voy a usar http://msdn.microsoft.com/en-us/library/dd264808(v=vs.110).aspx como parte de mi contraargumento. Especialmente linea

Un método puro puede modificar los objetos que se han creado después de ingresar al método puro.

Digamos que creamos un método como este:

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

Primero, esto supone que también GetEnumeratores puro (realmente no puedo encontrar ninguna fuente sobre eso). Si es así, de acuerdo con la regla anterior, podemos anotar este método con [Puro], porque solo modifica la instancia que se creó dentro del propio cuerpo. Después de eso podemos componer esto y el ApplyIterator, lo que debería resultar en una función pura, ¿verdad?

Count(ApplyIterator(source, action));

No. Esta composición no es pura, incluso cuando ambas County ApplyIteratorson puras. Pero podría estar construyendo este argumento sobre premisas equivocadas. Creo que la idea de que las instancias creadas dentro del método están exentas de la regla de pureza es incorrecta o al menos no lo suficientemente específica.

Eufórico
fuente
1
La función de pureza +1 no es una cuestión de pros o contras. La pureza de la función es una pista sobre el uso y la seguridad. Por extraño que parezca, el OP entró where T : class, sin embargo, si el OP simplemente lo pusiera where T : strutSERÍA puro.
ArTs
44
No estoy de acuerdo con esta respuesta. Llamar sequence.Apply(action)no tiene efectos secundarios; si es así, indique el efecto secundario que tiene. Ahora, llamar sequence.Apply(action).GetEnumerator().MoveNext()tiene un efecto secundario, pero eso ya lo sabíamos; ¡Muta el enumerador! ¿Por qué debería sequence.Apply(action)considerarse impuro porque llamar MoveNextes impuro, pero sequence.Where(predicate)considerarse puro? sequence.Where(predicate).GetEnumerator().MoveNext()es tan impuro.
Eric Lippert
@EricLippert Usted plantea un buen punto. Pero, ¿no sería suficiente simplemente llamar a GetEnumerator? ¿Podemos considerar eso puro?
Eufórico
@Euphoric: ¿Qué efecto secundario observable produce la llamada GetEnumerator, además de asignar un enumerador en su estado inicial?
Eric Lippert
1
@EricLippert Entonces, ¿por qué Enumerable.Count es considerado puro por los contratos de código de .NET? No tengo enlace, pero cuando juego con él en Visual Studio, recibo una advertencia cuando uso un conteo no puro personalizado, pero el contrato funciona bien con Enumerable.Count.
Eufórico
18

No estoy de acuerdo con las respuestas de Euphoric y Robert Harvey . Absolutamente esa es una función pura; el problema es ese

Simplemente aplica una acción a cada elemento de la secuencia antes de devolverlo.

No está muy claro qué significa el primer "eso". Si "eso" significa una de esas funciones, entonces eso no está bien; ninguna de esas funciones hace eso; el MoveNextdel enumerador de la secuencia hace eso y "devuelve" el elemento a través de la Currentpropiedad, no devolviéndolo.

Esas secuencias se enumeran perezosamente , no con entusiasmo, por lo que ciertamente no es el caso que la acción se aplique antes de que la secuencia sea devuelta por Apply. La acción se aplica después de que se devuelve la secuencia, si MoveNextse llama en un enumerador.

Como observa, estas funciones toman una acción y una secuencia y devuelven una secuencia; la salida depende de la entrada y no se producen efectos secundarios, por lo que estas son funciones puras.

Ahora, si crea un enumerador de la secuencia resultante y luego llama a MoveNext en ese iterador, entonces el método MoveNext no es puro, porque llama a la acción y produce un efecto secundario. ¡Pero ya sabíamos que MoveNext no era puro porque muta el enumerador!

Ahora, en cuanto a su pregunta, si aplica el atributo: no aplicaría el atributo porque no escribiría este método en primer lugar . Si quiero aplicar una acción a una secuencia, entonces escribo

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

Que está muy bien claro.

Eric Lippert
fuente
2
Supongo que este método cae en la misma bolsa que el ForEachmétodo de extensión, que intencionalmente no forma parte de Linq porque su objetivo es producir efectos secundarios ...
Thomas Levesque
1
@ThomasLevesque: Mi consejo es que nunca hagas eso . Una consulta debe responder una pregunta , no mutar una secuencia ; Por eso se llaman consultas . La mutación de la secuencia a medida que se consulta es extraordinariamente peligrosa . Considere, por ejemplo, lo que sucede si dicha consulta se somete a múltiples llamadas a lo Any()largo del tiempo; la acción se realizará una y otra vez, ¡pero solo en el primer elemento! Una secuencia debe ser una secuencia de valores ; si quieres una secuencia de acciones entonces haz un IEnumerable<Action>.
Eric Lippert
2
Esta respuesta enturbia las aguas más de lo que ilumina. Si bien todo lo que dice es indudablemente cierto, los principios de inmutabilidad y pureza son principios de lenguaje de programación de alto nivel, no detalles de implementación de bajo nivel. Los programadores que trabajan a nivel funcional están interesados ​​en cómo se comporta su código a nivel funcional, no en si su funcionamiento interno es puro o no . Es casi seguro que no son puros si se baja lo suficiente. En general, todos ejecutamos estas cosas en la arquitectura de Von Neumann, que ciertamente no es pura.
Robert Harvey
2
@ThomasEding: el método no llama action, por lo que la pureza de actiones irrelevante. Sé que parece que llama action, pero este método es un azúcar sintáctico para dos métodos, uno que devuelve un enumerador y otro que es el MoveNextdel enumerador. El primero es claramente puro, y el segundo claramente no lo es. Míralo de esta manera: ¿dirías que IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }es puro? Porque esa es la función que realmente es.
Eric Lippert
1
@ThomasEding: Te falta algo; no es así como funcionan los iteradores. El ApplyIteratormétodo vuelve inmediatamente . No ApplyIteratorse ejecuta ningún código en el cuerpo de hasta la primera llamada al MoveNextenumerador del objeto devuelto. Ahora que lo sabe, puede deducir la respuesta a este rompecabezas: blogs.msdn.com/b/ericlippert/archive/2007/09/05/… La respuesta está aquí: blogs.msdn.com/b/ericlippert/archive / 2007/09/06 /…
Eric Lippert
3

No es una función pura, por lo que aplicar el atributo Pure es engañoso.

Las funciones puras no modifican la colección original, y no importa si está pasando una acción que no tiene efecto o no; sigue siendo una función impura porque su intención es causar efectos secundarios.

Si desea que la función sea pura, copie la colección a una nueva colección, aplique los cambios que la Acción realiza a la nueva colección y devuelva la nueva colección, dejando la colección original sin cambios.

Robert Harvey
fuente
Bueno, no modifica la colección original, ya que solo devuelve una nueva secuencia con los mismos elementos; Por eso estaba considerando hacerlo puro. Pero podría cambiar el estado de los elementos cuando enumera el resultado.
Thomas Levesque
Si itemes un tipo de referencia, está modificando la colección original, incluso si está regresando itemen un iterador. Ver stackoverflow.com/questions/1538301
Robert Harvey
1
Incluso si copió en profundidad la colección, todavía no sería pura, ya que actionpuede tener otros efectos secundarios además de modificar el elemento que se le pasó.
Idan Arye
@IdanArye: Cierto, la Acción también debería ser pura.
Robert Harvey
1
@IdanArye: ()=>{}es convertible a Acción, y es una función pura. Sus salidas dependen únicamente de sus entradas y no tiene efectos secundarios observables.
Eric Lippert
0

En mi opinión, el hecho de que reciba una Acción (y no algo así como PureAction) hace que no sea pura.

E incluso estoy en desacuerdo con Eric Lippert. Escribió que "() => {} es convertible a Acción, y es una función pura. Sus salidas dependen únicamente de sus entradas y no tiene efectos secundarios observables".

Bueno, imagine que en lugar de usar un delegado, ApplyIterator invoca un método llamado Acción.

Si Action es puro, entonces ApplyIterator también es puro. Si Action no es puro, entonces ApplyIterator no puede ser puro.

Teniendo en cuenta el tipo de delegado (no el valor dado real), no tenemos la garantía de que sea puro, por lo que el método se comportará como un método puro solo cuando el delegado sea puro. Entonces, para hacerlo realmente puro, debe recibir un delegado puro (y eso existe, podemos declarar un delegado como [Puro], para que podamos tener una PureAction).

Explicando de manera diferente, un método Pure siempre debe dar el mismo resultado con las mismas entradas y no debe generar cambios observables. ApplyIterator puede recibir la misma fuente y delegar dos veces pero, si el delegado está cambiando un tipo de referencia, la próxima ejecución dará resultados diferentes. Ejemplo: el delegado hace algo como item.Content + = "Modificado";

Entonces, usando el ApplyIterator sobre una lista de "contenedores de cadenas" (un objeto con una propiedad Content de tipo string), podemos tener estos valores originales:

Test

Test2

Después de la primera ejecución, la lista tendrá esto:

Test Changed

Test2 Changed

Y esta la tercera vez:

Test Changed Changed

Test2 Changed Changed

Por lo tanto, estamos cambiando el contenido de la lista porque el delegado no es puro y no se puede hacer una optimización para evitar ejecutar la llamada 3 veces si se invoca 3 veces, ya que cada ejecución generará un resultado diferente.

Paulo Zemek
fuente