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 Pure
atributo (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
Apply
mé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?
c#
pure-function
Thomas Levesque
fuente
fuente
Respuestas:
No, no es puro, porque tiene efectos secundarios. Concretamente está llamando
action
a 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
Digamos que creamos un método como este:
Primero, esto supone que también
GetEnumerator
es 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 elApplyIterator
, lo que debería resultar en una función pura, ¿verdad?No. Esta composición no es pura, incluso cuando ambas
Count
yApplyIterator
son 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.fuente
where T : class
, sin embargo, si el OP simplemente lo pusierawhere T : strut
SERÍA puro.sequence.Apply(action)
no tiene efectos secundarios; si es así, indique el efecto secundario que tiene. Ahora, llamarsequence.Apply(action).GetEnumerator().MoveNext()
tiene un efecto secundario, pero eso ya lo sabíamos; ¡Muta el enumerador! ¿Por qué deberíasequence.Apply(action)
considerarse impuro porque llamarMoveNext
es impuro, perosequence.Where(predicate)
considerarse puro?sequence.Where(predicate).GetEnumerator().MoveNext()
es tan impuro.GetEnumerator
, además de asignar un enumerador en su estado inicial?No estoy de acuerdo con las respuestas de Euphoric y Robert Harvey . Absolutamente esa es una función pura; el problema es ese
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
MoveNext
del enumerador de la secuencia hace eso y "devuelve" el elemento a través de laCurrent
propiedad, 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, siMoveNext
se 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
Que está muy bien claro.
fuente
ForEach
método de extensión, que intencionalmente no forma parte de Linq porque su objetivo es producir efectos secundarios ...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 unIEnumerable<Action>
.action
, por lo que la pureza deaction
es irrelevante. Sé que parece que llamaaction
, pero este método es un azúcar sintáctico para dos métodos, uno que devuelve un enumerador y otro que es elMoveNext
del enumerador. El primero es claramente puro, y el segundo claramente no lo es. Míralo de esta manera: ¿dirías queIEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }
es puro? Porque esa es la función que realmente es.ApplyIterator
método vuelve inmediatamente . NoApplyIterator
se ejecuta ningún código en el cuerpo de hasta la primera llamada alMoveNext
enumerador 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 /…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.
fuente
item
es un tipo de referencia, está modificando la colección original, incluso si está regresandoitem
en un iterador. Ver stackoverflow.com/questions/1538301action
puede tener otros efectos secundarios además de modificar el elemento que se le pasó.()=>{}
es convertible a Acción, y es una función pura. Sus salidas dependen únicamente de sus entradas y no tiene efectos secundarios observables.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:
Después de la primera ejecución, la lista tendrá esto:
Y esta la tercera vez:
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.
fuente