Hacker News new | ask | show | jobs
by eco 3288 days ago
Just more pragmatic, I'd say. D isn't a "big agenda language" (to steal a line from Jonathan Blow). It's extremely multi-paradigm (some might argue to a fault). I think that's why you find people saying D is like C++ or D is like C# or D is like Go or D is like Rust. You get a little taste of everything using D.

Want function programming and purity? Check.

Want C style/low abstraction code? Check.

Want extreme C++ metaprogramming? Check.

Want C#'s LINQ? Check.

Want an improved version of C++'s STL? Check.

Want low cognitive load memory management through a GC? Check.

Want highly tailored memory management? Check.

Want high level object oriented abstractions? Check.

Want memory safety? Check.

Want systems programming? Check.

Want rapid prototyping? Check.

D, fundamentally, assumes the programmer knows what approach they should take and lets them do it. There are no "we know better" design decisions in the language. I think this might be because D is so community driven. With no real company backing D was left in the hands of enthusiasts coming from all sorts of different backgrounds to implement ideas they liked.

3 comments

> think that's why you find people saying D is like C++ or D is like C# or D is like Go or D is like Rust. You get a little taste of everything using D.

That's a major problem, and not a feature.

One of C++'s main drawbacks is its size and arcane features, to the point that the language is known for being impossible to master. If all D brings to the table is an agenda to pick off C++'s complexity and drive it up even further then I fail to see what problems that will solve while it creates many others.

But it didn't drive up the complexity. It drastically simplified how a lot of features work. C++ is very difficult to master not because of the number of features in the language (it really isn't even all that featureful compared to other modern languages) but because of the thousands of unexpected details you have to know. Scott Meyers made a career out explaining them (and implored D not to make the same mistake of needing someone like him). That doesn't mean the overarching feature can't be implemented in a simple way that avoids the unintended complexity though. Anybody you ask with knowledge of both D and C++ would say that D's metaprogramming facilities are both drastically easier and more powerful than what C++ offers, for instance. It's actually shocking how much you need to know to fully understand things like template/regular type deduction (which aren't the same), initialization, rvalue behavior, forwarding references (or is it universal references...they came up with the feature before they gave it a name), what is constexper-able, reference collapsing, etc. These things are all straightforward in D because they were either designed without the edge cases and legacy behavior or left out entirely because the problem was tackled in a different, more simple way at a fundamental level.
Want unit testing? Check.

And built in at the compiler level, so it happens when you compile your program as you normally would, the tests show up while compiling, and the output does not contain the code. Always impressed me.

What's D's LINQ equivalent?
The combination of ranges and UFCS led to it just naturally falling out of the language design. It looks like this (adapted from a LINQ example[1]):

    auto names = [ "Burke", "Connor", "Frank", "Everett", "Albert", "George", "Harris", "David"];
    names.filter!(a => a.length == 5)
         .array // convert from lazy range to array so we can sort
         .sort!()
         .map!(a => a.asUpperCase)
         .joiner("\n")
         .writeln;
Ranges enable lazy processing with efficient static dispatch against arbitrary types of ranges. Those individual algorithm functions are basically all template functions that return types tailored to match the input which allows the lazy evaluation to work. When writeln asks for the first element to print it asks joiner which asks map which asks asUpperCase and so on. The results are calculated upon request, not in advance, which helps you forgo a lot of memory allocations for storing temporary results.

UFCS lets you call a function as if it were a member of the first parameter (i.e. fun(x, y) -> x.fun(y)). This lets you write it as if it were chain rather than a series of inside out function calls (i.e. `writeln(joiner(map!(a => asUpperCase(a)(sort!()(array(filter!(a => a.length == 5)(names)))), "\n"))`).

There is exactly two memory allocations in all of that. Once for the initial array and again prior to sorting because it's not reasonable to sort a lazy range. We could have reused the initial array by eagerly removing the items being filtered from it if we wanted.

1. https://msdn.microsoft.com/en-us/library/bb308959.aspx

Ranges and pipeline programming.
Sebastian Wilzbach compiled this comparison of LINQ and D range primitives:

https://github.com/wilzbach/linq