28

The following code examples provide context to my question.

The Room class is initialized with a delegate. In the first implementation of the Room class, there are no guards against delegates that throw exceptions. Such exceptions will bubble up to the North property, where the delegate is evaluated (note: the Main() method demonstrates how a Room instance is used in client code):

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

    }
}

Being that I'd rather fail upon object creation rather than when reading the North property, I change the constructor to private, and introduce a static factory method named Create(). This method catches the exception thrown by the delegate, and throws a wrapper exception, having a meaningful exception message:

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

Does the try-catch block render the Create() method impure?

7
  • 1
    What is the benefit of the Room Create function; if your using a delegate why call it before the client calls 'North'?
    – SH-
    Commented Oct 26, 2016 at 22:07
  • 1
    @SH If the delegate throws an exception, I want to find out upon creation of the Room object. I don't want the client to find the exception upon usage, but rather upon creation. The Create method is the perfect place to expose evil delegates. Commented Oct 26, 2016 at 22:11
  • 3
    @RockAnthonyJohnson, Yes. What do you when executing the delegate may only work the first time, or return a different room on the second call? Calling the delegate can be considered / cause a side effect itself?
    – SH-
    Commented Oct 26, 2016 at 22:17
  • 4
    A function that calls an impure function is an impure function, so if the delegate is impure Create is also impure, because it calls it.
    – Idan Arye
    Commented Oct 27, 2016 at 0:19
  • 2
    Your Create function does not protect you from getting an exception when getting the property. If your delegate throws, in real life it is very likely that it will thrown only under some conditions. Chances are that the conditions for throwing are not present during construction, but they are present when getting the property. Commented Oct 27, 2016 at 18:22

5 Answers 5

26

Yes. That is effectively an impure function. It creates a side-effect: program execution continues somewhere other than the place to which the function is expected to return.

To make it a pure function, return an actual object that encapsulates the expected value from the function and a value indicating a possible error condition, like a Maybe object or a Unit of Work object.

13
  • 16
    Remember, "pure" is just a definition. If you need a function that throws, but in every other way it is referentially transparent, then that's what you write. Commented Oct 26, 2016 at 16:00
  • 1
    In haskell at least any function can throw but you have to be in IO to catch, a pure function that sometimes throws would normally be called partial
    – jk.
    Commented Oct 26, 2016 at 16:38
  • 4
    I've had an epiphany because of your comment. While I'm intellectually intrigued by purity, what I actually need in practicality is determinism and reverential transparency. If I achieve purity, that's just an added bonus. Thank you. FYI, the thing that's driven me down this path is the fact that Microsoft IntelliTest is effective only against code that is deterministic. Commented Oct 26, 2016 at 16:38
  • 4
    @RockAnthonyJohnson: Well, all code should be deterministic, or at least as deterministic as possible. Referential transparency is just one way to get that determinism. Some techniques, like threads, have a tendency to work against that determinism, so there are other techniques like Tasks that improve the threading model's non-deterministic tendencies. Things like random number generators and Monte-Carlo simulators are also deterministic, but in a different way based on probabilities. Commented Oct 26, 2016 at 16:40
  • 3
    @zzzzBov: I don't consider the actual, strict definition of "pure" all that important. See the second comment, above. Commented Oct 26, 2016 at 19:20
13

Well, yes... and no.

A pure function must have Referential Transparency - that is, you should be able to replace any possible call to a pure function with the returned value, without changing the program's behavior.* Your function is guaranteed to always throw for certain arguments, so there is no return value to replace the function call with, so instead let's ignore it. Consider this code:

{
    var ignoreThis = func(arg);
}

If func is pure, an optimizer could decide that func(arg) could be replaced with it's result. It does not know yet what the result is, but it can tell that it's not being used - so it can just deduce this statement has no effect and remove it.

But if func(arg) happens to throw, this statement does do something - it throws an exception! So the optimizer can not remove it - it does matter if the function get called or not.

But...

