捕获/抛出异常是否会使原本纯净的方法不纯?


27

以下代码示例为我的问题提供了上下文。

用委托初始化Room类。在Room类的第一个实现中,没有防范引发异常的委托的措施。此类异常将上升到North属性,在该North属性上评估委托(请注意:Main()方法演示了如何在客户端代码中使用Room实例):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

考虑到我宁愿在创建对象时失败,也不愿在读取North属性时失败,所以我将构造函数更改为private,并引入了一个名为Create()的静态工厂方法。此方法捕获委托引发的异常,并引发包装异常,并具有有意义的异常消息:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

try-catch块是否会使Create()方法不纯?


1
创建房间功能的好处是什么?如果您使用委托,为什么在客户致电“ North”之前先调用它?
SH-

1
@SH如果委托引发异常,我想在创建Room对象时查明。我不希望客户端在使用时发现异常,而是在创建时发现异常。Create方法是暴露邪恶代表的理想场所。
安东尼·约翰逊

3
@RockAnthonyJohnson,是的。在执行委托时,您只能执行第一次操作,或者在第二次调用时返回另一个房间,您会做什么?可以考虑给代表打电话/本身会引起副作用?
SH-

4
调用不纯函数的函数就是不纯函数,因此,如果委托Create不纯,那么它也是不纯的,因为它调用了它。
伊丹·阿里

2
Create在获取属性时,您的函数无法防止您获取异常。如果您的代表抛出,在现实生活中很有可能仅在某些条件下才抛出。可能的情况是,在施工过程中不存在投掷的条件,但在获得财产时却存在。
巴特·范·恩根·谢瑙

Answers:


26

是。 这实际上是一种不纯功能。它产生了一个副作用:程序执行将在函数预期返回的位置以外的其他地方继续进行。

为了使它成为纯函数,return是一个实际对象,它封装了该函数的期望值和一个指示可能的错误情况的值,例如Maybe对象或工作单元对象。


16
记住,“纯”只是一个定义。如果您需要一个引发函数,但在其他方面却是透明的,那就是您编写的。
罗伯特·哈维

1
在haskell中,至少任何函数都可以抛出,但是您必须在IO中才能捕获,有时会抛出的纯函数通常称为“部分
jk”。

4
由于您的评论,我顿悟了。尽管我对纯洁着迷,但实际上,我实际上需要确定性和崇高的透明度。如果我达到纯正,那只是一个额外的好处。谢谢。仅供参考,促使我走这条路的是事实,即Microsoft IntelliTest仅对确定性代码有效。
安东尼·约翰逊

4
@RockAnthonyJohnson:好吧,所有代码都应该是确定性的, 或者至少应尽可能确定性。引用透明性只是获得确定性的一种方法。诸如线程之类的某些技术趋向于与该确定性背道而驰,因此还有诸如Tasks之类的其他技术可以改善线程模型的非确定性趋势。诸如随机数生成器和蒙特卡洛模拟器之类的东西也是确定性的,但是基于概率的方式不同。
罗伯特·哈维

3
@zzzzBov:我不认为“纯”的实际,严格定义那么重要。参见上面的第二条评论。
罗伯特·哈维

12

好吧,是的。。。

纯函数必须具有引用透明性 -也就是说,您应该能够用返回的值替换对纯函数的任何可能的调用,而无需更改程序的行为。*确保您的函数总是抛出某些参数,因此没有返回值可以代替函数调用,因此让我们忽略它。考虑以下代码:

{
    var ignoreThis = func(arg);
}

如果func是纯函数,那么优化器可以决定func(arg)可以用它的结果替换它。它尚不知道结果是什么,但是它可以表明它没有被使用-因此它只能推断出该语句没有任何作用并将其删除。

但是,如果func(arg)碰巧抛出该语句,则它会执行某些操作-它将引发异常!因此优化器无法删除它-函数是否被调用很重要。

但...

实际上,这无关紧要。异常-至少在C#中-是例外。您不应该将其用作常规控制流的一部分-您应该尝试并捕获它,并且如果确实捕获到某些东西,请处理错误以恢复您正在做的事情或以某种方式仍然完成它。如果您的程序由于将失败的代码优化而无法正常运行,则说明您使用了错误的异常(除非它是测试代码,并且在构建测试时不应对异常进行优化)。

话虽如此...

不要从纯函数中抛出异常,以防它们被捕获-有充分的理由说明,功能语言更喜欢使用monad而不是stack-unwinding-exceptions。

如果C#具有Error类似Java(和许多其他语言)的类,则建议使用Error而不是Exception。它表明函数的用户做错了什么(通过了抛出的函数),并且纯函数中允许这样的事情。但是C#没有Error类,使用错误异常似乎源于Exception。我的建议是抛出一个ArgumentException,以明确该函数是用错误的参数调用的。


*从 计算上来说。使用天真的递归实现的Fibonacci函数将花费大量时间,并且可能会耗尽机器的资源,但是由于无限的时间和内存,该函数将始终返回相同的值并且不会产生副作用(除了分配内存并更改该内存)-仍然被认为是纯净的。


1
+1为您建议使用ArgumentException,并建议您不要“从纯函数中引发异常以使其被捕获”。
安东尼·约翰逊

您的第一个论点(直到“但是...”为止)对我来说似乎是不正确的。例外是该功能合同的一部分。从概念上讲,您可以将任何函数重写为,(value, exception) = func(arg)并消除引发异常的可能性(在某些语言中,对于某些类型的异常,实际上需要使用定义将其列出,与参数或返回值相同)。现在它和以前一样纯净(假设它总是返回aka,并且在给定相同参数的情况下抛出异常)。如果使用此解释,则抛出异常不是副作用。
AnoE

