Hacker News new | past | comments | ask | show | jobs | submit login
Better C – A subset of D Programming Language (dlang.org)
249 points by dgellow on April 28, 2020 | hide | past | favorite | 352 comments



One thing that really annoyed me about D is that its documentation lists basically every function with an 'auto' return type. Auto should be completely banned from documentation, it's a complete hindrance. It's the single biggest contributor to why I stopped using D for personal projects - I was so tired of having to hunt down what functions are going to return, sometimes having to resort to just using it and looking at what the compiler complains about.

And that's a huge shame. Because in general I really liked using D.


The use of Auto is requires in some places because the standard library returns types that cannot be named in the context of the calling function. This happens for example with algortihms that return a custom Range implementation that is declared within the scope of the function implementing the algorithm.

I am not sure what to make of this pattern. At least the documentation should be more explicit about these Voldemort types. Documentation has other issues as well. The standard documentation generator doesn't cope well with version statements (conditional compilation), potentially skipping docs for stuff that wouldn't be compiled into a particular build variant.


I'm glad that Rust has no `auto`. I find this:

    fn map<U, T, F, I>(it: I) -> impl Iterator<Item=U> 
        where I: Iterator<Item=T>, U: From<T>
    {
        it.map(|t| From::from(t))
    }
infinitely more readable than

    fn map<U, T, F, I>(it: I) -> auto
        where I: Iterator<Item=T>, U: From<T>
    {
        it.map(|t| From::from(t)) 
    }
The type signature of the first one clearly tells me that the return type is an `Iterator<U>`, even though the actual type cannot be named because of the anonymous closure.

The second one leaves me guessing what the return type is.

If the actual type cannot be named, it is rarely the case that this is all there is to it. Usually, users are expected to use that type "somehow" (it is a `Range`?), and that means that there are some interfaces that these types implement.


This wouldn't work for D. D doesn't constrain return types to something less than what they are. A Range is not just an Iterator, it has optional pieces that depend completely on the given type.

For example, the return of map could provide indexing, or it could provide forward and backward iteration, or it might have methods that are completely unrelated to the type.

There is no good reasonable and non-confusing way to describe all the things map could return depending on the input. It's much better to just describe it conceptually in the human-readable docs, and let the person understand the result.

I'll note that just above the function map in D's source is the documentation. You just need a little more context, and it will describe what map returns in a much more (IMO) useful fashion than a return type that might be several lines long and consist of various static conditionals:

"The call map!(fun)(range) returns a range of which elements are obtained by applying fun(a) left to right for all elements a in range."

This is the difference between duck typing and generics.


> For example, the return of map could provide indexing,

You can provide more interfaces in Rust:

    fn map(...) -> impl Iterator<Item=U> + Index<Target=U>
but you can't provide "conditional" interfaces (for most interfaces at least), e.g., this won't work:

    fn map(...) -> impl Iterator<Item=U> + ?Index<Target=U>
where `?Index` reads as "maybe implements Index".

To allow that you would essentially need to say that "if the input implements `Index`, the output implements `Index`":

    fn map<U, T, F, I, O>(it: I) -> O
        where I: Iterator<Item=T>, U: From<T>,
              O: impl Iterator<Item=U>,
              I:?Index<Target=T> -> O:?Index<Target=U>
    {
        it.map(|t| From::from(t))
    }
The type system implementation already supports these types of constraints, but there isn't a language extension that exposes that. I don't see any fundamental reasons that make this impossible, but there are many trade-offs involved.

Notice that, for example, the output Range does not implement the same interfaces as the input range, e.g., the input Range implements an `Index` interface over a range of `T`s, but the output Range implements an `Index` interface over a range of `U`s. In D this is super implicit in the implementation details (body) of an equivalent `map` function, but in Rust it needs to be part of the type signature to avoid changes to a function body to silently cause API breaking changes. In D, you could change the body of map to map only from Range(T) -> Range(T), without changing its interface, and that would break all code using it to map a Range(T)->Range(U).


Though it doesn't work for D, it could work for the documentation of D (what is being discussed), in some usefully hand-wavy way.

If I'm working in a typed language, and are dealing with functions max(a, b, c) and list(a, b, c), I would expect the documentation to say that one returns T, whereas the other a list(T). If it says auto, then I'm guessing from the names.

Maybe the target audience is programmers familiar with dynamic languages, who don't care so much and are used to reading the descriptions of functions about what is returned.


In rust this would be expressed as multiple impl blocks with different generic parameters which show up as such in the documentation.

https://doc.rust-lang.org/std/vec/struct.Vec.html#implementa...


What am I looking at there? Are those all traits on Vec that I then have to parse mentally so I can understand what I can do with it? Are all those pages basically to say "Vec works like an array of T"?

I've dealt with generics in other languages such as Swift and C#, and they were substandard to D's templates IMO. I remember in C#, I could not get a simple generic function that accepted both a string and Int to work, so I just gave up and wrote multiple functions without generics.

I'm sure some people find this documentation helpful, but it doesn't look as useful to me as map's simple one-liner.


> What am I looking at there? Are those all traits on Vec that I then have to parse mentally so I can understand what I can do with it? Are all those pages basically to say "Vec works like an array of T"?

No. The type which tells you that Vec works like a slice of T is https://doc.rust-lang.org/std/vec/struct.Vec.html#impl-Deref

