October 25, 2021
On Mon, Oct 25, 2021 at 04:04:12PM +0000, Dukc via Digitalmars-d wrote: [...]
> backwards compatibility is part of my D dream.

Sometimes I wonder about writing a DIP for declaring language version at the top of a module to (almost) guarantee backward compatibility.

But realistically speaking, that would mean a whole ton more of work for the already-scarce current compiler maintainers, so this is probably unlikely to ever happen.

But one can dream...


T

-- 
Without outlines, life would be pointless.
October 25, 2021
On Monday, 25 October 2021 at 16:31:36 UTC, H. S. Teoh wrote:
> Whaddya mean, that's one of the best things to happen to D lately, that gets rid of a whole bunch of evil recursive templates from druntime and Phobos.

lol it is pretty lame really.

> opDispatch is awesome. See: Adam's jni.d.

I don't think I use it in jni since that's all generated. But jsvar and dom both use it pretty heavily and i like it. (but I do wish the error message regression would be fixed)
October 25, 2021
On Monday, 25 October 2021 at 16:13:25 UTC, H. S. Teoh wrote:
> I think the idea behind pure is for the compiler to enforce purity in your code, since human beings are prone to making mistakes.

Strong purity is useful when you use a library that takes a lambda expression that is meant to be used for comparison and the like, so you can prove that the library algorithm is correct if the input expression is strongly pure. D's weak purity does not seem to even require that the function is idempotent (that it has the same effect if called twice), so it does not really provide enough guarantees to verify the correctness of algorithms that take external code as input. So it isn't surprising that people have not found much use for it.

Being able to put constraints on lambda-parameters to functions is very useful in general, but you might want stronger guarantees than D's «pure».


>> > - shared
>> 
>> Agreed, shared is one of the biggest mess in D. This is where it is obvious where the D language designers lack in experience and knowledge. It is also a very complex issue so my advice is to rather to copy an existing design or not at all and rely more on manual concurrent "safety".
>
> Agreed.

Shared is actually one of the more promising aspects of D where it could stand out, but you need to be very pedantic when designing it (theoretical) and make sure that the resulting type system is sound. Meaning, you cannot allow "convenient hacks" and "pragmatic exceptions". It remains to be seen if D can adopt enough strong guarantees (be more principled) to make "shared" useful.

I think it is possible to define shared in a potent manner, that gives optimization opportunities, but I am not sure if the majority of D users would embrace it.


> Unfortunately this can't be changed without subtle breakage of a whole ton o' code.  So, sadly, not gonna happen.

I actually think a lot of things can be changed with invisible-breakage if you plan for it. All you have to do is to extend the compiler-internal semantics to accept new features, without contradicting the old compiler-internal semantics. When it comes to syntax you could just use a language version identifier per source file.


> It's convenient for quick-n-dirty concurrent OO code where performance isn't critical. But if you want more modern concurrent techniques / high performance, yeah, it's not of much use.  It's arguably dated technology.

Synchronized has runtime debt. That is a higher-level language design choice and not really a system level design choice. You could redefine the semantics so you don't have to account for it in the runtime, I think.


October 25, 2021

On Monday, 25 October 2021 at 15:59:35 UTC, Adam D Ruppe wrote:

>

On Monday, 25 October 2021 at 14:52:55 UTC, Dennis wrote:

>

Apart from pure, I agree with everything on that list, and would even add a bunch of things:

Damn, like do you actually use any D features?

I know right? I'm starting to think maybe I should switch to Zig :p

I have used some of these features, but then they ended up being annoying or unnecessary anyway:

  • Module constructors make it hard to see what's happening when the program starts. I once submitted a D program for an assignment and it failed because it loaded local files for unittests in a module constructor, which I forgot to remove because I only looked at main.
  • opDispatch: suddenly member introspection (__traits(hasMember)) succeeds when you don't expect it, so now it's accepted as a broken InputRange/OutputRange/whatever in generic functions.
  • alias this: I use it, I encounter bugs and weird consequences like unexpected endless recursion, I remove it.

It's kind of like your quote from Discord:

>

so many times a questions comes up in learn like "how would i do this with metaprogramming" and im like "bro just use a standard function like you would in javascript"

Nowadays whenever I can express something with plain old structs/functions/arrays/enums, I do that instead of anything fancy, and I'm liking the result.

October 25, 2021

On Monday, 25 October 2021 at 17:23:01 UTC, Dennis wrote:

>
  • Module constructors make it hard to see what's happening when the program starts. I once submitted a D program for an assignment and it failed because it loaded local files for unittests in a module constructor, which I forgot to remove because I only looked at main.

But I think you need it if you want to create module-specific allocators? Otherwise users of the module have to remember to add initializers and destructors externally.

To prove that at compile time takes a very complex type system, I think? And checking it at runtime has a performance impact (unless you do runtime patching, which gets ugly real fast).

October 25, 2021
On Monday, 25 October 2021 at 17:23:01 UTC, Dennis wrote:
> - opDispatch: suddenly member introspection (`__traits(hasMember)`) succeeds when you don't expect it

Yea, I kinda wish it didn't affect hasMember. This is one place where I pretty consistently use a template constraint to at least prohibit "popFront" in there. Hacky i know.

My rule lately has also been opDispatch goes on its own thing. Like dom.d used to do

element.attribute

through opDispatch. Now it only allows it through

element.attrs.attribute

to help to contain it.

I still think it is cool tho lol

> Nowadays whenever I can express something with plain old structs/functions/arrays/enums, I do that instead of anything fancy, and I'm liking the result.

indeed, generally good strats.
October 25, 2021
On Mon, Oct 25, 2021 at 05:23:01PM +0000, Dennis via Digitalmars-d wrote: [...]
> - Module constructors make it hard to see what's happening when the
>   program starts. I once submitted a D program for an assignment and
>   it failed because it loaded local files for unittests in a module
>   constructor, which I forgot to remove because I only looked at main.

But that's hardly the fault of module ctors. You're not supposed to do unittest-related stuff outside of unittest blocks and stuff versioned by `version(unittest)`.  What you *should* have done is:

	version(unittest) static this() {
		// load stuff you need for unittests here
	}

	// regular module ctor (yes you can have multiple static ctors,
	// they just get concatenated together -- this is actually a
	// feature).
	static this() {
		// DON'T do stuff here that's unittest-related!
	}


> - opDispatch: suddenly member introspection (`__traits(hasMember)`)
> succeeds when you don't expect it, so now it's accepted as a broken
> InputRange/OutputRange/whatever in generic functions.

I suspect part of this problem is caused by the lame specialcased behaviour of template instantiation errors being ignored when it's opDispatch.  It's D's version of C++'s SFINAE, which leads to all sorts of stupidity like a typo inside opDispatch suddenly causing a dispatched member to disappear from existence 'cos the compiler swallows the error and pretends the member simply doesn't exist.  So if you're introspecting the member, the introspection code doesn't even see it. Which leads to hours of head-scratching over "I obviously declared the darned member, why does the code NOT see it?!?!".

Errors in opDispatch seriously ought to be treated as errors. If some member isn't supposed to be part of the dispatch, it should be checked for in the sig constraint or something like that, instead of the compiler silently swallowing the error.


> - alias this: I use it, I encounter [bugs](https://issues.dlang.org/show_bug.cgi?id=19477) and weird consequences like unexpected endless recursion, I remove it.

Yeah `alias this` beyond the most trivial of usecases leads to nothing but pain.


> It's kind of like your quote from Discord:
> 
> > so many times a questions comes up in learn like "how would i do this with metaprogramming" and im like "bro just use a standard function like you would in javascript"
> 
> Nowadays whenever I can express something with plain old structs/functions/arrays/enums, I do that instead of anything fancy, and I'm liking the result.

Isn't this what we're supposed to be doing anyway?  I mean, you can do *anything* with mixin(), but most of the time you shouldn't because there are much more readable and maintainable ways to accomplish the same thing. :-D


T

-- 
To err is human; to forgive is not our policy. -- Samuel Adler
October 25, 2021

On Monday, 25 October 2021 at 16:31:36 UTC, H. S. Teoh wrote:

> >
  • Function body literals ({} instead of () {})

One can argue about syntax till the cows come home, and we wouldn't come to an agreement. :D

I don't mind the look of it that much, but it's super ambiguous. The current parser has a hack to distinguish them from struct initializers, and fails to parse it in places where it the grammar allows it e.g. Issue 14378.

>

These are awesome! Lets you construct array arguments on the stack where a function accepting only an array would force a GC (or other) allocation.

You can pass an array literal to a scope int[] parameter @nogc.

>

But is() expressions are also the backbone of much of D's metaprogramming prowess, so unless you come up with something better, they are here to stay.

Yeah, they'd need a replacement, but man they are so hard to use currently. Was it is(T == E[], E) or is(E[] == T, E)? Why does E become in-scope when the is() is in a static if, but not in a template constraint? Why is function in an is() expression a function type while elsewhere it's a function pointer type?

> >
  • alias reassignment

Whaddya mean, that's one of the best things to happen to D lately, that gets rid of a whole bunch of evil recursive templates from druntime and Phobos.

That could be done in other ways, without introducing AST mutation.

> >
  • opCall

opCall is needed for function objects. How else are you supposed to reify functions when you need to?!

I don't know, I never made custom function types.

> >
  • opApply

This is actually useful in cases where the range API may not be the best way to do things (believe it or not, there are such cases).

You can still call an opApply-like function directly without foreach.

>

One example is iterating over a tree without needing to allocate more memory for the iteration.

I tend to use recursion for that, since my trees don't get that deep.

October 25, 2021
On Monday, 25 October 2021 at 17:17:14 UTC, Ola Fosheim Grøstad wrote:
>
> Shared is actually one of the more promising aspects of D where it could stand out, but you need to be very pedantic when designing it (theoretical) and make sure that the resulting type system is sound. Meaning, you cannot allow "convenient hacks" and "pragmatic exceptions". It remains to be seen if D can adopt enough strong guarantees (be more principled) to make "shared" useful.
>
> I think it is possible to define shared in a potent manner, that gives optimization opportunities, but I am not sure if the majority of D users would embrace it.
>
>
>> It's convenient for quick-n-dirty concurrent OO code where performance isn't critical. But if you want more modern concurrent techniques / high performance, yeah, it's not of much use.  It's arguably dated technology.
>
> Synchronized has runtime debt. That is a higher-level language design choice and not really a system level design choice. You could redefine the semantics so you don't have to account for it in the runtime, I think.

There are a few rules I've discovered with concurrent programming.

Always take the lock, don't dwell into atomic operations/structures too much. 99% of the cases should be handled by traditional mutex/semaphores and possible language layers above that.

Synchronized classes are good because the lock is implicit. However, it might be inefficient if you use several methods after each other which means several lock/unlock (Acquire/Release or whatever you name it) after each other.

One of the best designs in Rust was to combine the borrowing with acquiring the lock. Then you can borrow/lock on a structure do all the operations you want as in normal single threaded programming and it will be released automatically when the borrow goes out of scope. This is is a genius design and I'm not sure how to pry it into D.

Then we have the shared structs/classes in D where all basic types are forced to be atomic, which is totally insane. Lock free algorithms often consist of both normal and atomic variables. Also if you have several atomic variables, the operations on them together often introduce race conditions and your structure is not thread safe at all. When you are in lock free territory you are on your own and shared should only mean, this can be safely used from several threads at the same time. Compiler should do nothing more. Then we can also combine lock free structures with locked structures and where these are appropriate depends on the use case. Basically, the compiler should stay away from any further assumptions.
October 25, 2021

On Monday, 25 October 2021 at 17:45:22 UTC, H. S. Teoh wrote:

>

I suspect part of this problem is caused by the lame specialcased behaviour of template instantiation errors being ignored when it's opDispatch. It's D's version of C++'s SFINAE, which leads to all sorts of stupidity like a typo inside opDispatch suddenly causing a dispatched member to disappear from existence 'cos the compiler swallows the error and pretends the member simply doesn't exist. So if you're introspecting the member, the introspection code doesn't even see it. Which leads to hours of head-scratching over "I obviously declared the darned member, why does the code NOT see it?!?!".

Errors in opDispatch seriously ought to be treated as errors. If some member isn't supposed to be part of the dispatch, it should be checked for in the sig constraint or something like that, instead of the compiler silently swallowing the error.

It's actually worse than that. Errors inside opDispatch are gagged and cause member lookup to fail...unless they're nested inside another template, in which case member lookup will succeed, and you will only get the error when you try to actually access the member:

struct S1
{
    template opDispatch(string member)
    {
        // error is gagged by compiler
        static assert(0);
    }
}

// member lookup fails
static assert(__traits(hasMember, S1, "foobar") == false);

struct S2
{
    template opDispatch(string member)
    {
        // inner template
        auto opDispatch()()
        {
            // error is not gagged
            static assert(0);
        }
    }
}

// member lookup succeeds
static assert(__traits(hasMember, S2, "foobar") == true);
// ...but usage triggers the error
auto _ = S2.init.foobar;

It's special cases inside of special cases--a complete mess.