And that's a huge shame. Because in general I really liked using D.
And that's a huge shame. Because in general I really liked using D.
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.
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.
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"
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));
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 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.
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.
Most code samples get automatically tested, but READMEs currently do not.
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.
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.
https://doc.rust-lang.org/std/vec/struct.Vec.html#implementa...
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'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.
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)"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."
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.
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.
They want to have a generic function that returns opaque types implementing different interfaces depending on the inputs. I've replied to that above.
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).
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.
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.
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.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.
Functor f => (a -> b) -> f a -> f b
Functor f => (a -> b) -> f a -> f b
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.
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 {};
}
}Ahh.. welcome to computer "science", the ever repeating cycle of 'inventions'
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.
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.
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.
ceylon could, but they are barely readable anyway.
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?
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.
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.
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.
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.
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 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.
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.There is something so absurd about having "unnamed types" as an antipattern!
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.
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.
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.
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
The docs on static if may shed some more light: https://dlang.org/spec/version.html#staticif
Template arguments need to be known at compile time, and the extra set of parens is how templates parameters are declared in D.
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.
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.
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).
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.
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.
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.
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.
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.
for (Map.Entry<SomeLongType, AnotherLongType> x : someMap) {
final var key = x.getKey();
final var value = x.getValue();
...
}
Is valid. for (var x : someMap) {
final SomeLongType key = x.getKey();
final AnotherLongType value = x.getValue();
...
}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?
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.
https://devblogs.microsoft.com/cppblog/template-intellisense...
Now try that on vim.
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>. MyClass myVar = something ? SomeFunction() || somethingThatMightBeASubClassOfMyClass var myClass = ...
(or some other descriptive name of the variable)?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?
Could you please paste some example of a function that has a return value which is declared as auto?
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
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"
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".
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.
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.)
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?
auto auto map(Range) (
Range r
)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.
Maybe read and try to understand the complaint instead of pointing out something unrelated?
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.
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.
At least that's what I understood.
In practice though, D codes fast and runs fast, as promised on the official site.
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.
I wonder if the document could describe (in some regular way) how those auto types are constructed...from what input, with what operations?
(I see my snarky comment there got no reply.)
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.)
LLVM's coding standard though doesn't seem to have anything to say about implicit conversions: https://llvm.org/docs/CodingStandards.html
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.
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.