这种方法纯吗?


9

我有以下扩展方法:

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

它只是在返回序列之前对序列的每个项目执行操作。

我想知道是否应该将Pure属性(来自Resharper注释)应用于此方法,并且可以看到支持和反对该参数的参数。

优点:

  • 严格来说,它纯净的;仅在序列上调用它不会更改序列(它返回新序列)或进行任何可观察的状态更改
  • 在不使用结果的情况下调用它显然是一个错误,因为除非枚举序列,否则它没有任何作用,所以我希望Resharper在这样做时向我发出警告。

缺点:

  • 即使Apply方法本身是纯方法,枚举结果序列会使状态发生可观察的变化(这是方法的重点)。例如,items.Apply(i => i.Count++)将在每次枚举时更改项目的值。因此,应用Pure属性可能会误导...

你怎么看?是否应该应用该属性?


Answers:


15

不,它不是纯净的,因为它有副作用。具体来说,它正在调用action每个项目。另外,它也不是线程安全的。

纯函数的主要特性是可以调用任意次,并且除了返回相同的值外,它不会做任何其他事情。这不是你的情况。同样,纯正意味着除了输入参数外,您不使用其他任何东西。这意味着可以随时从任何线程调用它,而不会引起任何意外行为。同样,您的功能也不是这种情况。

同样,您可能会误认为一件事:函数纯度不是优缺点的问题。即使是唯一的怀疑,即它可能有副作用,也足以使其不纯净。

埃里克·利珀特(Eric Lippert)提出了一个很好的观点。我将使用http://msdn.microsoft.com/zh-CN/library/dd264808(v=vs.110).aspx作为我的反论证的一部分。特别是线

允许使用纯方法修改进入纯方法后创建的对象。

可以说我们创建这样的方法:

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

首先,这也假设GetEnumerator是纯粹的(我真的找不到关于它的任何资料)。如果是,则根据上述规则,我们可以用[Pure]注释此方法,因为它仅修改在主体本身内部创建的实例。之后,我们可以组成和和ApplyIterator,这应该产生纯函数,对吗?

Count(ApplyIterator(source, action));

号这组合物不是纯的,甚至当两个CountApplyIterator是纯的。但是我可能在错误的前提下建立了这种观点。我认为,在方法中创建的实例不受纯度规则约束的想法是错误的,或者至少不够具体。


1
+1函数纯度不是正反问题。功能纯度是使用和安全性的暗示。OP放入的奇怪程度足够高where T : class,但是如果OP简单地将where T : strut其放入纯净状态。
2014年

4
我不同意这个答案。通话sequence.Apply(action)没有副作用。如果有,请说明其具有的副作用。现在,打电话sequence.Apply(action).GetEnumerator().MoveNext()有副作用,但是我们已经知道了。它改变了枚举器!为什么sequence.Apply(action)由于调用MoveNext不纯而sequence.Where(predicate)被认为不纯,却被认为是纯洁的? sequence.Where(predicate).GetEnumerator().MoveNext()一点都不一样。
埃里克·利珀特

@EricLippert您提出了一个很好的观点。但是,仅调用GetEnumerator不够吗?我们可以认为那是纯洁的吗?
欣快感2014年

@Euphoric:GetEnumerator除了在初始状态分配枚举数之外,调用还会产生什么可观察到的副作用?
埃里克·利珀特

1
@EricLippert那么,为什么.NET的代码合同将Enumerable.Count视为Pure?我没有链接,但是当我在Visual Studio中使用它时,当我使用自定义非纯计数时会收到警告,但该合同对Enumerable.Count来说效果很好。
欣快2014年

18

我不同意欣快罗伯特·哈维的回答。绝对是纯函数;问题是

它只是在返回序列之前对序列的每个项目执行操作。

尚不清楚第一个“它”是什么意思。如果“它”表示这些功能之一,那是不正确的;这些功能都没有这样做;该MoveNext序列的枚举的这样做,而且“收益”通过的项目Current属性,而不是通过返回它。

这些序列是延迟枚举的,不是急切的,因此在序列返回之前肯定不采取行动Apply。如果在枚举器上调用,则在返回序列应用该操作MoveNext

如您所述,这些函数执行一个动作和一个序列,然后返回一个序列。输出取决于输入,并且不会产生副作用,因此这些都是纯函数。

现在,如果您创建结果序列的枚举器,然后在该迭代器上调用MoveNext,则MoveNext方法不是纯函数,因为它调用动作并产生副作用。但是我们已经知道MoveNext并不是纯粹的,因为它会变异枚举器!

现在,关于您的问题,您是否应该应用属性:我不会应用属性,因为我不会首先编写此方法。如果我想对序列应用动作,那么我写

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

这很清楚。


