April 18, 2019
On Thursday, April 18, 2019 4:07:57 AM MDT Eugene Wissner via Digitalmars-d wrote:
> On Thursday, 18 April 2019 at 08:53:37 UTC, Jonathan M Davis
>
> wrote:
> > There _are_ rare cases where a piece of code that isn't technically @noglobal is actually able to follow the compiler's guarantees (e.g. std.datetime's LocalTime() is conceptually pure because it always returns exactly the same value every time it's called, but it has to create that value the first time that it's called, because it was determined unacceptable to have static constructors in Phobos - and that means casting to pure).
>
> It isn't conceptually pure, it depends on the state of the word after compilation, it is just impure.

I don't see why the word after compilation would matter one whit. The semantics are the same whether you use a static constructor to initialize the underlying variable or do it lazily as it's currently doing (it's just that lazy way requires a cast). It always returns exactly the same object every time. That's what matters. And actually, given how pure works in D, it could legally return a newly allocated object every time so long as its value were the same (that would defeat the purpose of the singleton, put it would be perfectly legal as far as D's pure goes and would even negate the need for a cast). If you're worried about absolute functional purity, D already threw that out the window when it allowed pure functions to do imperative stuff. Instead, D just cares about how the function behaves from the outside - LocalTime() behaves correctly with regard's to D's pure and it's guarantees. It only needs a cast because it does it lazily instead of using a static constructor like it did originally.

- Jonathan M Davis



April 18, 2019
On Thursday, April 18, 2019 4:05:32 AM MDT Eugene Wissner via Digitalmars-d wrote:
> As it is now, pure is not like immutable or const, it is like @safe, it is just an annotation that tells, whether the code does something you don't expect. Nobody knows if pure will mean one day more or something different, probably not, because even official libraries break purity.

pure means that the function does not access mutable, global state except via the function's arguments. That's it. nd the compiler _is_ able to do stuff with that and does so right now. It will elide function calls (though under such restricted circumstances that it's not really worth it), but the bigger gains come from how it helps the type system. For instance,

Foo* foo(int i) pure { return new Foo(i); }

immutable f = foo();

compiles just fine, because the compiler is able to determine that the return value of foo is unique and that therefore the cast is safe. That allows for far more complicated functions which can be used to create immutable objects without requiring any explicit casting. pure has been improved in a number of small ways like that over time as people have figured out how to make the compiler better leverage the guarantees that pure provides.

If what you're looking for is to be able to elide function calls all over the place, then I doubt that you'll ever get that - if nothing else, because that would require more code flow analysis than Walter is typically willing to allow. Something like func(42) * func(42) will result in a call being elided if func is pure, but even splitting it up onto two lines kills that. e.g.

auto f = func(42);
f = func(42) * f;

won't elide anything and likely never will. As such, the gains come from pure tend to be very localized, and slapping it on everything is going to tend to be of minimal benefit. It does ensure that such code doesn't access non-immutable globals if they're not passed to it, which might be nice to know, but how useful it is is debatable, and it makes it a royal pain if you need that code to access globals later for caching or I/O or whatever.

> I always have to annotate the main function as impure. So what? It is still better to annotate a few functions as impure than the most code as pure.

Except that most programs aren't written in a way that it makes any sense for the majority of their code to be marked as pure. I/O alone tends to kill that.

> Casting away purity was meant by me only for debugging and similar things, not as permanent solution. Even Haskell provides perfomUnsafeIO for that. And if a pure function becomes one day impure, the code needs refactoring anyway. But I see, there is no way for someone who doesn't care about purity to disable it. Maybe compiler should provide some switch that disables purity, nogc and so forth for the code and all its dependencies.
>
> I actually agree with you that D is an imperative programming language and as I said above I think pure brings more harm than good and I could live without it at all. It is just there, it doesn't work as is and of course nobody knows, how it is supposed to work.

I don't get this. pure is fairly well understood. The guarantees that it provides are well understood. The only thing that's really changed over time (other than the introduction of "weak" purity, which is what made it so that pure is really just @noglobal rather than having anything to do with functional purity) is the set of things that the compiler is able to do based on the fact that a function is marked with pure. That list keeps getting longer (if slowly). It started with function call elision (based on "strong" purity, which is what pure had been originally) and has grown over time. As far as writing code goes, the hard part is when you try to write code that the compiler doesn't think is pure, but you're trying to write it in a way that it maintains the guarantees that the compiler makes with pure and bases its assumptions off of. _That_ can be hard to do, because it means fully understanding what the compiler will do based on pure, and that list grows over time. So, it's almost never something that really makes sense.

But if you're just using pure without trying to do any casts, just letting the compiler determine what's pure and isn't (either by using templates or by having it yell at you when you mark something that's pure that isn't), it's very straightforward. It works exactly as intended, and plenty of people know how it's supposed to work. The only real problem with it is its name, because newcomers think that it's all about functional purity, which it really isn't. It's just providing a set of guarantees about a function that the compiler can build on, and one of the things that it can do is figure out that a function is functionally pure and then do stuff like elide calls - but it can do more than that (like implicitly casting to immutable under some circumstances), and that other stuff that it can do doesn't necessarily have much to do with functional purity, just the knowledge that the only arguments to the function are the ones passed to it.

- Jonathan M Davis



April 18, 2019
On Thursday, 18 April 2019 at 14:52:53 UTC, Jonathan M Davis wrote:
> pure means that the function does not access mutable, global state except via the function's arguments. That's it. nd the compiler _is_ able to do stuff with that and does so right now.

That compiler actually does something with pure is new to me. So I was wrong here.

> It will elide function calls (though under such restricted circumstances that it's not really worth it), but the bigger gains come from how it helps the type system. For instance,
>
> Foo* foo(int i) pure { return new Foo(i); }
>
> immutable f = foo();

I'm not really convinced this pattern is useful, but fine there are may be different use cases, I'm not familiar with. But this doesn't even work under similar circumstances:

int* foo() pure
{
    return new int(5);
}

immutable f = foo();

gives: cannot use non-constant CTFE pointer in an initializer `&[5][0]`

> Something like func(42) * func(42) will result in a call being elided if func is pure, but even splitting it up onto two lines kills that. e.g.
>
> auto f = func(42);
> f = func(42) * f;

I've just tested it and if I don't miss something, no call elision is done. GDC eliminates actually the call if it has the source code, but GDC does it whether the function is pure or not. I also don't see how it may be possible.

size_t foo() pure
{
    return cast(size_t) new int(5);
}

It is a perfectly valid pure function, that doesn't depend on any global state, doesn't have arguments, without casting away the impurity, but it returns different values every time.

> I don't get this. pure is fairly well understood.

Thinking of the last discussion about pure, just before pureMalloc was introduced, I got a different feeling, but well, it kind of does, what the specification says.

I also don't find Haskell's purity "insane", but actually very useful and solid, so I might be biased torwards "strong purity" or "no purity at all".


April 18, 2019
On Thu, Apr 18, 2019 at 06:33:33PM +0000, Eugene Wissner via Digitalmars-d wrote: [...]
> Thinking of the last discussion about pure, just before pureMalloc was introduced, I got a different feeling, but well, it kind of does, what the specification says.
> 
> I also don't find Haskell's purity "insane", but actually very useful and solid, so I might be biased torwards "strong purity" or "no purity at all".
[...]

It may help to understand the historical context in which D's purity, particularly "weak purity", arose. What D calls "strong purity" is equivalent to Haskell's purity, and was the original definition of 'pure' in D.  What we call today "weak purity" came as a relaxation of strong purity in order to increase the scope of 'pure's applicability, and by so doing, increase the amount of code that can be made strongly pure.  More details in this article:

	http://klickverbot.at/blog/2012/05/purity-in-d/


T

-- 
Never trust an operating system you don't have source for! -- Martin Schulze
April 18, 2019
On Thursday, April 18, 2019 12:33:33 PM MDT Eugene Wissner via Digitalmars-d wrote:
> On Thursday, 18 April 2019 at 14:52:53 UTC, Jonathan M Davis
>
> wrote:
> > pure means that the function does not access mutable, global state except via the function's arguments. That's it. nd the compiler _is_ able to do stuff with that and does so right now.
>
> That compiler actually does something with pure is new to me. So I was wrong here.
>
> > It will elide function calls (though under such restricted circumstances that it's not really worth it), but the bigger gains come from how it helps the type system. For instance,
> >
> > Foo* foo(int i) pure { return new Foo(i); }
> >
> > immutable f = foo();
>
> I'm not really convinced this pattern is useful, but fine there are may be different use cases, I'm not familiar with. But this doesn't even work under similar circumstances:
>
> int* foo() pure
> {
>      return new int(5);
> }
>
> immutable f = foo();
>
> gives: cannot use non-constant CTFE pointer in an initializer `&[5][0]`

I take it that you were trying to initialize a variable that has to be initialized during CTFE? CTFE doesn't particularly like casts, and it couldn't even have pointers transfer from compile-time to runtime until fairly recently. So, the code generation probably inserts a cast that CTFE doesn't like. It works at runtime, and it should probably be made to work at compile-time. It's also more useful with far more complex pieces of code - like if you were initializing an immutable AA with a bunch of values or some other piece of data that required more than a constructor. It can be done without pure, but that then requires a cast, and it's up to the programmer to make sure that the data is unique, and it's safe to cast it to immutable, whereas with pure, the compiler can verify that for you.

> > Something like func(42) * func(42) will result in a call being
> > elided if func is pure, but even splitting it up onto two lines
> > kills that. e.g.
> >
> > auto f = func(42);
> > f = func(42) * f;
>
> I've just tested it and if I don't miss something, no call elision is done. GDC eliminates actually the call if it has the source code, but GDC does it whether the function is pure or not. I also don't see how it may be possible.

It's my understanding that calling a strongly pure function multiple times within the same expression will result in the compiler eliding more than the first call (though I haven't tested it recently). The function's parameters must therefore be immutable (or implicitly convertible to immutable as would be the case with ints), or it can't do it. So, _very_ few functions will qualify, and how often does anyone call the same function multiple times with the same arguments in a single expression? For it to really be useful, you'd have to be doing a lot of math code with pure functions or something else that involved a lot of immutable variables (which most code doesn't have) where it made sense to call the same function multiple times in the same expression (which most code doesn't do). It would be more useful with code flow analysis, because then you'd potentially get call elision across an entire function, but given Walter's stance on code flow analysis, I doubt that it's ever happening. In general, while it is my understanding that the compiler will elide multiple, identical calls to a strongly pure function within a single expression, that just isn't very useful in practice - which is part of why the whole idea that pure is there for actual, functional purity is kind of bogus. It's also why pure was expanded beyond strongly pure, because strongly pure functions don't happen often without weakly pure function helpers, and even then, they don't happen often. In reality, the biggest benefits to pure probably come from constructing immutable objects, with the secondary benefit being that you know that a section of code can't access any globals except through function arguments.

> size_t foo() pure
> {
>      return cast(size_t) new int(5);
> }
>
> It is a perfectly valid pure function, that doesn't depend on any global state, doesn't have arguments, without casting away the impurity, but it returns different values every time.

Well, you found a loophole then. The fact that you can allocate in a pure function is extremely useful, but it does come with the caveat that even though the allocated objects will always have the same value, they won't be the exact same object. So, by casting to get the pointer value, you can indeed cheat it. I'm not sure how possible it is to have the compiler prevent it, but it's also not something that's likely to be a problem in practice. It _is_ a loophole though brought on by one of the aspects of pure that was made more lax in order to increase its usefulness.

> > I don't get this. pure is fairly well understood.
>
> Thinking of the last discussion about pure, just before pureMalloc was introduced, I got a different feeling, but well, it kind of does, what the specification says.

The problem with pureMalloc is that you're trying to emulate what happens when allocating memory via new, which technically violates not accessing mutable, global state. It's just that it was decided that they way that it did it with new was acceptable, since mutating the GC bookkeeping wasn't really part of the program's logical state (though that does lead to the loophole you mentioned above). pureMalloc is then having the programmer do something similar without the compiler's help and without the GC cleaning up after it (so, it needs a corresponding free call). Call elision in particular is deadly, and while it's not going to happen often, having it happen in a way that would result in memory being freed twice or not freed at all would be a big problem. So, the whole pureMalloc thing is a bit of a mess. Certainly, it's not dealing with pure in any kind of normal manner, and it's trying to convince the compiler that something can be considered pure without the compiler then having problems due to the fact that it isn't actually pure. As far as just using pure goes and what that does, it's well understood overall. It's trying to trick the compiler where things get messy.

> I also don't find Haskell's purity "insane", but actually very useful and solid, so I might be biased torwards "strong purity" or "no purity at all".

I programmed in Haskell a fair bit in college. I think that it was a good experience, because it greatly increased my ability to write functional code, and it greatly increased how good I was with stuff like recursion. That being said, I think that it's an insane way to program in practice (monads being a prime example of some of what happens when you go down that route). The fact that D is multiparadigm means that you can use such idioms where they make a lot of sense (e.g. a lot of range code tends to be fairly functional in nature), but it's not forced on you. I really don't understand anyone who would _want_ to program in Haskell (or any language like it) as much more than a learning experience.

Regardless, D's pure really doesn't have much to do with functional purity at this point, even if that was why it was originally put in the language. Having it be @noglobal would be far more accurate, though it _can_ be used to have actual, functional purity in some cases. It's useful to have, but if we were going to ditch one of the function attributes, I'd probably put it near the top of the list. I don't want to lose it, but it tends to be truly useful in a rather limited number of circumstances, and I would hate to see it forced on code in general.

- Jonathan M Davis



April 18, 2019
On 4/18/19 2:33 PM, Eugene Wissner wrote:
> On Thursday, 18 April 2019 at 14:52:53 UTC, Jonathan M Davis wrote:
>> pure means that the function does not access mutable, global state except via the function's arguments. That's it. nd the compiler _is_ able to do stuff with that and does so right now.
> 
> That compiler actually does something with pure is new to me. So I was wrong here.

There's a lively discussion at https://github.com/dlang/dlang.org/pull/2627. Specialists welcome.

April 19, 2019
On 4/15/19 11:59 PM, Mike Franklin wrote:
> I think I may have found a simple migration path to @safe by default.  I'm still thinking it through, but if I can justify it, I will write a DIP.
> 
> `@safe` by default is a no-brainer in my opinion, but `pure` and `immutable` by default are less obvious.  With the aforementioned potential DIP, I have an opportunity to correct purity and mutability defaults as well...but is that something we want to do?
> 
> Can anyone save me some trouble and articulate why it would be bad to have `pure` and/or `immutable` by default?
> 
> Thanks,
> Mike

I haven't participated much to this but here's a thought. This migration is again in the category "let's change things and evaluate the costs". It is so much better to go with "let's add and enjoy the benefits".

It's cheap to make @safe and pure the defaults for a given module. Plant this at the beginning:

pure: @safe:

This is easy to enforce via scripting etc.

The problem is there's no opting out of pure. (There's opting out of @safe by using @system or @trusted.) So this is where the hammer should go:

pure(false)

We have discussed this on and off perhaps a couple dozen times. I've discussed it with Walter a few times and it is clear that some way of getting out of pure is sorely needed.

This is what the DIP should be about.
April 19, 2019
On Friday, 19 April 2019 at 12:23:08 UTC, Andrei Alexandrescu wrote:
> On 4/15/19 11:59 PM, Mike Franklin wrote:
>> I think I may have found a simple migration path to @safe by default.  I'm still thinking it through, but if I can justify it, I will write a DIP.
>> 
>> `@safe` by default is a no-brainer in my opinion, but `pure` and `immutable` by default are less obvious.  With the aforementioned potential DIP, I have an opportunity to correct purity and mutability defaults as well...but is that something we want to do?
>> 
>> Can anyone save me some trouble and articulate why it would be bad to have `pure` and/or `immutable` by default?
>> 
>> Thanks,
>> Mike
>
> I haven't participated much to this but here's a thought. This migration is again in the category "let's change things and evaluate the costs". It is so much better to go with "let's add and enjoy the benefits".

Most D code is yet to be written. Safe by default is sth. that all new D code should be

> It's cheap to make @safe and pure the defaults for a given module. Plant this at the beginning:
>
> pure: @safe:

NOPE. That doesn't work as DMD doesn't set pure for member functions.

---
pure:

struct Foo {
    int bar() { return 42; }
}

void main()
{
   Foo().bar(); // Error: pure function D main cannot call impure function onlineapp.Foo.bar
}
---


https://run.dlang.io/is/cgESWs
April 19, 2019
On Friday, 19 April 2019 at 13:09:50 UTC, Seb wrote:
> On Friday, 19 April 2019 at 12:23:08 UTC, Andrei Alexandrescu wrote:
>> pure: @safe:
>
> NOPE. That doesn't work as DMD doesn't set pure for member functions.

And it isn't good for the documentation.
April 19, 2019
On Friday, 19 April 2019 at 12:23:08 UTC, Andrei Alexandrescu wrote:
> (There's opting out of @safe by using @system or @trusted.)

This still doesn't work correctly for templates, which should be inferred based on input, not forced one way or another.

We should be doing

attr(true | false | null)

three way!


But yeah, that's where I would focus too. Then your modules can opt in pretty easily.