digitalmars.com                        
Last update Sat Oct 7 16:49:29 2023

A Fly In The Purity

June 5, 2011

written by Walter Bright

A while back, I wrote an article on how the D programming language added a “pure” annotation which could be applied to functions. A pure function does what you'd expect - the compiler enforces purity of the function. The function may not have any side effects, may not rely on any mutable data, and this is statically checkable.

Functional programming enthusiasts have long understood the benefits of pure functions - excellent encapsulation, low cognitive load, thread safe, etc. Pure functions have caught on as well among the D programming community. Good D style encourages as many functions as practical should be annotated as pure.

But there's a problem.

Pure functions can only call other pure functions. One might say “it's turtles all the way down.” Now let's say we've got a rather deep call chain of pure functions calling pure functions, and we suspect a bug lurks in the deepest, darkest one of them sitting in bottom of the sump pump in the basement.

What's the first tool many of us grab for? Stick a print statement in it to verify that the function is actually being called, and what it's arguments are, like:

import std.stdio;

pure int square(int x)
{
    writefln("square(%d)", x);
    return x * x;
}

Uh-oh. The compiler means it when it checks for function purity:

test.d(5): Error: pure function 'square' cannot call impure function 'writefln'

Let's examine the options:

  1. Remove the pure annotation. That'll enable square() to successfully compile, but then every pure function that calls square() will now fail to compile. The “turtles all the way down” now becomes “turtles all the way up” as you are forced to remove the pure attribute from the entire function call graph. Clearly, this is very unappealing.
  2. D has an all-purpose escape from type checking — the cast. Casting is a blunt instrument usable to get us out of all kinds of jams. Here, we can use it like:
import std.stdio;

int square_debug(int x)
{
    writefln("square(%d)", x);
    return x * x;
}

pure alias int function(int) fp_t;

pure int square(int x)
{
    auto fp = cast(fp_t)&square_debug;
    return (*fp)(x);
}

The pure function is split into two, and the impure one is bashed into submission by forcibly casting it to be pure. The best face we can put on this is that it works and gets the job done.

But I hate it.

D is meant to be a joy to work with, and there's no joy in this mudville. It's time to think of a language modification.

In a functional programming language, using monadic output for debugging messages may be the right choice. But D does not use monadic I/O, preferring straight calls to I/O functions. Leaving the debate of which is better to philosophers, let me just note that monads should not be necessary for taking care of this small matter in D.

What programmers want is to just stick the print statement in and have it work despite it being impure. How can we accommodate that, yet still be pure? Is there anything in D that can be pressed into service for this?

Yup. D has something called a Debug Statement. Debug statements are regular statements prefixed with the keyword debug. They are compiled in only when the -debug switch is passed to the compiler. With a simple change to the language, we can disable purity checking inside a debug statement. So, to make our original example compile:

import std.stdio;

pure int square(int x)
{
    debug writefln("square(%d)", x);
    return x * x;
}

Simple, easy, and it works. The compiler otherwise still treats the function as pure, so depending on the situation the user may see a variable number of writes. This is because the compiler is allowed to cache the result of pure functions, or on the contrary to call them repeatedly.

Some uneasiness about this is understandable. After all, it is breaking purity. The rationale is that debug code is not production code. It's reasonable to be able to break the rules as necessary for debugging. Of course this does put the onus on the programmer to not introduce a heisenbug with this where the program works when compiled with -debug and fails without.

Acknowledgements

Thanks to Andrei Alexandrescu, Brad Roberts, David Held, and Bartosz Milewski for their helpful comments and corrections on this article.

Home | Runtime Library | IDDE Reference | STL | Search | Download | Forums