The others are separate abstract operations which are available (implemented) on vecs e.g. AsRef/AsMut denote that you can trivially get a (mutable) reference to the parameter from a vec. The implementations are similarly trivial (https://doc.rust-lang.org/src/alloc/vec.rs.html#2348-2374).

> I'm sure some people find this documentation helpful, but it doesn't look as useful to me as map's simple one-liner.

Do you mean this one?

    auto auto map(Range) (Range r)


I’ve not looked much into D, but I’ve really been enjoying Rust.

I think the main takeaway is that there are very different ways of approaching language design. In Rust there was a decision to make the function signature the single place which defines the guaranteed input and output types to a function, but that is a trade-off. It encourages a more complex type system, as the flexibility of functions is on a sense constrained by the type system. Personally I like that explicitness, since there is only one place to look. In the future features like const generics and GATs will make that more powerful.

But on the other hand, D appears to be able to support much more complex types (possibly dependent types?) by not requiring that the type system can express them directly. In a sense the whole language can be used to define types. That’s a cool thing to be able to do, even if it means having to inspect documentation and method bodies to work out what they do.


On the "auto auto", I'm pretty sure I filed a bug report on that. There are alternate D docs that don't produce the "auto auto" (and it goes without saying that it isn't that way in the code itself).


No this one:

"Returns: A range with each fun applied to all the elements. If there is more than one fun, the element type will be Tuple containing one element for each fun."


To get that degree of flexibility in Rust you’d have to turn to macros, then the return type will depend on the generated code. That could be pretty much anything, so you’d need to read the docs anyway. Perhaps the languages are not that different after all.

D’s syntax for the template body looks much more similar to normal D code, in Rust the pattern macro syntax is like a language to itself and procedural macros need a fair bit of boilerplate, including explicit “quote” blocks.


This looks insane, mostly because there must be some repeated code in all these impls.

The D way of solving this is to statically query the properties of the passed in type at compile time whenever a part of the template needs to be specialized. It can make for very concise code, but you can't name the exact input type with this approach.


I don't think that's what the OP is talking about.

They want to have a generic function that returns opaque types implementing different interfaces depending on the inputs. I've replied to that above.


Rust does have auto; "let" and function literal parameter type inference, for a start. It just doesn't let you use it in the return type position.


That's the salient point of this thread though; Rust doesn't have type inference in any position that shows up in API documentation. (The closest thing Rust has is return types of `impl Trait`, but even that imposes a contract that the caller must adhere to and that the callee cannot see through.)


Which helps out the documentation side, but destroys code readability. Particularly since rust users appear to really like creating long method call chains, frequently with a closure or two sprinkled in. Take this "example" https://github.com/diwic/dbus-rs#server. For a beginning user of the library that is nearly impenetrable without breaking each of those calls apart. Even if your pretty familiar with rust you still have to break it apart and explicitly place the types in the "Let" statements to know the intermediate types in order to look up the method documentation.

This style of coding is so bad, that it turns out the example has a syntax error. Good luck finding it without the compiler or a quality editor though. Worse, the example doesn't actually work due to further bugs.

Anyway, rust by itself may be ok. Some of the core concepts are good, but the way people are using it is leading to inpenteratble messes of code. Code like the above combined with what seems excessive/unnecessary use of generics create problems for more advanced usage when it comes to learning and modifying a piece of code. Some people have blamed this on the language's learning curve, but I'm not sure that is appropriate. By itself the language is fairly straightforward, the difficulties occur when people are working around the language and pile in masses of write only code.

That particular code block IMHO is why rust is going to have a hard time truly gaining widespread usage. Even as someone somewhat familiar with rust, moving the example into a program, and modifying it in fairly trivial ways took me the better part of a day.


> Even if your pretty familiar with rust you still have to break it apart and explicitly place the types in the "Let" statements to know the intermediate types in order to look up the method documentation.

Maybe this is just me misreading your phrasing, but why would you actually have to break it apart into `let` statements? You can look up the types without modifying the program. Or are you talking about asking the compiler for the types with the `let _: () = ...` (or similar) trick? At that point you can just ask an IDE, also without modifying the program.


What’s the syntax error? I’m not a Linux user and there’s two different examples, but I am curious!

Most code samples get automatically tested, but READMEs currently do not.


`auto` is just a keyword, that's used in D and C++ to implement many many different language features.

Rust does not have

    fn foo() -> let { ... }
where

    fn foo() -> let { 0_i32 }
    let x: i32 = foo();
    fn bar() -> let { 0_f32 }
    let y: f32 = bar();
That is, you can't have an opaque function return type, that's both opaque, but simultaneously the user can name and use all interfaces from.

If you change the implementation of `bar` with such a feature to return `i32` instead, all calling code of `bar` would break. And that's precisely why Rust doesn't have D/C++'s `auto` in return position.


OP's point was about auto being littered in documentation and not in the code itself.

Having auto is a boon for certain design aspects. As system level programming language D offers everything in betterC mode. Of course it can offer more. But a small community can do only so much.


It's not "littered" in the documentation for anyone who understands the basics of D's ranges. auto is the correct choice here. Range functions are lazy, meaning they are almost always used in a chain of function calls that ends in something like `.array` to get a concrete type -- an array in this case. At no point in that process do you care about the actual return type of any of those functions.


? both are the same.


Is Range a kind of interface? If yes, then wouldn't that be the appropriate return type?

edit: Looking at other answers, I think Range is probably not an interface like they exist in Java, but rather a pattern of behavior per templates in C++. Concepts are supposed to solve this problem in C++, but I don't know how well they actually do.


A Range is something that implements one or more interfaces depending on its properties and guarantees. So in order to name a Range that way you'd first have to create interfaces for all possible guarantees. That doesn't sound practical. It's analogous to C++ containers implementing common concepts without deriving from corresponding interfaces.

See also https://tour.dlang.org/tour/en/basics/ranges


Yes, this is what C++ concepts are supposed to solve: https://en.cppreference.com/w/cpp/language/constraints


In D, you don't declare that a type X should have operations A, B, C. Instead, at the moment of template instantiation, you can verify if the provided type has operations A, B and C.


That makes for terrible docs and discoverability, which is the problem here.

Maybe D should allow the user to name the return type (an existential variable) and static assert stuff on it:

`SomeVar f(…) with isRange!SomeVar` or whatever. `auto` just means "you have to read the implementation because it can be literally anything"


Well, it is visible in documentation, but even those can be hard to parse:

uint startsWith(alias pred = (a, b) => a == b, Range, Needles...)(Range doesThisStart, Needles withOneOfThese) if (isInputRange!Range && (Needles.length > 1) && is(typeof(.startsWith!pred(doesThisStart, withOneOfThese[0])) : bool) && is(typeof(.startsWith!pred(doesThisStart, withOneOfThese[1..$])) : uint));


You did pick a particularly nasty example for that one. I do agree that it is not so easy to read these constraints. It can also be a bit frustrating when you need to chase down why exactly a particular line of code doesn't meet such a constraint. Tests like isInputRange are themselves fairly involved expressions and in the worst case, you end up staring at those after a template instantiation failed.


D's version of concepts have optional operations, it has been found to decrease the need for names


>so tired of having to hunt down what functions are going to return, sometimes having to resort to just using it

With highly generic functions, it's often not possible to know what they'll return without knowing what you'll call them with. Especially given that D functions like "map" and "reduce" tend to return special iterator types so that the compiler is able to smarty fuse them where possible. If D had concepts like C++20, you could probably describe them with something looking like:

    template<class R>
    
      concept __SimpleView =                         //     exposition only
        ranges::view<R> && ranges::range<const R> &&
        std::same_as<std::ranges::iterator_t<R>, std::ranges::iterator_t<const R>> &&
        std::same_as<std::ranges::sentinel_t<R>, std::ranges::sentinel_t<const R>>;
But at least for me that doesn't seem like it would be much more helpful than just reading the documentation, which states what the function returns, if not necessarily the type.


So you still can see that it depends on some input. That is hugely more useful then auto.


>With highly generic functions, it's often not possible to know what they'll return without knowing what you'll call them with.

Of course it is. Map's type is "(a -> b) -> [a] -> [b]". D just absolutely and completely failed here, despite this being a solved problem 40 years ago.


Not in D, it isn't, for performance reasons. It takes an input range and returns a type that iterates through that range applying the callable (doesn't have to be a function!) to each element as requested.


>It takes an input range

Which should have a type.

>and returns a type that iterates through that range

Which should have a type. The entire point is that this is a solved problem, there is no excuse to simply throw up our hands and say "screw documentation we'll just say this function is a mystery".

Functor f => (a -> b) -> f a -> f b

And please don't miss the point and tell me D doesn't have Functor. The entire point is that D has something, and it doesn't tell us what that something is. It should. Documentation is good.


D has functors, but it doesn't have 'Functor'.

Or rather, D doesn't have concepts (of which 'Functor' is a special case); that is, the notion of a type that is characterized by having the ability to execute operations is not expressible in its typesystem.

Or rather, it is, but only with classes. You want something like "a return type; fulfilling the condition of being able to be used in this way." This is not something you can specify as a function attribute in D. Instead, ranges use a form of duck typing. The next step in the call chain can tell whether the previous step gave it something it can use using template inconditions, ie. `isInputRange!T`. But the previous step can't assert that it is returning a type that fulfills a constraint. In other words, there's type inconditions but not type outconditions.


What you're describing has names - structural types, refined types (a.k.a. contracts a.k.a. pre- and post-conditions)...

It's simply a failure of D the language/compiler (and a huge anti-pattern) to not expose internal types in a way that can be displayed to the programmer.


No such internal type exists. The range interface is purely a library feature. The problem is that D has no way to include a type constraint as a part of the function type, at present. Something like out template contracts would do it probably.


java can't express its types in its own syntax either. nor scala, nor sharp.

ceylon could, but they are barely readable anyway.


> > It takes an input range > Which should have a type.

It does. That'd the `Range` here: https://dlang.org/phobos/std_algorithm_iteration.html#.map.m...

> > and returns a type that iterates through that range > Which should have a type.

It does. But the name of that type depends on the type of the range and on the callable.

> Functor f => (a -> b) -> f a -> f b

This doesn't work because the return type isn't `f b`, it's `g b` where g depends on what f is. It also depends on the callable, because the first parameter isn't necessary a function. The closest is

Callable c, Range r0 => c a b -> r0 a -> r1 b

Where `r1` isn't even a concrete type but a type that depends on both `c` and `r0` and is made up on-the-fly (per instantiation).

> Documentation is good.

I agree. How would you suggest improving the signature of map given that D doesn't have typeclasses? Or with types that depend on other types in the template?


class Range g => FunctorTo f g | f -> g where fmapto :: Callable c => c a b -> f a -> g b

is how you would define that class of types in Haskell. It says "If g is a range, then a pair of types f and g satisfy the FunctorTo interface[1] when knowing f determines g, and there's an implementation of fmapto with this type".

Maybe D needs typeclasses. This thread has certainly put me off of D, because being able to write down types is really quite important to me.

[1] The Haskell class keyword is defines something closer to an interface than an OO class.


> because being able to write down types is really quite important to me.

The issue arises only with generic heavily templated functions. Nothing in D forbids you to write your programs with all types explicitly written down. That's btw how I mostly write my code.


>But the name of that type depends on the type of the range and on the callable.

That should not be a problem. It is a problem in D because of a lacking in D.

>How would you suggest improving the signature of map given that D doesn't have typeclasses?

The language needs fixed so it can express its own types.


> That should not be a problem. It is a problem in D because of a lacking in D.

It is not a problem, the compiler copes with it. The problem comes from the fact that such a type is absolutely not interesting to know how it is written. The unmangled type is unreadable.


Yes, the type is very interesting to know. That's why other languages make it known. Ignoring the entire discussion to reply twice with "nu uh!" is not very productive.


There's usually no reason to know the return type of any range-based function in D. It's not like auto is applied as a return type willy nilly. And anyone who understands D's ranges and how they are used should have no problem seeing a function declaration that returns auto.


Right, the entire world is wrong because you can't admit a fault in a random piece of software you are emotionally invested in. Makes sense.


Not quite. I'm saying that in this case it doesn't matter because of the way the API is used. Is it confusing for people who don't understand D ranges? Yes, it certainly can be. It was for me when ranges first came along. But once you understand how D ranges are intended to be used, then you realize you rarely ever care what the return type is. D is not Rust, or C++, or (insert language here).

When the return type actually matters, auto should be avoided unless there's no way around it. But that's why we have "Returns:" in Ddoc. The function signature itself is not the complete documentation. I mean, you're acting like all D functions are documented to return auto. They aren't. It's used where it needs to be.


> The language needs fixed so it can express its own types.

The language expresses its types just fine (it's in the mangled name in the object file). The issue is that there is no point in the human readable form of these types.


It doesn't have to be specialized to []


Of course not. But it still has a type.

Functor f => (a -> b) -> f a -> f b


That still means "it returns something".


It gives us much more information than "something". It tells us it is a list of the type of the second argument to the function we passed it. Or in the generic version a "something you can iterate over" of things of the type of the second argument to the function we passed.


no ? there are plenty of maps which don't return something which looks like [a] -> [b].


So? They still have a type, it doesn't have to be "screw you figure it out yourself".

Functor f => (a -> b) -> f a -> f b


I have seen few videos by Walter Bright, Andrei Alexandrescu on ranges and I have no problem understanding D`s documentation. Maybe you should learn the language before using it.


Repeating the same over and over again doesn't make it any clearer for the ones trying to follow your line of argumentation.

Seems like you had some deep exposure to Haskell, ML or Hindley-Milner in general which, when excessively consumed, detaches from reality.

For one reason or the other you take this discussion serious and personal.


If something is unclear, you can ask for clarification. I repeated the statement to three people because three people repeated the same argument to me. This is how conversations work. I have very limited exposure to haskell, am not detached from reality, and am taking nothing personal. Of course the discussion is serious, why would I spend time engaging in a frivolous and meaningless discussion?


could you provide the type of this map function ?

    auto map(auto x) { 
      if constexpr(is_same<decltype(x), int>) {
       struct T1 { int getStuff() { return 0; } };
       return T1 {};
      } else { 
       struct T2 { void doStuff() { } };
       return T2 {};
      }
    }


Read the rest of the discussion.


being a solved problem 40 years ago

Ahh.. welcome to computer "science", the ever repeating cycle of 'inventions'


You don't have to wait for the compiler to complain. Using:

    pragma(msg, T);
where T is any type will print the type to the screen during compilation. pragma(msg) will print all kinds of things, making it a very handy tool to visualize what is happening while compiling. I use it all the time.


Print-debug you program before it's even compiled and even have a bug is a bag idea.


Why so?


You shouldn't need to reverse-engineer your own program just to understand the code you wrote yourself.


I salute those who never have to debug their own code :-)


Apparently it is a common Go pattern to discover which interfaces a type implements.


It's often because these functions have unnamed types. Chain of lazy computations in D often return unnamed types (so called "Voldemort" types) because finding good names for those inner structs is a challenge, they have a single use (which is to have a particular signature).


> Chain of lazy computations in D often return unnamed types (so called "Voldemort" types) because finding good names for those inner structs is a challenge

There is something so absurd about having "unnamed types" as an antipattern!


Not really - the concrete type isn't important, but what you can do with it is. One could argue that instead we'd use a concept in place of `auto`, and Bjarne has argued exactly that for C++.


> Not really - the concrete type isn't important, but what you can do with it is.

Then surely that's what should be shown? Rust uses `impl <Trait>` for that, the actual return type is opaque but you know it implements the specified trait.


Not everything is worth naming, if there isn't an obvious good name for somethink (like say, a Java Anonymous Classes) then why not allow it to have no name?


Java's objects aren't types even though they're mixed up with its type system. Objects are supposed to represent units of computation so anonymous classes aren't absurd.

But the reason why it seems that types without names are absurd is that types are only real for the interpreter or compiler. At runtime they aren't used anymore. So it's absurd that a construct made for humans to understand and describe code starts to become something opaque to human understanding because they're impossible to be named.


I've felt this as well, been using D for a couple years now, and this is the kind of thing that just makes me have to context switch more than I'd like. With the current implementation of the language it's hard to avoid, and function's return type can be quite complex, so writing it down can be hard.

Another reason is the (ironically) dynamic nature of a return type. E.g.

auto whatDoesItReturn(int i)() { static if (i == 0) { return int.init; } else { return string.init; } }

Template code can do that quite easily and then you don't have a choice but to write auto as the return value.

What would be fantastic if the documentation could be given access to the the compiler's return type inference, so that it could document the auto returns with a little more info.

Another way useful approach would be to implement protocols like in swift, or traits like in scala/rust/others, signatures in ml, etc. Then you would be able to define the interface of what a function returns.


> auto whatDoesItReturn(int i)() { static if (i == 0) { return int.init; } else { return string.init; } }

I agree with your point, but for the sake of the audience who doesn't know D I think this example is misleading, as one could take the "int i" parameter as a runtime one, while it's actually a compile time one (the equivalent of C++ non-type template parameter). If you instantiate the function with 0 as a compile-time parameter, it is a function that returns int; otherwise it's a function that returns string. It is never a function that can return int or string.


Yeah I realise I could've explained a bit more about template arguments indeed! The point was that it can be hard to specify the return type.


Yes, this is the problem for returns, but it's going to be difficult for the compiler to put something useful. The best it can do is point you at the code that returns, and let you figure it out.

I still think the best option is let the author describe it in ddoc, as the semantic meaning can be much easier to convey that way.


I haven't looked at D yet, but... yuck! `var` for return types in a method signature is seriously unhelpful!

If the docs are filled with this, then D is certainly coming off my list of langs to look at.

For functions that can return different types, I think interfaces or union types would be more helpful (not sure if D supports either though).


Yes, it supports interfaces and unions. One thing not being stated enough in this thread also is that the docs are not just a regurgitated version of the prototypes -- there's actual hand-written text that tells you what the things return.


In the standard library, it's primarily in the range-based functions that you see this, where the type doesn't generally need to be known. And where you do see it, the documentation will describe what the function returns more accurately than any convoluted made-up type the function signature could show you.


What would be the return type of that? Does D have an Either type?


Depends on the value of the template parameter. If it's 0 the return type is an int, if it's not 0 the return type is a string.

There's a wanting implementation of a sumtype in the standard library (https://dlang.org/phobos/std_variant.html#.Algebraic), and a much better one as a package: https://code.dlang.org/packages/sumtype


Interesting, so the return type isn't known until runtime.


No. It is known at compile time.

Template declarations in D take 2 parameter lists. The first is the template parameters the second the runtime parameter: in auto whatDoesItReturn(int i)() { static if (i == 0) { return int.init; } else { return string.init; } }

we have (int i) as template parameter and () as an empty runtime parameter. In C++ syntax whatDoesItReturn<int i>()

at instanciation the syntax is different:

whatDoesItReturn!0() will instantiate a function returning an int

whatDoesItReturn!42() will instantiate a function returning a string.


When it comes to templates it's not until instantiation time - when the compiler see the code being used. So this is just an issue during compilation.

The docs on static if may shed some more light: https://dlang.org/spec/version.html#staticif


So the previous code wouldn't compile unless the compiler knew what values were going to be passed into whatDoesItReturn?


Aye correct! I should've been more clear about that, sorry.

Template arguments need to be known at compile time, and the extra set of parens is how templates parameters are declared in D.


I missed this the first time, but there are two sets of parentheses in the example. Apparently the first set are like template parameters, and the second are the actual arguments to the function.


Personally I dislike var/auto in languages because I like having types explicitly written. But in case of languages like Java or Kotlin you can move the cursor over the variable name and you will see the type, also you can right-click and select "replace with explicit type" and it will work. In D, IDEs struggle with templates and can rarely index templated code (no wonder, because most of the code doesn't exist until build time).

Most people will tell you, "oh just use auto, it makes the code more generic". That's sweet, except as soon as I want to pass it to another function, I need to have the concrete type. Like you, I usually just copy-paste the full type from the error message and move on.


In Java, `var` comes in useful at times. For example:

    for (Map.Entry<SomeLongType, AnotherLongType> x : someMap) {
        final SomeLongType key = x.getKey();
        final AnotherLongType value = x.getValue();
        ...
    }
In the above code snippet, `var x` would have been very useful because the actual type just repeats information that can be found in the next two lines. Also, usually, I'll use more speaking names instead of `key` and `value`.

But if the body of the loop just refers to `x.getKey()` and `x.getValue()`, without extracting them into local variables, then it makes sense to put the exact `Map.Entry` type into the loop header.


You can just use map.forEach and avoid the types in the next two lines too.


In java 10+

    for (Map.Entry<SomeLongType, AnotherLongType> x : someMap)    {
        final var key = x.getKey();
        final var value = x.getValue();
        ...
    }
Is valid.


I prefer to put "var" into the loop header because the combination of two types is hard to read. It's easier to read (for me!) when the type of the key and the type of the value are separated, like they are on the first two lines of the loop body.

    for (var x : someMap) {
        final SomeLongType key = x.getKey();
        final AnotherLongType value = x.getValue();
        ...
    }


Personally I dislike var/auto in languages because I like having types explicitly written. But in case of languages like Java or Kotlin you can move the cursor over the variable name and you will see the type, also you can right-click and select "replace with explicit type" and it will work. In D, IDEs struggle with templates and can rarely index templated code (no wonder, because most of the code doesn't exist until build time).

It's 2020. Why couldn't things work like this, where one can open a window for a concrete type using templates, and it shows the code?


A well designed language should be usable from a text editor. Even in 2020.


I agree. I will paraphrase this as - a well designed language should be usable, at minimum, from a pure text editor, and should not put unreasonable burden on an IDE.


The cynic in me wants to say that such a language doesn’t lend itself to static analysis. As soon as you can do great things with static analysis you can build those features into an IDE, therefore causing the “pure text editor” to feel crippled giving rise to the idea that this language is “unusable from a pure text editor”.


A good dev uses an IDE. In 2020.


This is a really tired argument that we don't need to get into right now. Different things appeal to different people.


Two things can be true. You shouldn't need an IDE to grok the code.


A good dev shouldn't require crutches


Interactivity is not a crutch.


Interactivity is a base attribute of an ide, and not the first reason people use it; the context here is that using an ide doesn't define a good programmer. So being able to clicky click is what makes good programmers to you?


They said that good devs use IDEs, not that IDE use makes you a good dev. Those are very different statements.


They said "A" good dev uses an ide, and a snappy 'in 2020' retort. It's plainly clear their intention is to imply only good devs use ide's in the modern era.


No, that's not how simple if statements work. I don't know what else I can say.

If I say "A good CPU has more than two cores in 2020." then I'm just saying it's a requirement, not sufficient all by itself. I'm not calling a twelve-year-old phenom X3 a good CPU. The "in 2020" is just to emphasize that anyone failing this standard is falling behind the times.


Templates don't exist until compile time, until you build the code, the IDE plugin doesn't have the full data on what types exactly are there. Java/C# generics are more limited in functionality, but it's a tradeoff in exchange for better ahead of time knowledge of types.


Visual Studio can do that, you just provide an example type and the IDE shows what the result would be.

https://devblogs.microsoft.com/cppblog/template-intellisense...

Now try that on vim.


Hard disagree.

   MyClass myVar = new MyClass()
is not DRY. It also makes it practical to use complicated structures out of generics/templates without killing the developer With<Deeply<Nested,Template>, Declarations>.


Hard disagree. If you are assigning a value that's the result of an expression you might have somewhat complicated logic. Being able to say what you expect returned is very useful.

    MyClass myVar = something ? SomeFunction() || somethingThatMightBeASubClassOfMyClass


Anything wrong with

   var myClass = ...
(or some other descriptive name of the variable)?


Personally I prefer this, since it also carries on to every following line.

Like, in C#

    List<Account> accountsToDelete = accountService.GetAccountsToDelete();    
    repository.Delete(accountsToDelete);
becomes

    var accountsToDelete = accountService.GetAccountsToDelete();
    deleterService.Delete(accountsToDelete);
So as much type information is already encoded into names so that all references to this object are clear in what we're handling, and so the type declarations at the point where the variable is declared is just redundant noise.

If your variables aren't informative when I'm reading the code, I'll be confused 5 lines later anyways. So make them informative at the start. And given that, doesn't that mean the List<Account> is a bit redundant?


Beware the m_pszSlipperySlope [0].

[0] https://en.wikipedia.org/wiki/Hungarian_notation


Even if you have var/auto, you don't have to use it every time.


var/auto is a trade-off which requires good tooling support, as you said. Once you're able to see the types as you see fit in your IDE, I feel that you're mostly better off, for all the reasons already mentioned in the thread.

Of course good tooling is still a big requirement, but I still think it's the best decision in the long-term: it's way easier to improve and change tools like IDEs (especially with LSP?), rather than the language itself.

Regarding explicit types in functions, you also don't always need them in languages such as OCaml. I feel that the answer to your criticism could be to just have "auto" also for function arguments, especially when you're just prototyping.


In places where you need to know the exact type auto probably isn't the right choice.

There are many cases where the type is clear however or irrelevant or in generic code is hard to express (thus depending on documentation/comments unless obvious) which would also be hard to read.


I've been using D since 2009 both personally and professionally. 'auto' return did bother me in a few places but it never came close to being a deal breaker.


It's helpful to bring up issues you are encountering in the D forums. This is the first I've heard of this particular one.


It's been raised many times before. By me and numerous others. The standard library is generic, which isn't the end of the world, but you can't tell from the documentation how you can work with the output. It's common for someone to ask a question and be told "add .array to the output". They'd never know that after reading the documentation.


Reading the example code in the documentation is very helpful with this.


And the "Returns:" section!


I had this issue last year when I started learning D. I eventually got used to it, but it was a stumbling block at first. The #d IRC channel helped me out.


I've never programmed in D so I don't know, but from curiosity I wanted to check if what you write is true. However, I can't find any function that is declared as auto.

Could you please paste some example of a function that has a return value which is declared as auto?


Here's one example:

https://dlang.org/phobos/std_algorithm_sorting.html#partitio...

The important line is

auto pieces = partition3(a, 4);

So, what's the type of pieces? The D standard library is written to be generic. And sure enough, that line of code will run. Where it turns into a problem is when you try to do something with it. If pieces is a range, there are certain things you can't do with it. Or maybe you can. Who knows. You'll never learn it from reading the documentation. I've been using D since 2013 and I still struggle with this at times. It's a valid complaint. (D's a great language, but is short on manpower to fix rough edges like this.)


> You'll never learn it from reading the documentation.

Did you not see the Returns section from that link?

"Returns: A std.typecons.Tuple of the three resulting ranges. These ranges are slices of the original range."

Further note: If you just saw `std.typecons.Tuple!(typeof(Range.init[0 .. $]), typeof(Range.init[0 .. $]), typeof(Range.init[0 .. $]))` which is what would have to be written there instead of auto, would that make you feel better? Do you not have to read the documentation to figure out what the function does or what actually goes into those tuples?


The major examples are parts of the stdlib which offer higher-level pipelines (some answers here indicate the need for such things to be very flexible in their return values to allow this, but this is not a priori obvious - Java manages similar functionality with just the Stream<T> class after all).

In this (and the sibling pages in algorithm) nearly every entry is listed as either "template" or "auto" relhttps://dlang.org/phobos/std_algorithm_iteration.html


D's classes and interfaces are much like Java's, so it's possible and easy to write functions that express return types like that. It's also quite restrictive for generic programming. D's metaprogramming features are much more powerful. That means a template can return different types that do not conform to a single, easily-expressed interface. The std.algorithm package is built on that concept.



Returns

A range with each fun applied to all the elements. If there is more than one fun, the element type will be Tuple containing one element for each fun.


Yes, D has prosaic descriptions. So does Python. Or Ruby.

Maybe read and try to understand the complaint instead of pointing out something unrelated?


Maybe it was fixed? I would imagine that saying in documentation that return type is auto is equivalent to not mentioning return type at all, so feels like something not intentional.


> Maybe it was fixed?

It wasn't. They probably didn't look into the more "generic" functions e.g. hofs and algorithms.

The vast majority of functions in https://dlang.org/phobos/std_algorithm_iteration.html returns "auto"


I checked your link, each function has a “Returns:” section that describe what to expect. Isn’t that what you want from a documentation?


A type is much denser, than a textual description, and in most cases sufficient.


Having each of the generic range functions return a <FN>Result type that all implement some variations of a random access range is absurd. Having "auto map(); Returns an input range" is much denser and helpful than "MapResult map(); struct MapResult { @property bool isEmpty(); ...}".

A more explicit "impl(InputRange) map()" may be better until you consider that map is generic on the kind of range you give it, so that just turns into "impl(MapResult!R) map(R)()". A more roundabout, pointless way of saying "auto".


> A type is much denser, than a textual description, and in most >cases sufficient.

In feeble languages with simpleton typesystem may be, but in highly generic templated language like D it is not the case. The type is not dense at all.

What's funny is that in general people complain that compiler errors in D are unreadable. You know why they are unreadable?

Because they print out the types of the functions in which the error occurs and that is nothing more than word salad for generic functions.

Types with hundreds of characters are very common.


Depends the type, it can be quite long and complicated. I always have a lot of issues trying to read return types from templated functions in C++ because they look really messy.


Perhaps then it would make sense to shorten types in a smart way.


Sometimes I feel like auto is definitely overused. In a recent fix, I changed something that returned a boolean (no templates involved) from returning auto to returning bool.

But sometimes auto is the best tool for the job, especially when writing wrapping types. In that case, yes, you have to read the documentation (and I mean what is written in the ddoc comments). But in many cases, you don't have to, because you recognize the pattern, or it's simply a wrap of the underlying type's function.


+1, I cringed when Herb Sutter released the 'Almost Always Auto' C++ presentation on YouTube. Sure, auto has its place, and I personally use it, but I just knew that less experience devs would go nuts with it, and it'd only make their lives easier for a short time.


Plus, seeing the word 'auto' all over the place leaves a weird impression.


It lets C++ almost return to C-style duck typing.

Whether that's really what you want and whether that is the best approach to solve the problem at hand is a matter of preference and the problem space.

It is also allows for a "gradual typing" approach that Dart 1 had.


I think it’s really interesting that as dynamically-typed languages increasingly encourage explicit type hints, statically-typed languages are recommending “almost always auto”.


In dynamically typed languages, adding types can stop things blowing up at runtime. In compiled languages, all the type-inference is still done at compile time, so if it compiles than you're not going to get a crash from accidentally adding a string to an integer at runtime.


The template language is weakly typed and code written in it, too, can “crash” when it runs (which is the compilation time) for the same reasons.


Vim vs emacs, auto inference vs explicit, I don't think it will ever end. :)


I don't know D, but after reading more comments looks like auto is also compensating for poor type system. For example functions that accept arguments of many types apparently need to be declared that are returning auto.

At least that's what I understood.


Scala has a strong type system and when working with it on a daily basis I was not programming, I was thinking about types and fighting with the compiler. And that is the language which has a pretty decent IDE plugin. When I moved to D, it felt like a breathe of fresh air to me. Therefore, the whole talk above about auto and types looks like subjective nitpicking.

In practice though, D codes fast and runs fast, as promised on the official site.


Aren't there any type synthesizers which modify your code based on inferenced types?



No. I mean altering code to strong typing using type inference. So that auto becomes int, or MyClass*.


It already ended. VSCode and auto inference.


I vehemently disagree. Auto/var is a tool that may be used judiciously by your userbase. This new philosophy of blocking your users from using dangerous tools because you know better than them just invites workarounds and kludges. Give the user the tools and warn them of the ways it can go wrong. There's a reason Rust is losing the war to C++.

Anyways, var/auto is critical in some cases. C#'s LINQ, for example, would be very difficult to develop with if you had to manually figure out the type you were returning with long queries every time you wanted to restructure your query.


But they were talking about _docs_.


I disagree; you don't seem to be addressing what the parent is talking about (documentation). Whatever reason Rust is losing a war against C++ (it's really not), this isn't it.


More a failure of documentation/tools? We've been content for a decade to just name the arguments and return without any context. Like the old joke "Where am I?" answered by "In a hot air balloon!". Correct but useless.

I wonder if the document could describe (in some regular way) how those auto types are constructed...from what input, with what operations?


They already do.


Im annoyed that you have to spell out auto all over the place. It should be implicit. The compiler should automatically add auto if you have not specified something else.


I really hate the auto keyword but as I like D so much, I kinda get used to it.


Ugh, that is even more ill-advised than using auto without good reason in C++.


In the interests of perpetuating this endless flamewar, here's Herb Sutter saying C++ programmers should use auto 'by default'.

(I see my snarky comment there got no reply.)

https://softwareengineering.stackexchange.com/a/180616/


Herb Sutter is biased. He works on C++ language lawyering, he works on new features, he works in the STL, etc.

People like him tend to be biased about using auto because they write mostly libraries and generic ones at that (data structures, for instance).

In most code out there you actively avoid templates if possible, so that code is concrete, compiles faster and is easier to debug.


Well, there's two ways to write an application, analogous to two styles in mathematics as described by A. Grothendieck.

http://www.landsburg.com/grothendieck/mclarty1.pdf


Bingo! Writing a template library is totally different to writing an application.


Another reason for C/C++ is that they are very permissive with implicit lossy conversions. If you specified explicit type, chances are the compiler helpfully made a lossy conversion for you.


Sutter made that point under Maintainability and robustness.

Like Sutter's answer, this point doesn't answer the complaint. People on the anti-auto side say it seriously harms readability, as locals' types are no longer clear at a glance. They aren't asking for a list of reasons why some people favour auto, they're asking for an answer to their readability problem.

Perhaps IDEs could infer types and display them as a superscript. That would keep just about everyone happy. (Perhaps not Vim users.)


Herb didn't say it well enough. It's a tradeoff between correctness and readability. If it wasn't for C++'s weak type system you wouldn't be required to make this tradeoff and could improve readability while still staying correct.


In IDE-friendly languages like Kotlin, you can enable showing of inferred types, e.g. https://i.stack.imgur.com/tiqjc.png


Perfect, just what I was thinking. Does something like that exist for C++? Visual Studio 2019 doesn't seem to have it, but it's able to show a local's type when I hover over the local's identifier.


If you use only explicit constructors and ban cast operators except for the most basic of types, then you have a sane language and explicitness in your code.


Sounds sensible. Google seems to agree with you. https://google.github.io/styleguide/cppguide.html#Implicit_C...

LLVM's coding standard though doesn't seem to have anything to say about implicit conversions: https://llvm.org/docs/CodingStandards.html


It's sane on C++ scale, but on absolute scale that sanity is still wanting, Sutter mentions this.


In the context of C++, the use of auto allows to avoid unintended type conversions during initialization.


Well, not everyone likes automatic type deduction -- some people just like to torment him/herself by repeating information that is unneeded. Why?


Type inference systems generally have crumby error messages, and are harder to reason about all around. The best type inference systems are those that allow inference within a function definition, but the function signature requires explicit annotation. This keeps the inference local, which is easier for humans to read about. Good tooling can make either system easier to work with, but tooling is much less important in the type annotation case because the cost of adding annotations is trivial (contrary to your "torment" vocabulary) compared to reasoning about (non-local) type inference.

In general, this reduces to the principle that concrete is easier to reason about than abstract. The type signature of an unannotated function in a type inference system is maximally abstract, while (especially in practice) the signatures for functions that are manually annotated are more concrete if not fully concrete. There are still problems in non-type-inferred systems with programmers who try to be egregiously abstract, but these are fewer and farther between.


Sometimes when reading a long passage in a novel you need a reminder who "he" is.


For more context and details, Walter Bright wrote a series of blog article between 2017 and 2018 on the subject (though their content is a bit outdated as more D features are now supported in BetterC mode):

- "D as a Better C" (2017): https://dlang.org/blog/2017/08/23/d-as-a-better-c/

- "Vanquish Forever These Bugs That Blasted Your Kingdom" (2018): https://dlang.org/blog/2018/02/07/vanquish-forever-these-bug...

- "DasBetterC: Converting make.c to D" (2018): https://dlang.org/blog/2018/06/11/dasbetterc-converting-make...


This is a great idea. I maintain that Ada is a better "better C" than any of the alternatives I've looked into, but it has an obvious big hurdle: while it has approximately the same use cases as C, it is completely different in terms of looks and handling. One of the strong points of D is that it still seems very much like C. Very good call to emphasise this.


I’ve always liked the idea of Ada, but never had a place to use it. Though someone on HN pointed out that NVidia was using Spark for secure code sections. Quite interesting!

Personally while D seems a great tool, I really keep running into situations where a language that lives on top of C/C++ is useful. So I’ve been trying out Nim for those use case, using the ARC GC which appears to work well for embedded. It’s deterministic but with move semantics for performance optimization. Interesting approach IMHO. But the biggest advantage is being able to directly interface with any C or C++ natively. D/Rust both seem to have difficulty being 100% onboard with C++ (for good reasons).


Most likely me, or not.

In any case here are the latest news on the subject,

"GTC 2020: Exterminating Buffer Overflows and Other Embarrassing Vulnerabilities with SPARK Ada on Tegra"

https://developer.nvidia.com/gtc/2020/video/s21122-vid

Another well know project that is now adopting Ada/SPARK is GenodeOS, https://genode.org/about/road-map


> I really keep running into situations where a language that lives on top of C/C++ is useful. > the biggest advantage is being able to directly interface with any C or C++ natively. D/Rust both seem to have difficulty being 100% onboard with C++

I thought one of the centerpieces of D was seamless c++ interop? Where does it fall down?


You just need to do a bit of tiresome explicit declarations to statically link the code.

For libraries with a large API that can be an annoyance.


AFAICT, D requires the C++ types be exported, included any predefined template objects or functions. E.g. you can’t dynamically wrap `std::map<K,V>`, unless it’s already been instantiated in C++ library you’re linking. The same in Rust as well. See D Lang manual:

    Note that all instantiations used in D code must be provided by linking to C++ object code or shared libraries containing the instantiations. 
Whereas Nim is one of the few languageS that can dynamically wrap C++ template types into its type system. Maybe Zig can do it too? See Nim manual [2].

    type StdMap {.importcpp: "std::map", header: "<map>".}[K, V] = object proc
    ...
    var x: StdMap[cint, cdouble]
    x[6] = 91.4
    ...
    std::map<int, double> x;
    x[6] = 91.4;
This makes it nice to wrap C++ libraries. :-)

1: https://dlang.org/spec/cpp_interface.html#cpp-templates 2: https://nim-lang.org/docs/manual.html#importcpp-pragma-impor...


So, more than for using c++ with c++ -- in your build/link setup?


I like Ada. D took inspiration from it.


I know. I think you and I have discussed aspects of it at various points in comment threads before. I'm impressed with how well D is doing things that are really very hard.


For me Oberon is the better C.

https://www.miasap.se/obnc/oberon-report.html


Nim was inspired by Oberon, and compiles to C (so easy interop). Neat language.


On the Oberon linage that place belongs to Active Oberon.

http://cas.inf.ethz.ch/news/2

Only Active Oberon exposes non traced references, RAAI and a couple of other low level features that made it more complex to do systems programming in Oberon and Oberon-2, let alone Oberon-07.


I use Pascal as better C


After I discovered C, I never wrote another line of Pascal :-)


Pascal is not better, it's just more verbose.


Just a couple of ways how it is better, using Turbo Pascal 7 for MS-DOS, released in 1992.

- Units (modules) for separate compilation with strong type checking and not needing useless prefixes

- A proper string type

- Proper arrays with bounds checking, it has functions to retrieve upper and lower bounds

- Allows the definition of numeric ranges

- Enumerations are their own type, can be used as array indexes or data sets

- Support for sets

- Thanks to reference parameters there is less need to deal with pointers that might be invalid

- Already allows for a simplified way of doing Type Driven Development

- Supports type safe function pointers

- Supports single inheritance OOP with minimal RAII support for heap allocated objects

- Comes with a quite powerful OOP framework for TUI applications, Turbo Vision

- While it doesn't prevent use-after-free, it allows for memory regions that can be deallocated on one go

- Can map arrays to memory regions with bounds checking

- Besides inline assembly without UNIX's clusmy syntax, allows to use registers as variables

- If one really wants do do unsafe C style coding there are compiler pragmas to allow it and yes even pointer arithmetic and unsafe casts.

This in 1992, if I take a recent version of Delphi, FreePascal or Oxygene, there are even more bullet points.


Unfair comparison: Turbo Pascal 7 is more like C++20.


Not really, hence why I mentioned 1992. C has had plenty of time to catch up, but apparently it wasn't something that WG14 cared about.

And it is not like many aren't actually coding in GCC C, Clang C, TI C, xlc C, aC C and so on.

Which even with those C extensions fail on the security story.


At university, my classes in C required using C89. The rationale given by the professor was that he wanted us prepared for industry, and if you learn to stick to C89 you will be able to write C for any job out there. I argued that C99 block scoped variables free up stack space (pretty silly argument but true), he wasn't buying it.

I think this kind of thinking is very common across the C community.


And to be clear, this kind of thinking hampers progress in the name of genericity.

The ironic thing, is when someone makes that argument, they are admitting that the education they are giving creates students around the mean. When I teach someone, I try to give them a mental framework that others don't have and some unique skills that will differentiate them.

Learning C89 gives someone the "most chances" of getting a random job who hasn't upgraded to better technology (◔_◔). Where if I show someone how to use a constraint solver to optimize something, or how to run a quick Monte Carlo simulation to test a hypothesis, those folks will have a huge advantage over the C89 slinging autobot. The only reason to learn "popular tech" is to hide within a flock of nameless cogs.


Well, I learned the better technology (Pascal) rather than the popular tech (C), and then I could not find a development job


I am going to weaken my argument a bit and say that once something gets to be a large enough part of status quo, you have to learn it, at least enough to get by. Back then it was C, now it is JavaScript, Python and Kubernetes.


From an educational standpoint, C89 is a bit further from modern languages, so might be a better call on that basis. If you know C89 and some recent languages, you can probably roll forward to C99 or C11 without much trouble. If you started with C11, and haven't every worked in something from the 80s... well, that's an education.

> C99 block scoped variables free up stack space

I'm not sure that's actually true. Or at least if it is, I think it's the choice of a particular implementation. The compiler doesn't actually need to allocate anything until first use, and nothing prevents it from hoisting allocations earlier.


It's actually true - in the sense that at the end of the block the part of the stack frame used by the variables defined in it becomes available for reuse (which is normally done at compile time).


Hmm, I think I might've misread, but I interpreted what was being asked for the ability to mix declarations and statements, as C99 allows. If what's being asked for is simply the ability to declare variables at the top of blocks, I think that's allowed in C89? Certainly GCC and clang both allow it without warning with -std=c89 and -pedantic. I agree that there are cases where enclosing a variable in a block will allow earlier reuse of the memory that backs it.


I misremembered my C89 gripe! The issue was that the following is not allowed:

    for (int i = 0; // ...
Forcing you to write

    int i;
    for (i = 0; // ...
The real annoyance here is that you can't restrict the scope of a variable you need initialized at the start of a for loop to the loop block. It's all fine and dandy when you are just using `i` (and never miss resetting it, or use it somewhere expecting it to be one thing and have it end up being another). When you have several complex loops in a function, it can be a bit aggravating.

(Declaring variables willy nilly anywhere in a block is nice too)


Ah, yeah. There shouldn't be any runtime difference if you don't leak the address of `i` somewhere the compiler can't see, which should be uncommon for an iterator. But I agree it's awkward.

You can wrap the whole thing in an extra block, so at least you don't have to hoist `i` to the top of the function.

> (Declaring variables willy nilly anywhere in a block is nice too)

You can always introduce another block, although it's not uncommon that that's harder on legibility than just hoisting.


I don't think that the mixing of definitions and statement has any effect on how the stack frame space is (re)used: even if all variable definitions are collected at the beginning of the block, it may still be possible for the compiler to find the points inside the block after which some of the variables are no longer used while some others are brought into action; such variables could, then, share the same space despite having the same lexical scope.


Pascal is better, because it has an actual string type that can store \0 characters, and a boolean type, and reference-counted dynamic arrays, and set types, and ranges in case statements (that makes it less verbose!), and a for loop that can go to MAXINT without overflow


Mr. Walter Bright, I'm late to the party here, but I hope you will see my comment.

I have a lot of respect for you and Andrei, and I think D deserves much more love than it gets.

I personally feel like there are too many options when it comes to D. Perhaps it would be good to double down on some combination of those options and make that widely known?

As a newcomer to D, I am exposed to GC/non-GC code, dmd/llvm compiler/gcc compiler, several debuggers, some BetterC option, etc. I don't know which toolchain is best, and I don't want to deal with the integration issues between them.

All of these options that exist are great, but the brand gets diluted. D seems to be the ideal thing: it's seemingly a better C++, and a better C, with a clean syntax like Java.

I trust it's a better C, but how do I know the combination of the D toolchain that I'm choosing is better than sticking with the devil I know already?

What I would love to see is an opinionated package spun off from D. One compiler, one debugger, one IDE, one standard library. Make my onboarding experience better and take the thinking out of assembling a D toolchain.

Every time I find myself thinking "maybe I should look into using D instead of C for this project", I spend a few hours with D, and then I get frustrated at all the options and I go back to the devil I know.

I truly wish I could be one day convinced that I can install D and I get a solid platform without weird moving parts.


Yes, I completely agree, the onboarding experience is daunting. It's quite hard to understand all the moving parts and figure out which one I should care about or not.


If you were to pick one option, stick with BetterC.


D is pretty exciting. Seems like a perfect choice for those of us looking for an alternative systems programming language and are not completely convinced we'll be happy in Rust.


Yeah I think "better C" is a space which has a really good reason to exist and I'm always interested to see new entrants.

IMO describing Rust as a "C replacement" is slightly off the mark because Rust's value proposition is very different from C. Rust is about giving you the best possible performance in a safe-by-default language. C is about giving you maximal control over memory, with a very thin layer of abstraction over the hardware.

C has traditionally been used in a lot of places where Rust's USPs add a lot of value -- for instance in kernel development -- simply because there wasn't a safe alternative. However I think there are other cases where the strengths of C still add value; for instance in game development you're largely trying to do high-throughput processing over large swaths of structured data, and Rust concepts which help safety like RAII just get in your way. Yes there are ways to get around this in Rust, but Rust is not really optimized for structured, manual memory management.

As a result I think there is plenty of room for something like C which just has better ergonomics and some more modern features.


Many programmers think of the programming world as: "assembly < C < everything else", and that is a misconception that was actually quite easy to "learn" if one wasn't ready to invest a lot of time in looking for more options.

When I see someone saying that they prefer D/Nim/Zig value proposition as a "better C" than Rust, what I see is that people are just starting to realize that there are many more options than C for doing programming tasks that require a more precise interaction with the hardware.

It's just not "assembly", but there are multiple different assemblers with different trade-offs. It's also not "assembler < C" anymore, since in some aspects newer languages do offer more low level control about certain things than C.

It's also not just C/C++/Zig/Rust/Nim/D competing at the lowest-level of the space. Ada, Forth, Scheme, and many other languages are also widely used in this space. They just aren't the "next new thing".


I think that turn over happened when a new generation started to learn computing after UNIX took over the server room and teachers that didn't care to teach anything else.

So only the old generation remembers how things used to be, and those that are curious about the computing history and evolution of programming languages.


D also now supports Ownership/Borrowing (experimental) on a per-function basis, meaning it can be added incrementally to an existing program.


For reference, the blog article on borrowing/ownership from 2019: https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...


D's borrow checker fails in comparison to Rust's. It serves very little purpose and provides almost no guarantees. These issues have been brought up multiple times but they are brushed aside with ignorant responses like "I've been told my entire career what I'm doing will fail but I continue to do it anyways".


Yes, you do create accounts to follow D around and complain about it.


What is the difference between the two?


Rust was designed from the ground up and every feature is implemented and designed to work with its borrow checker. So it is able to provide a definitive guarantee that memory isn't free'd or used after it is free'd. The only exception is in "unsafe" code, where you can break the borrow checker. But this limits where you have to worry about potential bugs.

In D, the ""borrow checker"" is being tacked on as an after thought, in an attempt to copy Rust. This means that it doesn't play nice with existing features and makes it difficult if not impossible to guarantee that memory isn't leaked or used after it was free'd. For example with exceptions. The checker doesn't check for exceptions and if memory is free'd correctly if an exception is thrown. This isn't a problem in Rust because it doesn't have exceptions so it doesn't have to worry about checking them so it can maintain its strong guarantee.


> This isn't a problem in Rust because it doesn't have exceptions

Rust does have something similar to exceptions when compiled with "panic=unwind" (the default). It uses the same mechanism as C++ exceptions to unwind the stack (while calling all the necessary destructors), can be caught (std::panic::catch_unwind) and rethrown (std::panic::resume_unwind), and has some of the same concerns as C++ about "exception safety" (mostly within unsafe code - the programmer has to take care to leave the objects in a safe state when it can unwind).


I didn't know that used something akin to exceptions. Still panics aren't as common as exceptions (in D), and still only have to worry about unsafe code (for the most part).


D's version of Ownership/Borrowing adheres to the notion:

"only one mutable reference to a mutable object OR many const references to an object"

and that's where the similarity to Rust begins and ends. The realization of that principle is quite different.


Rust guarantees memory safety, or are you saying D isn't similar in that regard as well? If that's what you are saying, then I agree completely. D's ownership/borrowing does not provide memory safety, and that makes it pretty much useless.

There's really no point discussing this with you any further. You don't contribute anything to the discussion. That's just PR babble, and provides zero information and doesn't even deny the fact that exceptions break memory safety with D's borrow checker (you know it that's why you don't deny it like a "good" PR person).


> However I think there are other cases where the strengths of C still add value; for instance in game development you're largely trying to do high-throughput processing over large swaths of structured data

That world is dominated by C++ for the most part, though. So D's -betterC would be fighting against an incumbent that is itself also already a "better C".

What value does D's -betterC bring to the table here? Skimming through it, it doesn't look like there's any compelling reason to switch to -betterC from an existing C++ code & knowledge base.


I think a lot of people would argue that many C++ features, like OO, are not strictly improvements. Also C++ is a massive language, and part of the beauty of C is it's simplicity.

I think what a lot of people would want is something which maintains that simplicity, but mainly delivers on basic QOL lessons we've learned in the past 40 years, like that it's nice not to have to pass around array lengths as separate variables.


> I think a lot of people would argue that many C++ features, like OO, are not strictly improvements.

Sure, but D has those OO features, too, so that's an argument against both, not one.

> like that it's nice not to have to pass around array lengths as separate variables.

-betterC doesn't have dynamic arrays, though. That's one of the "unavailable features" currently.

But that's also sommething C++ solved as well - std::vector does exist, after all, as does move semantics to avoid copying it unnecessarily.

C++ is far from perfect, of course, but D's -betterC doesn't really fix any of C++'s issues and it doesn't retain C's simplicity. It's in an awkward middle ground between the two - is that really a viable place to be? For users that already know & have C++ codebases, what's the sales pitch here? For existing C users, what does -betterC do that hasn't already been done and already failed to attract the C holdouts?


One thing that D does better and that on C++ depends pretty much on the team and which compiler toolchain you use, bounds checking for strings and arrays.

You can get them in C++, and VC++ does it by default on debug code, but not all compilers do by default and it isn't required by the standard unless you explicitly make use of the at() variants.

On the other hand, every attempt to bring bounds checking to C has failed, lets see how far Microsoft goes with Checked C.


> part of the beauty of C is it's simplicity.

True, but the trouble with simplicity is a number of things become excessively tedious and error-prone to code - such as ensuring no buffer overflows.

C makes up for its lack of expressivity by adding a text preprocessor. The preprocessor is a tacit admission that the core language simply isn't powerful enough. When people find themselves doing metaprogramming with the C preprocessor, it's really time to graduate to a more powerful language. DasBetterC doesn't need a preprocessor, and its metaprogramming facilities far outstrip the C preprocessor.


> its metaprogramming facilities far outstrip the C preprocessor.

...and are severely crippled by applying betterC constraints to CTFE:

  // CTFE-only
  string genint(string name) {
      return "int " ~ name ~ ";";
  }

  void main() {
      mixin(genint("x"));
  }
Error: array concatenation of expression "int " ~ name ~ ";" requires the GC which is not available with -betterC


You don't have to use all of C++ everywhere when programming C++.

In particular, I really love that I seldom use operator new. Objects are instantiated in the stack and object creation is ridiculously fast.


> C is about giving you maximal control over memory, with a very thin layer of abstraction over the hardware.

Which suggests an avenue I haven't seen anyone take yet: Pick a subset of features common to sane hardware and write a language which gives access to those features in a way which is reasonably portable and as explicit as C.

Pick a spot midway between a macro assembler and a language with a complicated optimizer and give as much access to the hardware as possible while not wedding yourself to a single ISA. Make cross-compilation a first-class feature using a module system, and error out if the programmer tries to use SIMD intrinsics when compiling for an MSP430 or something.


Rust is essentially C++ with auto-generated Move Constructors, and compiler errors on use-after-move, at the cost of preventing library developers from providing function overloads that eat temporaries as an optimization.

So, yeah, really not a C replacement at all imo


I'm not super familiar with r-value references, but doesn't passing-by-value in Rust do essentially the same thing as that optimization does in C++?


Yes.

But this is all a bit uglier at the call site, since either the library provides value-semantically-similar things with different names that either eat their arguments or copy-from-reference them, provides only the argument-eating version and relies on the caller to `clone` at their discretion, or provides only the referencing version and fails to elide copies.

In C++ you can provide a referencing version, and an argument-eating version with the same name that is called automatically when the user gives it a temporary or specifically requests it via `std::move`. Automatically eating temporaries is very nice in the case where the caller would like to compose a bunch of "create-new-from-a-set-of-references" operations in a single expression to create one new thing from an initial set of references.

The canonical example is eliding copies in stuff like

    B*(A*x + b) + c
with overloaded * and + for vectors, since you really don't want to use something with a name other than + to request copy-elision. If you are one of those people who is grumpy about operator-overloading, you can imagine doing this with other "copy-some-refs-create-something" type functions, ... but actually you are probably grumpy about overloading those too.


There is a Rust idiom that works a little bit like that overloading case:

    fn eats_a_string(s: impl Into<String>) {
        dbg!(s.into());
    }

    fn main() {
        eats_a_string("foo");
        eats_a_string("foo".to_string());
        eats_a_string(&"foo".to_string());
    }
In that case, eats_a_string() will allocate internally in the first and third cases, but it won't allocate in the second case, because the caller gives up ownership of its own allocated String. I wouldn't say this is a super common idiom, and "provide only the argument-eating version and rely on the caller to `clone` at their discretion" is often preferred to be simpler and more explicit. But you see it occasionally in the standard library, for example here: https://doc.rust-lang.org/std/ffi/struct.CString.html#method...


This sort of pattern tends to be more useful in case it's something like `CustomString` rather than just a regular `String`, and you still want your function/method to work with regular string literals. Also, other similar usecases, where there's a custom type that can be made from a standard type.


Can you expand on this point of view?

I would enjoy reading a couple paragraphs, or a blog entry about this.


The only place where I think C on kernel will always have a place is on UNIX clones, they are just too symbiotic.

So as long as UNIX clones exist C will be around. OSes with POSIX APIs is a different matter, because there the kernel can be something totally different, e.g. mainframes.

So for that, we still need something like Checked C.

Or as Oracle, ARM, Google are doing, hardware memory tagging.


D is a much better option for those that believe that it isn't a crime to have a GC on a systems programming language.

It basically follows on the school of thought at Xerox PARC, ETHZ, and Microsoft Research.

Now what it lacks is more more manpower to improve its runtime capabilities and having a big name actually pushing it forward.

However this doesn't need to be a zero sum game, any language that helps to fix C is welcomed to the party, including attempts like Checked C and Frama-C.


the betterC form of D does not use the GC (and the compiler will let you know).


I used to be a regular on D forums. :)

Yes, betterC does not use the GC, but I was speaking about the whole language, and many anti-GC folks eventually discover that they can stick to regular D and still deliver what they were trying to do.

You can imagine an OS written in D, where BetterC mode gets used in the layers that for whatever reason cannot afford a GC, while all the remaning layers can happily take advantage of it.


Agreed. I've never felt the need to use betterC. The biggest use case I think is writing modules/libraries for other languages to use.


Mine is Zig.


Are you aware of anyone with large projects in Zig? I'm curious how fast it compiles, given in my view faster compilation is one of the biggest advantages of D over C++ and Rust, due to it having a lightweight compiler, DMD. Zig seems to still use LLVM, like Rust, which is relatively slow.


In his live-stream three days ago, Andrew Kelley showed the beginnings of Zig directly generating debug-mode executables, very quickly. And that’s before incremental compilation and linking. This guy is amazing.

https://www.twitch.tv/videos/602715503 [edit: at 10:20]


Ah, that's awesome! Really cool to see a new language taking compile-time so seriously.


Fast compile time isn't actually that hard for a C-like language. Good code optimization and fast compile time is hard.


Fast compile time on large projects is really hard for C and C-compatible languages due to the preprocessor and #include. Not an issue if you're comparing compilation for a single file, but it turns into a substantial fraction if your compile time when that file transitively #includes 10s or 100s of headers.


Zig is still very experimental though, the language isn’t stable yet


True, but I'm not in a hurry. We've waited a long time for a C/C++ alternative, we can wait a bit more. Some find it in D, some find it in Rust, some (like me) are waiting for Zig, and some for something else.


Definitely. However I will say that Zig does so many things right already that I struggle to code in more mature languages without thinking "I wish I was writing Zig right now" all the time.


Andrew needs to radically change his stance on Tabs for the language to be taken seriously. White space must be ignored, and Andrew should overlook his personal bias and cater to the ‘preserve white space’ crowd. Because formatting and readability matter - a lot.


You refer to "Andrew's stance on tabs" without being specific or providing a reference, so I wend digging for more details in the language FAQ.

From the reference I found[1], Andrew's stance is: The current zig tooling forces spaces instead of tabs because the tooling for the language is not yet complete. The self hosted compiler (which the community will start using just as soon as it's ready) already does accept both tabs and spaces.

So, I'm not able to understand your complaint at all. Perhaps you could you elaborate, provide a reference, or soften your stance?

[1]: https://github.com/ziglang/zig/wiki/FAQ#why-does-zig-force-m...


I am very curious why the stage1 compiler cannot just run zig fmt prior to parsing the code as a stopgap until the self-hosted compiler is complete…


I'm not looking for an alternative to C but I used this quarantine to start nim, and it feels quite right until now


Nim actually happens to be a very good better C. The fact that it compiles to C is a key feature of that.


For me the single best feature that stands out in D is the uniform member access using dot.

- Want to access member in a aggregate (class/struct)? Use .

- Want to access member in pointer to an aggregate? Use .

- Want to access member in a module? Use .

- Want to access reference (not free standing)? Use .

This makes switching between different implementations pretty easy. Coupled with UFCS, this is pretty fantastic!


I can strongly relate to that. You can write a custom "fill" method for your array and just do "arrray.fill", bam! It just works. You want numpy "zeros"?

Just do a template

void zeros(T)(ref T arr) { arr.each!"a = 0"; }

someArr.zeros;


Walter here - AMA!


A compiler related question:

A often recurring C idiom is passing a pointer to a struct as function argument where passing the plain struct by copy would do. A big reason is the good old 'passing by pointer is faster', which I thought to be no longer relevant with modern optimizing compilers. Of course I found out the hard way that even on modern compilers object copies are not elided on call. I had performance sensitive parts of my (non-x86) code that were dominated by the compiler's builtin memcpy, due to my struct-happy coding style (e.g. rolling my own range struct and passing it by value everywhere).

I understand mostly why eliding argument copy is so much harder than eliding a return object copy, there are so many ways to observe its effect, and you have to obey the calling convention. Another aspect is the lack of programmer-communicated immutability in C, which you have addressed with D. Does the D compiler help in this situation? Can it guarantee that (immutable) argument copies will be elided in certain circumstances? (e.g. in file-scope static functions)


> Can it guarantee that (immutable) argument copies will be elided in certain circumstances?

No, but it's an interesting idea I never thought of. By the way, passing by 'ref' works handily and avoids icky pointer passing, while being efficient. I recommend as a "best practices" coding style using 'ref' parameters instead of pointers where possible.


> A big reason is the good old 'passing by pointer is faster'

Not even with old compilers, necessarily, this is more about the CPU architecture. If you've got an architecture with no cache (eg 386 and below for Intel) you'll not care about cache miss vs cache hit, but once you have cache you'll often find that the pointer dereference will hit main memory, and thus be slower than just passing by value. Not always, of course, but sometimes, so since 1989 (for Intel) you've needed to profile that to be sure.


The proposed NUBI ABI[1] for MIPS was interesting here in that for call by value if a struct was register sized, it would be passed in a register, but if larger it would be implicitly passed by reference. See section 3.4, Arguments.

It was then up to the callee to make a copy if necessary, say if it modified the struct contents.

Hence it would have been possible to elide the copies, on a per function basis, depending upon how the function used the structure.

[1] ftp://ftp.linux-mips.org//pub/linux/mips/doc/NUBI/MD00438-2C-NUBIDESC-SPC-00.20.pdf


Thanks for the interesting pointer.

I was also thinking in the direction of the compiler transforming the call-by-value struct object argument into a call-by-ref one at specific call sites. e.g. when the object is clearly on the caller's stack, is not mutated by the function and the function is not taking its address.

As Walter pointed out, you can use refs in BetterC (and of course C++) directly, but I don't see why it cannot be automatically applied to C in general.


Another complication to the normal "pass structs by pointer" is that small structs are actually split up and passed in registers depending on the platform ABI, which can be more efficient in certain cases.


Hi Walter, I have two questions:

1. Why do functions need to be annotated with @safe and nothrow in betterC? Why not make them default? I understand making it default for non-betterC might break some code.

2. string type was uniform and awesome until it was treated as Unicode. Any plans to fix this and remove auto decoding? That would be awesome.

Edit: three questions to two. I had another question about using threads in betterC. But looks like we can't use D threads as they are runtime dependent.


1. Making the defaults different between D and betterC would result in an impedance mismatch that would cause more trouble than it is worth.

2. I think you're referring to autodecode. There is an effort ongoing to extricate us from that, but it's difficult while maintaining backwards compatibility.

3. That's right. You'll have to use C threads.


Would you be open to being interviewed for the Computer History Museum? I suggested this in the CHM feedback form, but I'm not sure if it disappeared into the void or if the form actually even works. I know that sometimes even their search chokes.

See their Oral Histories collection: https://www.computerhistory.org/collections/oralhistories/


Sure, I think that would be fun.

We've got a paper on the history of D accepted into this year's History Of Programming Languages (HOPL), which makes us very proud. Andrei, Mike and myself spent a lot of time combing through old emails and n.g. postings to develop an accurate timeline of when and how things came about and from whom.

I was really looking forward to the HOPL conference in London in June to present the paper, but CV scuttled that.


Hi Walter, Thanks for doing this AMA!

Is "Better C" Done? Or are there any features/changes being planned? Will it always stay backward compatible?


It's done for the time being. I'm sure we'll find ways to improve it, though! We try pretty hard to provide easy ways for existing code to adapt to future D. We're not quite ready to be craven to every past mistake yet!


Hi Walter, I am a bit late to the thread but thank you for all your work and for joining this thread.

What is your vision for D/betterC/SafeD?

Is the intention at the moment to stay as a systems programming language only, or is your vision that D or betterC or SafeD gain traction as an embedded target language?

I definitely think the focus on correctness and the ease of unit testing would be great in the embedded development space.


BetterC is very suitable for embedded work. I wish I had it available when I did such work.


What's the best way for an experienced C programmer who doesn't know C++ to learn D?

What's the story in terms of using existing C (or C++) code with BetterC or D in general?


> What's the best way

I'd pick up a comprehensive book like Ali Cehreli's "Programming in D",

https://www.amazon.com/Programming-Tutorial-Reference-Ali-Ce...

and come hang out in the D forums:

https://forum.dlang.org/


> What's the story

You can mix and match C and D code easily in the same program, and to a lesser extent C++ code. This means a larger project can be incrementally converted to D while keeping it running and shipping.


Book "Learning D" by Mike Parker


- what is the status of the language now?

- what is the status of the ecosystem now?

- what are the big issues that make d less appealing?

- ms alexandrescu is still involved in the project?

- where do you see the language in 2 years for now?


Do you know of use of “BetterC” in real-life (i.e: for non-pet projects)? It would be interesting to hear about some real use cases.


I know several people like it and use it very much, but I haven't followed their projects. Many were skeptical of the BetterC project, but it has proven its worth.

I've used it myself to easily convert some of my older C programs to D and make them easier to maintain.


BetterC is interesting because it allows you to compile D to WASM. Unfortunately D runtime and GC haven't been ported to WASM yet so they are not fully supported.


It was recently discussed in the forums, it should be coming, the actual situation has delayed the development.


We have 100kLOC of not -betterC but D without its runtime. If you write a -betterC library normally you can use it with normal D, but the reverse isn't true.


libmir[0] is being used in production by different companies for numerical processing.

[0] https://www.libmir.org/


Hi Walter. I have two and a half questions:

* Regarding the syntax of 'lazy'. It seems to me that it would make for better readability if the lazy keyword were required at the callsite, along the lines of doStuff(lazy getValueUsingExpensiveComputation()); rather than the current syntax where it's not clear at the callsite which, if any, of the arguments are lazy. C# does something similar with ref. What's the thinking behind D's syntax?

* What's the state, and future, of precise garbage collection in D?


Relevant post in a discussion thread that's related to the future of lazy in D: https://forum.dlang.org/post/ngkvntcrfdbsnxzfmcky@forum.dlan...


Yeah, lazy is a failure. Can't hit a homer every time. I'm not sorry we tried it. But it's time to take it behind the woodshed.


Why is lazy a failure and what makes it so?

And are there any languages where lazy is not a failure in your opinion?

Edit: removed link to wrong swift feature proposal.


> What's the thinking behind D's syntax?

Mainly that it be easy and quick for those familiar with C and C++ to get up to speed. With C, C++, and D, you cannot really know what will happen with the argument without looking at the corresponding parameter declaration.


Got it, thanks.



That page links to [0] which says that certain patterns become unsafe using the precise GC.

[0] https://dlang.org/spec/garbage.html#precise_gc


Would it make sense to spin "Better C" off into a separate language? It would for example help to create an ecosystem of 3rd party libraries not dependent on gc.


Seconding the question. More to the point, a language that only needs libc and not a separate runtime.

It's a big boon that existing C projects can migrate one object file at a time to betterC, without needing to pull in extra dependencies. Moreover, embedded dev is where C is still the most entrenched due to its runtime simplicity (simple ctr0, linker script, and you're off).


You can certainly treat it that way. It specifically does not need its own libraries, all it needs are the usual C libraries. Libraries created with it are also directly accessible from C.


No. D already has image problems with D1 and D2 (as in people crop up on the forum asking about it a decade after the switch), making D better C a separate language probably wouldn't be a great idea


I'm sorry that the question is not really related to your compilers but I'm curious about your opinion on Zig?


I know nothing about Zig.


You should probably address that. It's a serious competitor to D (as better C). At the very least, you might see some ideas worth stealing!


Nitpick: Zig is more a competitor to the Better C subset of D, than to D itself.


You should definitely check Zig out. It's a relatively simple (compared to D, Rust, C++) language with C-level semantics but more modern syntax. It is designed to be extremely easy to interface with existing C source code, enabling one to convert a program from C to Zig incrementally and with minimal headaches

https://ziglang.org


Not included as part of the subset linked above, but D's Contract Programming piqued my interest: https://dlang.org/spec/contracts.html


Ada (https://learn.adacore.com/courses/intro-to-ada/chapters/cont...) and Eiffel (https://www.eiffel.com/values/design-by-contract/introductio...) also support contracts.

Contract-based programming is a very nice way to quickly find errors and specify how different parts of the program should interact.


C++20 almost got them.

There are some Java annotation processors that rewrite the bytecode for contracts.

As extra info.


Java annotations have always struck me as interesting since they fill the Lisp macros and C preprocessor niche for Java.

It's another layer of complexity, but it extends the language to offer a bit more richness or layers of abstraction.

I would not be surprised if a future C++ standard adds contracts officially. Boost seems to already have contracts: https://www.boost.org/doc/libs/1_67_0/libs/contract/doc/html...


Yes, many aren't aware how much annotation processors and compiler plugins are capable of.

On the .NET side you have Roslyn, Attributes, expression trees and on F# quotations.

While they are all a bit cumbersome to use versus what Lisp macros allow for, they already allow for quite a lot.

Yes, there is some hope that C++23 will bring them.

The more languages support DbC the better, only so can we start seeing more widespread adoption across the industry.


Now with WebAssembly support thanks to LDC, the Better C experience is definitely much more productive than dealing with emscripten.

Then again, I am biased.


The chosen example (printf) doesn't make D look very good compared to C. If I modify the example to include a simple type error:

    printf("Hello %s", 123);
Then the D version will still happily compile without warnings and will segfault.

Compiling with GCC -Wall -Werror, the type error is easily caught by the compiler.


Use dmd master and you will have printf/scanf format argument validation [0].

[0] https://github.com/dlang/dmd/pull/10852


Yeah, I am glad we fixed that one.


The problem is that since this Better C is not compatible with any C dialects, and requires a D compiler, there is no reason to restrict D programs to this subset. At least no reason that is related to simply working in something better than C. The real reasons can be articulated better, starting with a different name, like "Reduced D" (D that avoids garbage collection, exceptions and/or has a smaller footprint or whatever). How about "Dm": D Minor: a sad subset of D.

A subset of C++ is a better C, that can be written in such a way that C compilers translate it, so that is meaningful. Well, at least a slightly better C, anyway.


every time I've looked into this I've found the documentation severely lacking in examples of how one would go about incrementally migrating a c project to better c. does someone have a pointer to a blog post or even a git commit that illustrates the first step of migrating a single c file, with all the attendant makefile changes?


The dmd backend itself used -betterC to make the conversion from the original C-with-Classes code. DMC++ itself underwent the conversion to D using -betterC.

https://github.com/DigitalMars/Compiler/tree/master/dm/src/d...

It still looks a lot like C.


1. rename the file from prog.c to prog.d

2. set up to compile with dmc prog.c -c -betterC

3. replace all the preprocessor stuff

4. compile it, and fix the errors diagnosed by the compiler

The hardest part will be how much metaprogramming was done using the C preprocessor. Once that's dealt with, the rest is fairly mechanical.


Regarding the ongoing auto/var readability issues debate, how about the popular dynamic languages that often do not even have the means of specifying a type? They surely must be suffering from huge readability issues?


Such languages (e.g. python) serve a different purpose, at least in my opinion. Using python as scripting language for small "adhoc automation" things is great.

For long-living and bigger code bases, worked on by teams, explicit types and "proper" static type system are the better choice.

E.g. it's less about readability, but maintainability.


I find using Python libraries where the documentation doesn't tell me anything about the return type a hugely frustrating experience.


This is a nice tutorial introduction on using D as a better replacement for C [1].

Some excerpts "At one time, C was my go-to language for most programming. One day I realised that most of my C programs kept reimplementing things from C++: dynamic arrays, better strings, polymorphic classes, etc".

[1]https://theartofmachinery.com/2019/04/05/d_as_c_replacement....


I think D is great for scripting.. and having no python experience but java/c/c++ it feels more familiar. The standard library is useful.


> To link D functions and libraries into C programs, it's necessary to only require the C runtime library to be linked in.

Not that I'm suggesting this would be a better approach, but wouldn't it be possible to link both the C and D runtime libraries and wrap the main function to do the initialization?


For those who have been around a while, this is not the same as DailyWTF’s Better C dialect.

https://thedailywtf.com/articles/The_Secret_to_Better_C


Feels like a more accurate name would be "Freestanding D" but it's a cool idea.


I actually wrote "D as Better C" before realizing "Das Better C" sounded much better. And a shout out to all our German speaking D users!


So D-- then?


I think they should call it only slightlybBetterC

All the features I like in D such as dynamic arrays and associate arrays are gone


Ohh! Ohh! C and C++ are used. How about ℂ (double-struck C) to mean "Like C but double the fun!"


Reminds me of Ada++


It's interesting how this works:

- enumerate everything C is doing bad.

- presents fixes with a full bag of unnecessary features that no c programmer wants.

I wonder why there is no "Fixed C".


The trouble is, everyone has a different view of what is unnecessary.

Fortunately, you can simply not use any feature you find unnecessary. We won't harangue you about it, I promise! (Well, maybe a little bit if it's so cool we just can't resist.)


That is why they are now persona non-grata on Apple, Google and Microsoft platforms.

So on Apple platforms they will eventually have to deal with C++ and Swift no matter what.

On Microsoft platforms, they complain about C support and get shown the door to C++.

And on Google platforms, given the years of ignoring security advices, starting with Android 11, hardware memory tagging is a required feature, with the kernel randomly attaching GWP-ASan to processes, while on ChromeOS Linux gets to run on a hypervisor sandbox.


What I wanted: The ability to query heap objects for their size.

What I got: some 50,000 foot pure functional typeless language with loads of magic in the compiler that you'll never be able to reason about without having written one yourself.


There have been many, many "Fixed C"'s.

That's the problem. This isn't a technology issue, it's a social one.


I'm actually curious. until now I came across new language that sells themselves as c replacement but with a lot and lot of new features, usually "here is a new language it does a lot more than C but still compatible with it".

examples in top of my head:

- adding object oriented feature like built-in constructor/destructor while only default values are needed as week as a defer statement.

- weird template, while I can sense that we only want a proper hygienic macro system.

And "array type", pointer with length, to deal with contiguous memory.

The rest is well described in the video, but those 3 points are my major issues.


There's no such thing as a hygienic macro system if you mean textual macros.

Also array type is the single biggest thing required to fix C. How many bugs in big C projects are caused by bad indexing and overruns.

On top of, "oh no free features".


I am hoping that at least Microsoft's Checked C doesn't die like so many other attempts.


Here's my proposal for fixing that in C:

https://www.digitalmars.com/articles/C-biggest-mistake.html


There are already two very successful "Fixed C"'s in wide usage: C++ & Objective-C.

The remaining C usage is a mix of existing domain expertise, existing large codebases impractical to migrate to anything else (eg, Linux), or a rejection of anything more than what C provides.

In all of those cases anything that's not exactly C is unlikely to motivate any changes. The users that could change to a "Fixed C" already did decades ago.


Sorry to see this at the bottom of the thread, because you have a real point here. None of the attempts at "better C" or even "replacing C" have been serious attempts, and it's frankly getting a bit insulting. No one who still uses C wants its replacements to "catch up" on the past 20 years of new language development. We want C, but without the problems. We don't want D but less so. The response below - "you don't have to use any features if you don't want to!" - is a ridiculous cop-out and the same thing people who defend C++ are saying.


While I understand the value in a small language that you can wrap your head around, I think that that is a relatively uncommon problem that real-world language developers shouldn't be catering to today (and C certainly isn't small or simple). These days, programs are simply expected to do more while not suffering from the kinds of security vulnerabilities that C code is so chock-full of. A lot of the solutions for these problems necessitate an increase in complexity.

I can understand that "you don't have to use any features if you don't want to" is somewhat silly when talking about C++, since it has so many features that can bite you when you least expect it, especially when dealing with years of legacy code. D is far better designed than C++, however; it would not take you nearly as long to learn enough D to be comfortable reading others' code.

All I'm trying to say is that "C, but without the problems" might actually look more like BetterC than you think it does. True zero-cost abstractions are clearly still an unsolved problem, but abstractions are a necessity. C's niche is rapidly disappearing.

Also, out of curiosity, what don't you like about Zig? I saw this on your website, but I couldn't find any specific issues you'd had with it.

> Wow, Zig is competitive with assembly?

> Yeah, I totally had the same reaction. I’m interested to see how it measures up under more typical workloads. People keep asking me what I think about Zig in general, and I think it has potential, but I also have a lot of complaints. It’s not likely to replace C for me, but it might have a place somewhere in my stack.


Thanks for your comment. It's pretty normal to face this downvotes when talking about language. And my phrasing was not the most kind of all.

It's just the CS community, at some point when you are writing languages you tend to try to outsmart the language you are fixing by creating overly complex behavior. Over-engineering is every single new languages.

Even Zig fails to do better than C, I think it's syntactically a disaster.

Here is a few lines from the documentation:

  const ptr = @intToPtr(?*i32, 0x0);

  while (it.next()) |item| { /* ... */ }

  pub fn main() anyerror!void { /* ... */ }

  exe.addCSourceFile("test.c", [_][]const u8{"-std=c99"});

  const file = std.fs.cwd().openFile("does_not_exist/foo.txt", .{}) catch |err| label: { /* ... */ }


Do you not like those because they're unfamiliar? What about the syntax indicates over-engineering?

Here's what I see in your example:

- @ indicates built-in function, which avoids namespace collisions.

- Optional types via the ?, which solves the null pointer problem.

- Real iterators, which are less error-prone and nicer to read than traditional for loops in a lot of common applications.

- The || syntax is better than something like `for (type var : array)` in Java, don't you think? Especially since it works with any iterator?

- anyerror!void is a nice way of saying that the function can return an error. Remember that we don't have exceptions here.

- It has interoperability with (multiple standards of) C.

- Error handling is higher-level and less error-prone than in C without exceptions that can create hidden jumps in your code.

So it clearly solves issues that people run into frequently in C. It brings in a nice sampling of high-level language features without doing anything that would compromise its niche as a systems language. It still has manual memory management and does not have exceptions. The behavior is simpler and requires that you language-lawyer the specification for undefined behavior far less often than in C. Your knowledge of C isn't necessarily enough to be able to immediately read Zig code, but that's because Zig isn't C. It's not hard to learn.


Thanks for spending your time explaining this. I think a lof people will understand those lines better.

I actually already knew everything. I watched closely the development of Zig, and just give up when I realized all those decisions where made carelessly in my view or I just simply disagree with the direction of the syntax.

Since you took your time I will take mine to address what I don't like:

- There is no way to write "int" by default. I understand why. But I don't agree.

- struct and enum statements (within braces) are separated by a comma which is inconsistent with statements in function separated with ",". In my view expect a difference between parameters and statements. parameters (a,b,c) statement {a;b;c}

- You have to type "var" or "const" everywhere, but not in struct statement and enum statement. Sometimes it's like a "def" sometimes not.

- You have to type const when importing module:

  const std = @import("std"); // Why ? Why should I specify const, this should be inferred. Same when I define a struct. 
- the "undefined" keyword when it means "uninitialized" even in the documentation.

  var my_var: i32 = undefined;
- The worst, no default values for struct.

- The list of keywords is insane: errdefer, allowzero, orelse, unreachable, anyerror

- About iterator I was doing well with:

  while(get_next(context,  &item)) { /* ... */ }
It's like everything has been carefully design to be even less readable than C code. I have the feeling that the syntax help more the compiler than de developer.

I wish I could bet thousands of dollars that there will be only one compiler in the whole life of Zig language (also because it relies way too much to LLVM).


> It's like everything has been carefully design to be even less readable than C code.

D is designed to be very readable to the C programmer. Some changes, like replacing:

    (int)(expression)
with:

    cast(int)(expression)
is designed to make the code more readable, and greppable. Cast is a fundamentally dangerous operation, so being able to grep-and-check for such is worthwhile.

The D compiler actually recognizes the C form and suggests using the D form to fix it.

Converting C to DasBetterC is largely just a) eliminating use of the preprocessor and then b) making the syntax changes suggested by the compiler.


Yes, I can see the design of D was made carefully.

D is actually a great language. I just "don't understand" why it's not massively used instead of more recent versions of C++, I consider newer version of C++ as a totally different language with more drawbacks than advantages. Half of new features are present to fix previous half baked features. This is quite embarrassing.

Sorry for my selection of words (and for the unrelated opinion, I had to rant), I'm not good at spending time to rephrase when the end content is the same.

I remember one time I spent the whole day browsing the website of D, everything actually make sense. There are still features that I wouldn't use, but this is what I expect from a programming language design. I was impressed by a lot of decisions.

I seemed to remember that I was a bit sad to not find proper performance benchmarks against other languages. I was intrigued because the documentation was so complete, it was just lacking this (maybe I just couldn't find them). It was a long time ago so it has probably been fixed. I will give it a try at some point.


The performance of D vs C and C++ should be identical for code written the same way, as they share the same optimizer and code generators.

Also, we used to publish benchmarks. These inevitably did not produce illumination, but long ripostes from people arguing that the benchmark was unfair, inaccurate, nobody would write code that way, we sabotaged other languages, etc.

We encourage people to run their own benchmarks on their own code and let the results speak for themselves.

One issue with moving to D is it takes a while for people to learn "the D way". For example, if they come from C they write C style code in D. From C++, they write C++ style in D. From Python they write Python style in D. Inevitably they'll run into some difference where D doesn't have an analogous feature, and would get a bit frustrated (even though in D the task would be accomplished a different way).

It takes a bit of perseverance and faith to get through that until one discovers "the D way" and then they're hooked.

One of the reasons for DasBetterC was to reduce this issue as much as possible, though the people who like metaprogramming with the C preprocessor will have more work to do in getting adapted to D's powerful metaprogramming features, which of course work nothing like text macros.


> Also, we used to publish benchmarks. These inevitably did not produce illumination, but long ripostes from people arguing that the benchmark was unfair, inaccurate, nobody would write code that way, we sabotaged other languages, etc.

Oh, this is pretty sad...

Anyway thanks for your time. Was nice to interact with the author of D. I truly believe what you've done will inspire a lot of people.

I hope you didn't encounter too many people saying creating D was foolish.

I spent time checking about a lot of languages and only D (and Jai, but it's not released) would restore my joy of programming.


> The worst, no default values for struct.

This is incorrect. There are other factual errors here as well.


Thanks for replying! Most of what you wrote makes reasonable sense to me. I guess that a lot of the syntax decisions are subjective. Even if I don’t necessarily agree with all of them, I can appreciate your viewpoints better now.


Did you reach Zig’s developer regarding the issues you mention? The language isn’t stable yet, good time to try to influence it if you think they are doing some mistake.


I'm not interested in spending time fixing something that is not considered broken by someone else. They are not bugs that have been introduced by mistakes.

Don't get me wrong, I like to help people. But I learnt to not help people that do not seek help.

You get a plumber fixing your pipes because you need your pipes to be fixed, but if every single plumbers were offering there help it would be annoying.

There are already more that three hundreds proposals:

https://github.com/ziglang/zig/issues?q=is%3Aissue+is%3Aopen...

A lot of them are about the syntax. Maybe this won't be endless but I'm sure a lot of time are being wasted. The root of the issue I point seems to be bound to the way the devs are working. Not sure if it will be efficient to tell them to "think more carefully". I do believe my time can be more profitable elsewhere (like commenting here, ah ah).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: