October 29, 2021
On Fri, Oct 29, 2021 at 03:51:05PM +0000, Patrick Schluter via Digitalmars-d wrote:
> On Thursday, 28 October 2021 at 22:48:10 UTC, Dukc wrote:
> > On Thursday, 28 October 2021 at 19:46:32 UTC, Patrick Schluter wrote:
> > > Not at all. free cannot, by its semantic, be pure (same for malloc).  Trying to make free pure is a silly challenge.
> > 
> > https://dlang.org/phobos/core_memory.html#.pureFree
> 
> I don't understand it. It does not make any sense. pure functions are function that do not depend on global state for their result. Weak purity allows for some exception like for a print function which has global effects but these effects have no feedback and can ne ignored.

Actually, even that is wrong. I/O changes global state, and as such have no place in pure functions, not even weakly pure ones.  Weakly pure functions may only modify *state they receive via their parameters*, and nothing else.  Modifying any other state violates the definition of (weak) purity and breaks any purity-based optimizations.

The only I/O permitted in pure functions, whether weakly pure or strongly pure, are in debug statements, and that's only for the purposes of debugging.  It should definitely not be something program semantics depend on.


> This is not the case with allocation/free, which are, by defintion, dependend on a global state (even if only thread  local).

Yeah, pureFree makes no sense at all. It's a disaster waiting to happen.


> Each call to malloc, by definition must return another value and/or
> can return a same value with other parameter. The result does not
> depend on ANYTHING in its scope.
> pure allocation/free is a recipe for disaster.
> My understanding of purity at l east.

I agree. I don't know when/why pureFree was introduced, but it's all kinds of wrong.

One may argue that pure functions may return allocated memory (this is explicitly allowed in the specs), but IMO this is only allowed if the allocation is *implicit*, i.e., part of a language construct, such that its effects are not visible within the domain of language constructs. Otherwise this opens the door to all kinds of subtle bugs that basically make purity a laughably useless construct.


T

-- 
Computerese Irregular Verb Conjugation: I have preferences.  You have biases.  He/She has prejudices. -- Gene Wirchenko
October 29, 2021

On Friday, 29 October 2021 at 16:08:10 UTC, H. S. Teoh wrote:

> >

This is not the case with allocation/free, which are, by defintion, dependend on a global state (even if only thread local).

Yeah, pureFree makes no sense at all. It's a disaster waiting to happen.

I think the original sin here is allowing GC allocation (new, ~=, closures) to be pure, for "pragmatic" reasons.

Once you've done that, it's not hard to justify adding pureMalloc too. And once you have that, why not pureFree? It's just a little white lie; surely nobody will get hurt.

Of course the end result is that pure ends up being basically useless for anything beyond linting, and can't be fixed without breaking lots of existing code.

October 29, 2021
On Fri, Oct 29, 2021 at 04:14:35PM +0000, Paul Backus via Digitalmars-d wrote:
> On Friday, 29 October 2021 at 16:08:10 UTC, H. S. Teoh wrote:
> > > This is not the case with allocation/free, which are, by defintion, dependend on a global state (even if only thread local).
> > 
> > Yeah, pureFree makes no sense at all. It's a disaster waiting to happen.
> 
> I think the original sin here is allowing GC allocation (`new`, `~=`, closures) to be `pure`, for "pragmatic" reasons.
> 
> Once you've done that, it's not hard to justify adding `pureMalloc` too. And once you have that, why not `pureFree`? It's just a little white lie; surely nobody will get hurt.
> 
> Of course the end result is that `pure` ends up being basically useless for anything beyond linting, and can't be fixed without breaking lots of existing code.

I think the real root problem is mixing incompatible levels of abstraction.

At some level of abstraction, one could argue that GC allocation (or memory allocation in general) is an intrinsic feature of the layer of abstraction you're working with: a bunch of functions that do computations with arrays can be considered pure if the implementation of said arrays is abstracted away by the language, and these functions use only the array primitives given to them by the abstraction, i.e., they don't allocate or free memory directly, so they do not directly observe the external effects of allocation.  Think of a program in a functional language, for example. The implementation is definitely changing global state -- doing I/O, allocating/freeing memory, etc.. But at the abstraction level of the function language itself, these implementation details are hidden away and one can meaningfully speak of the purity of functions written in that language. One may legally optimize code based on the abstracted semantics, because the semantics at the higher level are preserved in spite of the low-level implementation details being changed.

The problems come, however, when you have code that operates *both* at the abstract level *and* deal with the low-level implementation at the same time.  Suddenly, there is no longer a clear separation between code in the higher-level abstraction and the lower-level implementation where you have to deal with dirty details like allocating and freeing memory. So the assumptions that the higher-level abstraction provides may no longer hold, and that's where you begin to run into trouble. Optimizations based on guarantees provided by the higher-level abstraction become invalidated by lower-level code that break these assumptions (because they operate outside of the confines of the higher-level abstraction).

This is why array manipulation in a D pure function is in some sense permissible, under certain assumptions, but things like pureFree do not make sense, because it clearly mixes incompatible levels of abstraction in a way that will inevitably lead to problems.  If we were to permit array allocations in pure code, then we must necessarily also commit to not go outside of the confines of that level of abstraction -- i.e., we are not allowed to use memory allocation primitives that said array operations are based on. As soon as this is violated, the whole thing comes crashing down, because your program now has some operations that are outside the abstraction assumed by the optimizations based on `pure`.  Meaning that these optimizations now may be invalid.

The situation is similar to `immutable`. If you're operating at the GC level, there is strictly speaking no such thing as immutable, because the GC code casts untyped memory into immutable and vice versa, so that the same block of memory may be immutable at one point in time but become mutable when it's later collected and reallocated to mutable data.  But this does not mean we're not allowed to optimize based on immutable; by the same line of argument we might as well throw const and immutable to the winds.  Instead, we declare GC code as @system, with the GC interface @trusted, i.e., the GC operates outside of the confines of immutability, but we trust it to do its job properly so that when we return to the higher-level abstraction, all our previous assumptions about immutable continue to hold.

So for pure, it's the same thing. For something to be pure you must have a well-defined set of abstractions based on which the optimizer is allowed to make certain transformations to your code.  You must adhere to the restrictions imposed by this abstraction -- which is what the `pure` qualifier is ostensibly for -- otherwise you end up in UB territory, just like the situation with casting away immutable.  The only sane way to maintain D's purity system is that code marked pure cannot contain anything that violates the assumptions we have imposed on pure.  Otherwise we're in de facto UB territory even if the spec's definition of UB doesn't specifically state this case.

Long story short, pureFree makes no sense because it's very intent is to make a visible change to the global state of memory -- clearly at a much lower level of abstraction than `pure` is intended to operate at, and clearly outside the `pure` abstraction.  In fact, I'd say that *anything* that explicitly allocates/deallocates memory ought to be prohibited from being marked `pure`.  Array operations are OK if we view them as intrinsic, opaque operations that the pure abstraction grants us. But anything that explicitly deals with memory allocation is clearly an operation outside the `pure` abstraction, so allowing it to be marked `pure` will inevitably break assumptions and land us in trouble.


T

-- 
Meat: euphemism for dead animal. -- Flora
October 29, 2021
On Friday, 29 October 2021 at 21:16:49 UTC, H. S. Teoh wrote:
> Long story short, pureFree makes no sense

What about:

void foo() pure {
   int* a = malloc(5);
   scope(exit) free(a);
}


How is that any different than

void foo() pure {
   int[5] a;
}

?

I expect that's how it is actually used. (Perhaps it would be better encapsulated in a function along the lines of:

void foo() pure {
    workWithMemory((int* a) {

    }, 5);
}


And then the malloc/free could be wrapped up a bit better)


October 30, 2021
On Friday, 29 October 2021 at 21:16:49 UTC, H. S. Teoh wrote:
> Array operations are OK if we view them as intrinsic, opaque operations that the pure abstraction grants us.

My point is that in a systems-level language like D, where the side effects of memory allocation are directly observable from normal code [1][2], operations that allocate memory are *not* opaque, pure abstractions, and cannot possibly be "viewed" as such.

I understand that, historically, this kind of argument has (successfully) been used to justify things like `pureMalloc` and pure array concatenation, but the argument is based on a false premise, and always has been.

[1] https://dlang.org/phobos/core_memory.html#.GC.stats
[2] https://dlang.org/phobos/core_memory.html#.GC.query
October 29, 2021
On Sat, Oct 30, 2021 at 12:29:00AM +0000, Paul Backus via Digitalmars-d wrote:
> On Friday, 29 October 2021 at 21:16:49 UTC, H. S. Teoh wrote:
> > Array operations are OK if we view them as intrinsic, opaque operations that the pure abstraction grants us.
> 
> My point is that in a systems-level language like D, where the side effects of memory allocation are directly observable from normal code [1][2], operations that allocate memory are *not* opaque, pure abstractions, and cannot possibly be "viewed" as such.

It can be, if such operations cannot be marked pure (they are certainly not pure). I.e., if code marked `pure` is restricted to a subset of operations, then it can be consistently optimized based on purity assumptions.

It's the same idea as @safe.  D as a whole cannot be considered safe due to operations like pointer arithmetic and untagged unions, but code restricted to the @safe subset can.


> I understand that, historically, this kind of argument has (successfully) been used to justify things like `pureMalloc` and pure array concatenation, but the argument is based on a false premise, and always has been.
> 
> [1] https://dlang.org/phobos/core_memory.html#.GC.stats
> [2] https://dlang.org/phobos/core_memory.html#.GC.query

GC.stats / GC.query are impure operations, as are memory (de)allocation operations, and should not be allowed in pure code.


T

-- 
People say I'm indecisive, but I'm not sure about that. -- YHL, CONLANG
October 30, 2021

On Wednesday, 20 October 2021 at 09:47:54 UTC, SealabJaster wrote:

