Deprecating this(this)

Jonathan M Davis newsgroup.d at jmdavisprog.com
Mon Apr 2 07:45:36 UTC 2018


On Monday, April 02, 2018 09:56:19 Shachar Shemesh via Digitalmars-d wrote:
> On 01/04/18 03:32, H. S. Teoh wrote:
> > The one nagging question I've been having about pure is: how much are we
> > actually taking advantage of the guarantees provided by pure?
>
> My problem is that pure do not provide many guarantees.
>
> >  We have
> >
> > developed very clever ways of extending the traditional definition of
> > pure and invented creative ways of making more things pure, which is all
> > great.
>
> Can anyone explain to me what good are the guarantees provided by a
> function that is pure but not strictly pure? I couldn't find them.

Honestly, I think at this point pure is easier to understand if you think of
it as @noglobal and don't think about functional purity at all. What pure
does is make it so that the function cannot access global, mutable state
except through its arguments. So, all that it's working with (except for
constants such as enums or immutable, module-level variables) is what it's
given via its arguments. As such, the primary benefit of pure is that you
know that no global state is being mucked with unless it was passed to the
function as an argument. _That_ is the guarantee that pure provides.
Everything else about pure is just built on top of that guarantee and what
the compiler can infer from it.

If the function is "strongly" pure (i.e. its parameters are immutable or
implicitly convertible to immutable) then the compiler knows that if the
function is called multiple times with the exact same arguments, then it
knows that the result will be the same each time (though it does have to
take into account the fact that each call could return a newly allocated
object - they'd just be equivalent objects every time). So, when you're
dealing with a strongly pure function, you're then dealing with actual,
functional purity, and the compiler can choose to optimize calls such as
foo(5) * foo(5) so that foo(5) is only called once.

Weakly pure functions (i.e. pure functions that aren't strongly pure) don't
have those same optimization benefits, because calling them could result in
the arguments being mutated, but because weakly pure functions don't access
global, mutable state, the compiler can safely call them from a strongly
pure function without violating the guarantee that multiple calls with the
same arguments to a strongly pure function are supposed to give the same
result.

Most pure functions are weakly pure (e.g. unless a pure member function is
immutable, then it's weakly pure), so the optimization benefits are pretty
minimal in most cases. And even if a function is strongly pure, the
optimization is only done within an expression (or maybe statement - I can
never remember which), because going beyond that would require data flow
analysis, which the compiler rarely does. As such, pretty much the only time
you end up with function calls being elided thanks to pure is when you have
a strongly pure function where you do something like foo(5) * foo(5). So,
the optimization benefits of pure are pretty minimal. It's that guarantee
about not touching global, mutable state which is the main benefit. As such,
if we were adding pure now, I would strongly argue for calling it something
like @noglobal. I think that it would reduce the confusion considerably. As
it stands, while it helps make functional purity possible in some cases, it
ultimately doesn't have much to do with functional purity in spite of its
name.

> > And perhaps infer uniqueness in some cases for implicit casting to
> > immutable.
>
> Can you expand on that one?

int[] foo()
{
    return [1, 2, 3, 4, 5];
}

void main()
{
    immutable arr = foo();
}

does not compile, because the return value is mutable, and you can't
implicitly convert int[] to immutable int[]. However,

int[] foo() pure
{
    return [1, 2, 3, 4, 5];
}

void main()
{
    immutable arr = foo();
}

compiles just fine. Because foo is pure, and the compiler knows that the
return value could not possibly have come from the function's arguments, it
knows that the return value is unique and that it won't violate the type
system to implicitly cast it to immutable. As such, you can write a function
as complicated as you want to create the return value, and so long as the
function is pure, and the compiler can determine that the return value did
not come via an argument, you can implicitly convert the return value to
immutable rather than having to use something like
std.exception.assumeUnique or an explicit cast, which relies on the
programmer verifying that the object being cast is indeed unique rather than
having the compiler guarantee it.

Whether the implicit cast is allowed ultimately depends on the types of the
pure function's parameters and the actual arguments, so it's not always
obvious whether it will work or not, but in general, it works very well for
initializing complex, immutable objects without having to rely on getting
casts right. e.g. this still compiles

int[] foo(int[] a) pure
{
    return [1, 2, 3, 4, 5];
}

void main()
{
    immutable arr = foo([1, 2]);
}

because the compiler can see that the argument being passed is unique and
that therefore the return value is unique whether it came from the argument
or not, whereas this doesn't compile

int[] foo(int[] a) pure
{
    return [1, 2, 3, 4, 5];
}

void main()
{
    int[] a = [1, 2];
    immutable arr = foo(a);
}

because the compiler can't guarantee that the return value isn't a slice of
the argument (at least not without looking at the implementation, but all
the compiler looks at is the signature).

- Jonathan M Davis



More information about the Digitalmars-d mailing list