Hacker News new | ask | show | jobs
by gmueckl 2245 days ago
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.

2 comments

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."

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