@AnoE如果您采用func这种方式编写,我给出的块可能已经被优化掉了,因为抛出的异常将被编码为ignoreThis,而不是展开堆栈,我们将其忽略。与展开堆栈的异常不同,以返回类型编码的异常不会违反纯度-这就是为什么许多功能语言更喜欢使用它们的原因。
伊丹·阿里

确实……您不会忽略此想象的转换的异常。我想这全是语义/定义的问题,我只是想指出,异常的存在或发生并不一定会使所有纯净的概念无效。
AnoE

1
@AnoE但是我编写的代码忽略此想象的转换的异常。func(arg)将返回元组(<junk>, TheException()),因此ignoreThis将包含元组(<junk>, TheException()),但是由于我们从不使用ignoreThis该异常,因此对任何内容均无效。
伊丹·阿里

1

一个考虑因素是try-catch块不是问题。(基于我对上述问题的评论)。

主要问题是North属性是一个I / O调用

在执行代码的那一刻,程序需要检查客户端代码提供的I / O。(输入是委托形式的,或者名义上已经传递了输入是无关紧要的)。

一旦失去对输入的控制,就无法确保该函数是纯函数。(特别是该函数是否可以抛出)。


我不清楚您为什么不想检查对Move [Check] Room的呼叫?根据我对这个问题的评论:

是。在执行委托时,您只能执行第一次操作,或者在第二次调用时返回另一个房间,该怎么办?可以考虑给代表打电话/本身会引起副作用?

正如Bart van Ingen Schenau所说,

您的Create函数不会保护您在获取属性时不会发生异常。如果您的代表抛出,在现实生活中很有可能仅在某些条件下才抛出。可能的情况是,在施工过程中不存在投掷的条件,但在获得财产时却存在。

通常,任何类型的延迟加载都会 隐式地将错误推迟到这一点。


我建议使用Move [Check] Room方法。这将使您可以将不纯的I / O方面分成一个地方。

Robert Harvey的答案类似:

要使其成为纯函数,请返回一个实际对象,该对象封装了该函数的期望值和一个指示可能的错误情况的值,例如Maybe对象或Unit of Work对象。

由代码编写者确定如何处理输入中的(可能)异常。然后,该方法可以返回一个Room对象或一个Null Room对象,或者可能使异常冒泡。

这一点取决于:

  • Room域将Room Exception视为Null还是更糟。
  • 如何通知在空/异常室上调用North的客户端代码。(保释/状态变量/全局状态/返回单声道/任何东西;有些比另一些更纯净:))。

-1

嗯...对此我感觉不对。我认为您尝试做的是确保其他人在不知道自己在做什么的情况下正确地完成工作。

由于该函数由使用者传递,因此可以由您或其他人编写。

如果传入的函数在创建Room对象时无效,该怎么办?

从代码中,我不确定北方正在做什么。

假设,如果功能是预订房间,并且需要时间和预订时间,那么如果您还没有准备好信息,那么您将获得例外。

如果它是长期运行的功能怎么办?创建Room对象时,您不会希望阻止程序,如果需要创建1000 Room对象该怎么办?

如果您是编写使用此类的使用者的人,那么您将确保传入的函数编写正确,不是吗?

如果有其他人来撰写消费者,则他/她可能不知道创建Room对象的“特殊”条件,这可能会导致执行,并且他们会挠头试图找出原因。

另一件事是,抛出新异常并不总是一件好事。原因是它将不包含原始异常的信息。您知道收到错误(通用异常的友好消息),但您不知道错误是什么(空引用,索引越界,除零等)。当我们需要进行调查时,此信息非常重要。

仅当您知道如何处理异常时,才应处理该异常。

我认为您的原始代码已经足够好,唯一的一件事就是您可能想在“ Main”(或任何知道如何处理它的层)上处理异常。


1
再看一下第二个代码示例;我用较低的异常初始化新的异常。整个堆栈跟踪都将保留。
安东尼·约翰逊

哎呀。我的错。
2016年

-1

我将与其他答案不同意,说没有。异常不会导致原本纯净的功能变得不纯净。

任何对异常的使用都可以重写为对错误结果进行显式检查。在此之上,异常可以仅被视为语法糖。方便的语法糖不会使纯代码不纯。


1
您可以改写杂质的事实并不能使功能纯净。Haskell证明不纯函数的使用可以用纯函数重写-它使用monads编码返回类型纯纯函数中的任何不纯行为。IO monads的存在并不意味着常规IO是纯正的-为什么异常monads的存在应该意味着常规IO是纯净的?
伊丹·阿里

@IdanArye:我一直在争辩说没有杂质。重写示例只是为了证明这一点。常规IO是不纯净的,因为它会引起明显的副作用,或者由于某些外部输入,可能会因相同的输入而返回不同的值。引发异常不会执行任何这些操作。
JacquesB

没有副作用的代码是不够的。引用透明性也是功能纯度的要求。
伊丹·阿里

1
确定性不足以实现参照透明性-副作用也可以是确定性的。引用透明性意味着可以用表达式的结果替换该表达式-因此,无论您评估引用透明表达式多少次,以什么顺序以及是否对它们进行求值,结果都应该相同。如果确定性地引发了异常,那么我们对“无论多少次”标准都很好,但其他条件则不然。我在自己的答案中争论过“是否”标准,(续...)
Idan Arye

1
(...续),并为“在什么样的顺序”的-如果fg都是纯函数,那么f(x) + g(x)应该有相同的结果,无论您评估的顺序f(x)g(x)。但是,如果f(x)throws Fg(x)throws抛出G,那么从该表达式引发的异常将是Fif f(x)首先被评估,而Gif g(x)被首先评估-这不是纯函数的行为!如果将异常编码为返回类型,则结果将最终Error(F) + Error(G)独立于评估顺序。
伊丹·阿里
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.