>

Just for giggles, without pesky things like breaking changes; rational thinking, logical reasoning behind the changes, etc.

What interesting changes would you make to the language, and what could they possibly look like?

Here's a small example of some things I'd like.

import std;

interface Animal
{
    void speak(string language);
}

struct Dog
{
    @nogc @nothrow @pure @safe
    static void speak(string l)
    {
        // Pattern matching of some kind
        // With strings this is just a fancy switch statement, but this is the gist of it
        match l with
        {
            "english" => writeln("woof"),
            "french" => writeln("le woof"),
            _ => writeln("foow")
        }
    }
}

struct Cat
{
    // Remove historical baggage. Make old attributes into `@` attributes
    @nogc @nothrow @pure @safe
    static void speak()
    {
        writeln("meow")
    }
}

// ? for "explicitly nullable"
void doSpeak(alias T)(string? language)
if(is(T == struct) && match(T : Animal)) // Match structs against an interface.
{
    // immutable by default. ?? is the same as in C#
    auto lang = language ?? "UNKNOWN";

    // So of course we'd need a mutable keyword of some sort
    mutable output = $"{__traits(identifier, T)} speaking in {lang}"; // String interpolation
    writeln(output);
    T.speak(lang);
}

void main()
{
    doSpeak!Dog;
    doSpeak!Cat; // Should be a compiler error since it fails the `match` statement
}
  • I would probably choose a different syntax for templates.
  • I would add ?. and ??
  • I would work on the levels of strictness. D definitely does this better than any other language, but I have used it and found it wanting from time to time. Most projects start as a prototype and you don't need a lot of strict rules for that. For that you don't want type checking or method signature validation, you want a scripting language. As you get going, you want to still move fast but you probably want a strongly-typed language and some other features. At some point, certain parts of the code, libraries or even whole programs can be battened down and you want features like compile-time guarantees and performance. I would say D is the leader in this for all the languages that I use, but I think it could have a little more room to grow and have better documentation.
  • Possibly add something to help autocomplete. For example, in C#, extension methods get picked up by autocomplete. That is the advantage they have over normal methods.
October 30, 2021
On Friday, 29 October 2021 at 21:56:10 UTC, Adam Ruppe wrote:
> On Friday, 29 October 2021 at 21:16:49 UTC, H. S. Teoh wrote:
>> Long story short, pureFree makes no sense
>
> What about:
>
> void foo() pure {
>    int* a = malloc(5);
>    scope(exit) free(a);
> }
foo is pure, but malloc and free aren't individually. If the declaration of the purity of foo is curtailed because malloc and free have to be marked as pure, then it is a failure of the language. In fact there should be an equivalent of @trusted for purity, telling the compiler "trust me, I know that that combination of impure functions is on a whole pure". Marking malloc/free as pure doesn't cut it as these functions cannot, by definition, be pure individually.

But I start to understand where this abomination of purity comes from. Transitivity. I suspect that applying transitivity unthinkingly is not such a good idea as can be seen also with const (immutable is transitive as it describes a property of the data, const is not as it is a property of the means to access the data, not the data itself).



>
>
> How is that any different than
>
> void foo() pure {
>    int[5] a;
> }
>
> ?
>
> I expect that's how it is actually used. (Perhaps it would be better encapsulated in a function along the lines of:
>
> void foo() pure {
>     workWithMemory((int* a) {
>
>     }, 5);
> }
>
>
> And then the malloc/free could be wrapped up a bit better)


October 30, 2021
On Friday, 29 October 2021 at 21:56:10 UTC, Adam Ruppe wrote:
> On Friday, 29 October 2021 at 21:16:49 UTC, H. S. Teoh wrote:
>> Long story short, pureFree makes no sense
>
> What about:
>
> void foo() pure {
>    int* a = malloc(5);
>    scope(exit) free(a);
> }
>
>
> How is that any different than
>
> void foo() pure {
>    int[5] a;
> }
>
> ?


That depends on how malloc and free are implemented... Should you for instance be allowed to do locking in a pure function? Probably not. You cannot call such code in a real time thread. If you cannot call a pure function in a real time thread then I think the advantage is completely lost for system level programming.

The problem here is having a good definition for pure.

October 30, 2021
On Friday, 29 October 2021 at 21:56:10 UTC, Adam Ruppe wrote:
> On Friday, 29 October 2021 at 21:16:49 UTC, H. S. Teoh wrote:
>> Long story short, pureFree makes no sense
>
> What about:
>
> void foo() pure {
>    int* a = malloc(5);
>    scope(exit) free(a);
> }
>
>
> How is that any different than
>
> void foo() pure {
>    int[5] a;
> }
>
> ?


That depends on how malloc and free are implemented... If malloc involves locking
 or system calls (which is difficult to avoid) then the difference is that it cannot be used in real time or other low level code where neither locking or system calls can be used.

The problem here is having a useful definition for pure. What is the purpose for "pure"? With no clear purpose it becomes rather difficult to pinpoint what the boundary ought to be.