2
我猜想这种方法和ForEach扩展方法在同一个包中,这不是Linq的一部分,因为它的目标是产生副作用...
Thomas Levesque 2014年

1
@ThomasLevesque:我的建议是永远不要这样做。查询应该回答一个问题,而不是改变序列 ; 这就是为什么它们被称为query的原因。在查询序列时对其进行突变是非常危险的。例如,考虑一下,如果这样的查询Any()随时间经过多次调用会发生什么情况?该动作将一次又一次地执行,但仅限于第一项!一个序列应该是一个序列; 如果您要执行一系列操作,请进行操作IEnumerable<Action>
埃里克·利珀特

2
这个答案使水比照亮的更多。尽管您说的一切都是正确的,但不变性和纯净性的原则是高级编程语言原则,而不是低级实现细节。在功能级别上工作的程序员对他们的代码在功能级别上的行为方式感兴趣而不是对其内部工作是否纯净感兴趣如果降低得足够低, 它们几乎肯定不是真正的引擎盖。我们通常都在冯·诺依曼(Von Neumann)架构上运行这些东西,这当然不是纯粹的。
罗伯特·哈维

2
@ThomasEding:该方法不会调用action,因此的纯度action无关紧要。我知道它看起来像它调用的一样action,但是此方法是两种方法的语法糖,一种返回一个枚举器,另一种是MoveNext枚举的。前者显然是纯粹的,而后者显然不是。这样看:你说那IEnumerable ApplyIterator(whatever) { return new MyIterator(whatever); }是纯粹的吗?因为那才是真正的功能。
埃里克·利珀特

1
@ThomasEding:您丢失了一些东西;迭代器不是这样工作的。该ApplyIterator方法立即返回。在对返回的对象的枚举器ApplyIterator进行第一次调用之前,不会运行主体中的代码MoveNext。现在您知道了,您可以推断出此难题 的答案:blogs.msdn.com/b/ericlippert/archive/2007/09/05/…答案在这里:blogs.msdn.com/b/ericlippert/archive /
2007/09/06

3

它不是纯函数,因此应用Pure属性会产生误导。

纯函数不会修改原始集合,并且您传递的动作是否无效都无关紧要。它仍然是一种不纯功能,因为其目的是引起副作用。

如果要使函数纯净,请将集合复制到新集合,将Action所做的更改应用于新集合,然后返回新集合,而使原始集合保持不变。


嗯,它不会修改原始集合,因为它只返回一个具有相同项目的新序列;这就是为什么我考虑使其纯净。但是,当枚举结果时,它可能会更改项目的状态。
托马斯·列维斯克

如果item是引用类型,则即使您要返回item迭代器,它也会修改原始集合。参见stackoverflow.com/questions/1538301
Robert Harvey

1
即使他对副本集进行了深度复制,它也仍然不是纯粹的,因为action除了修改传递给它的项目外,它还有其他副作用。
伊丹·阿里2014年

@IdanArye:的确,行动也必须是纯粹的。
罗伯特·哈维

1
@IdanArye:()=>{}可以转换为Action,它是一个纯函数。它的输出仅取决于其输入,并且没有可观察到的副作用。
埃里克·利珀特

0

在我看来,它接收到一个Action(而不是PureAction之类的东西)的事实使其不纯净。

我什至不同意埃里克·利珀特。他写道:“(()=> {}可转换为Action,它是一个纯函数。它的输出仅取决于其输入,并且没有明显的副作用”。

好吧,想象一下,而不是使用委托,ApplyIterator调用了一个名为Action的方法。

如果Action是纯函数,那么ApplyIterator也是纯函数。如果Action不纯净,则ApplyIterator不能纯净。

考虑到委托类型(不是实际的给定值),我们不能保证它将是纯净的,因此,仅当委托是纯净的时,该方法才能作为纯净方法使用。因此,要使其真正变得纯净,它应该接收一个纯净的委托(并且存在,我们可以将委托声明为[Pure],这样我们就可以拥有PureAction)。

用不同的方式解释它,纯方法应该始终在相同的输入下给出相同的结果,并且不应产生可观察到的变化。可以为ApplyIterator提供相同的源和委托两次,但是,如果委托正在更改引用类型,则下一次执行将给出不同的结果。示例:委托执行类似item.Content + =“ Changed”的操作;

因此,在“字符串容器”(具有Content类型字符串属性的对象)列表上使用ApplyIterator,我们可能具有以下原始值:

Test

Test2

第一次执行后,列表将具有以下内容:

Test Changed

Test2 Changed

这是第三次:

Test Changed Changed

Test2 Changed Changed

因此,我们正在更改列表的内容,因为委托不是纯粹的,并且如果执行3次则无法优化避免执行3次调用,因为每次执行都会产生不同的结果。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.