In practice, this matters very little. An exception - at least in C# - is something exceptional. You are not supposed to use it as part of your regular control flow - you are supposed to try and catch it, and if you do catch something handle the error to either revert what you were doing or to somehow still accomplish it. If your program does not work properly because a code that would have failed was optimized away, you are using exceptions wrong(unless it's test code, and when you build for tests exceptions should not be optimized).

That being said...

Don't throw exceptions from pure functions with the intention that they'll be catched - there is a good reason functional languages prefer to use monads instead of stack-unwinding-exceptions.

If C# had an Error class like Java(and many other languages), I would have suggested to throw an Error instead of an Exception. It indicates that the user of the function did something wrong(passed a function that throws), and such things are allowed in pure functions. But C# does not have an Error class, and the usage error exceptions seem to derive from Exception. My suggestion is to throw an ArgumentException, making it clear that the function was called with a bad argument.


* Computationally speaking. A Fibonacci function implemented using naive recursion will take a long time for large numbers, and may exhaust the machine's resources, but these since with limitless time and memory the function will always return the same value and will not have side-effects(other than allocating memory and altering that memory) - it's still considered pure.

5
  • 1
    +1 For your suggested use of ArgumentException, and for your recommendation to not "throw exceptions from pure functions with the intention that they'll be catched". Commented Oct 26, 2016 at 18:28
  • 1
    Your first argument (until the "But...") seems unsound to me. The exception is part of the contract of the function. You can, conceptionally, rewrite any function as (value, exception) = func(arg) and remove the possibility of throwing an exception (in some languages and for some types of exceptions you actually need to list it with the definition, same as arguments or return value). Now it would be just as pure as before (provided it always returns a.k.a throws an exception given the same argument). Throwing an exception is not a side effect, if you use this interpretation.
    – AnoE
    Commented Oct 26, 2016 at 22:56
  • @AnoE If you would have written func that way, the block I have given could have been optimized away, because instead of unwinding the stack the thrown exception would have been encoded in ignoreThis, which we ignore. Unlike exceptions that unwind the stack, exception encoded in the return type do not violate purity - and that's why many functional languages prefer to use them.
    – Idan Arye
    Commented Oct 27, 2016 at 0:13
  • Exactly... you would not have ignored the exception for this imagined transformation. I guess it's all a bit of a issue of semantics/definition, I just wanted to point out that the existence or occurence of exceptions does not necessarily void all notions of purity.
    – AnoE
    Commented Oct 27, 2016 at 5:50
  • 1
    @AnoE But the code I've writted would ignore the exception for this imagined transformation. func(arg) would return the tuple (<junk>, TheException()), and so ignoreThis will contain the tuple (<junk>, TheException()), but since we never use ignoreThis the exception will have no effect on anything.
    – Idan Arye
    Commented Oct 27, 2016 at 10:24
1

One consideration is that the try - catch block is not the issue. (Based on my comments to the question above).

The main problem is that the North property is an I/O call.

At that point in the code's execution, the program needs to check the I/O provided by the client code. (It would not be relevant that the input is in the form of a delegate, or that the input was, nominally, passed in already).

Once you lose control of the input, you cannot ensure the function is pure. (Especially if the function can throw).


I'm not clear why you do not want to check on the call to Move[Check]Room? As per my comment to the question:

Yes. What do you when executing the delegate may only work the first time, or return a different room on the second call? Calling the delegate can be considered / cause a side effect itself?

As Bart van Ingen Schenau said above,

Your Create function does not protect you from getting an exception when getting the property. If your delegate throws, in real life it is very likely that it will thrown only under some conditions. Chances are that the conditions for throwing are not present during construction, but they are present when getting the property.

In general, any type of lazy loading implicitly defers the errors until that point.


I would suggest using a Move[Check]Room method. This would allow you to separate the impure I/O aspects into one place.

Similar to Robert Harvey's answer:

To make it a pure function, return an actual object that encapsulates the expected value from the function and a value indicating a possible error condition, like a Maybe object or a Unit of Work object.

It would be up to the code writer to determine how to handle the (possible) exception from the input. Then the method can return a Room object, or a Null Room object, or perhaps bubble out the exception.

It this point it depends on:

  • Does the Room domain treat Room Exceptions as Null or Something Worse.
  • How to notify the client code calling North on a Null / Exception Room. (Bail / Status Variable / Global State / Return a Monad / Whatever; Some are more pure then others :) ).
-1

Hmm... I didn't feel right about this. I think what you arw trying to do is to make sure other people do their job right, without knowing what they are doing.

Since the function is passed in by the consumer, which could be written by you or by other person.

What if the function pass in is not valid to run at the time the Room object is created?

From the code, I couldn't be sure what the North is doing.

Let say, if the function is to book the room, and it need the time and the period of booking, then you would get exception if you have not have the information ready.

What if it is a long running function? You will not want to block your program while creating a Room object, what if you need to create 1000 Room object?

If you were the person who will write the consumer that consume this class, you would make sure the function passed in is written correctly, wouldn't you?

If there is another person who write the consumer, he/she might not know the "special" condition of create a Room object, which could cause execption and they would scratch their head trying to find out why.

Another thing is, it not always a good thing to throw a new exception. Reason being is it will not contain the information of the original exception. You know you get an error (a friendly mesaage for generic exception), but you wouldn't know what is the error (null reference, index out of bound, devide by zero etc). This information is very important when we need to do investigation.

You should handle the exception only when you know what and how to handle it.

I think your original code is good enough, the only thing is you might want to handle the exception on the "Main" (or any layer that would know how to handle it).

2
  • 1
    Look again at the 2nd code example; I initialize the new exception with the lower exception. The entire stack trace is preserved. Commented Oct 27, 2016 at 0:40
  • Oops. My mistake.
    – user251630
    Commented Oct 27, 2016 at 1:06
-1

I will disagree with the other answers and say No. Exceptions does not cause an otherwise pure function to become impure.

Any use of exceptions could be rewritten to use explicit checks for error results. Exceptions could just be considered syntactic sugar on top of this. Convenient syntactic sugar does not make pure code impure.

6
  • 1
    The fact that you can rewrite the impurity away does not make the function pure. Haskell is a proof that use of impure functions can be rewritten with pure functions - it uses monads to encode any impure behavior in the return types pure pure functions. The existence of IO monads does not imply that regular IO is pure - why should the existence of exceptions monads imply that regular exceptions are pure?
    – Idan Arye
    Commented Oct 27, 2016 at 11:12
  • @IdanArye: I'm arguing there is no impurity in the first place. The rewriting example was just to demonstrate that. Regular IO is impure because it causes observable side effects, or may return different values with the same input because of some external input. Throwing an exception does not do any of those things.
    – JacquesB
    Commented Oct 27, 2016 at 17:45
  • Side-effect free code is not enough. Referential Transparency is also a requirement for function purity.
    – Idan Arye
    Commented Oct 27, 2016 at 18:53
  • 1
    Determinism is not enough for referential transparency - side-effects can also be deterministic. Referential transparency means that the expression can be replaced with it's results - so no matter how many times you evaluate referential transparent expressions, in what order, and whether or not you evaluate them at all - the result should be the same. If an exception is deterministically thrown we are good with the "no matter how many times" criteria, but not with the other ones. I've argued about the "whether or not" criteria in my own answer, (cont...)
    – Idan Arye
    Commented Oct 28, 2016 at 15:43
  • 1
    (...cont) and as for the "in what order" - if f and g are both pure functions, then f(x) + g(x) should have the same result regardless of the order you evaluate f(x) and g(x). But if f(x) throws F and g(x) throws G, then the exception thrown from this expression would be F if f(x) is evaluated first and G if g(x) is evaluated first - this is not how pure functions should behave! When the exception is encoded in the return type, that result would end up as something like Error(F) + Error(G) independently of the evaluation order.
    – Idan Arye
    Commented Oct 28, 2016 at 15:44

Not the answer you're looking for? Browse other questions tagged or ask your own question.