Pre-RFC: Struct constructor name inference

That's not exactly what I'm saying. Rather than "don't do that", I want to say "let's provide tools to make it easier to avoid longer functions". This is how ergonomics can aid readability.

Hence your argument seems like a generic one against type inference.

Very much in agreement with this. I can only see this kind of feature being used to create difficult-to-parse code. In the case of anonymous functions, you at least have the definition right next to its usage, but this idea could possibly allow definitions in entirely different crates to be inferred without some arbitrary restriction.

Yes, it is a generic argument against Hindely-Milner type inference. I think that, while judicious use of backflowing inference is fine, it is way too tempting to use (given how often I have to ding people for using auto, and C++ type deduction is far weaker than HM inference) [1]. That said, "you can already do this" is a really bad excuse for extending type inference to more places. Aggregate literals for { x: i32, .. } types are fine, but conversion to named types very much is not.

[1] There is a separate reason I'm not a fan of strong inference, i.e., strong, constraint-solving inference, strong metaprogramming, and always-terminating type checking are a CAP theorem-type situation, though it is not terrifically relevant.

I don't think ergonomics can solve a lack of organizational discipline, which is what I'm really worried about. Functions are long because organizing code is Hard. I will underscore that I review a lot[2][3] of C++ at work, and I still see these problems with weak auto inference.

[2] Unfortunately I can't get a per-language stat breakdown to back this up. =(

[3] This is C++17 with all the bells and whistles, too, and library equivalents of try! and whatnot.

1 Like

I personally think HM inference is great (but the principal type property is something I think we should sacrifice for higher ranked types, GADTs, etc.) as it enables great productivity when prototyping (write 2-3 functions, get the compiler to tell you want the type is, then paste the typing annotations) and makes static typing (sorely needed!) easier to sell.

I'd note that in my experience from Haskell, with its HM global type inference, typically code is not long and deep nor is it common to see top level declarations without type annotations.

C++ has the disadvantage that it isn't sound. Concerns regarding strong type inference in that light is understandable. I would absolutely agree with more type annotations in unsafe code.

Just to be clear, my argument is not that "what's one more bad thing". I think extending type inference would be a net positive (we disagree on that, and that's fine).

Agreed that it is not super relevant because Rust's type system is turing complete. (Not to speak of type inference, which has many ways in which the principal type property is not upheld.)

Ergonomics is not a silver bullet and I would agree it cannot replace organizational discipline. You'll need a hail of bullets including ergonomics, lints (e.g. to reject too-complex functions), and organizational discipline.

I'm no stranger to messy code bases. Take rustc as an example. The rust-lang/rust repo has some huge files and messy, long, and deep functions which are not easy to read. My experience is that this happens primarily because it is so easy to say "We'll just do the refactoring in a follow-up.". The temptation must be resisted or you need a system to actually do the refactoring in a timely manner. Eagerly create new functions and plan for growth.

And you know exactly how I feel about that. I don't think that's an argument you want to make because it can be used to justify a lot of bad design. There is a certain... "if you give people toys, they will play with them."

Unfortunately, we've reached an impasse, because I can construct totally opposite arguments from reviewing thousands of lines of C++, and we are 100% going to get nowhere.

Right; this is a bigger problem, which I have always solved with "no, this CL will not be approved in this state". Unfortunately, most people don't take this seriously enough... which explains really draconian approaches like Go.

1 Like

That's a fact of life possibility with inference, not something specific to this feature proposal.

For example, this is no better:

fn foo() -> T {
  let x = Default::default();
  // A whole lot of garbage.
  x
}

General inference will always require taste from the programmer to have good readability.

1 Like

Centril already showed that example (well, the worse version of <_>::default(), but point taken really.

I'm just not happy with the idea to extend inference even more with the justification that "well you can already do all this bad stuff that you need discipline about". Having places where you must write types (like function declarations) is valuable.

1 Like

Agreed, but we have the reason for why function declarations have them (they don't need them technically, as language like SML demonstrate): so that errors in one function don't break other functions.

If you can come up with a decision procedure for where inside a function they should be there (even if technically unnecessary), then we can have a conversation about whether that should become a rule. But until then, the precedent is for unrestricted bidirectional inference in function bodies.

Yeah I really wish that there was a principled approach to this like, at all. The only reason you want this is readability, which is extremely "vertical" (in that you care about both lexical and type information at the same time) and subjective... the common wisdom for "when to use auto" is murky enough in C++, and last time I sat down to do this I got stuck at "when do I force people to write Vec::<T>::new()?"

I can create a thread to brainstorm such a principled approach, if you think other people would be interested.

2 Likes

Why not? A separate thread will capture the subject better should Rust ever turn that way, and in the meantime it will let this thread refocus on its original subject.

1 Like

One thing worth noting is that Rust type safety mainly takes place at function boundaries, i.e. it is often not as cost-free to extract big functions into smaller functions, and it's indeed true that type inference can be a big problem in enormous functions. (I always end up needing to write

Nevertheless it's a totally different problem. "well you can already do all this bad stuff that you need discipline about" is indeed not a good reason, but the actual solution is to come up with a solution that solves the existing problems when we see that this becomes the bottleneck for more features that are not necessarily bad. For example, if the compiler or clippy warns about a value's type is too many line distant from the inference site, this problem is less significant.

I would be interested.

I have to say that I love extensive type inference in most cases, but something about this particular proposal still feels off to me. Perhaps it is the inconsistency-induced surprise factor?

In all other cases, the type of a literal is known just by looking at its value (and ultimately, construction syntax), e.g. "foo" is always &str and 3.14 is either f32 or f64. So far this has been the case in the context of struct literals too, because the type name was part of the literal construction syntax; but now, looking at the provided code example, _ { field: value } annoys me because it looks like a literal but it is missing the usual immediate type information associated with such "leaf" constructs.

(I know that structs are technically not leaves, but because of their usage and "atomicity", to me they are like so.)

Instead of comparing it to f32/f64, I would rather compare it to trait-related inferences like <_>::default() as mentioned above.

This!

It would, for instance, allow to have constants using ineffable ("Voldemort") types:

#[allow(bad_style)]
const get_ft = || 42;

And in a similar fashion, to have type-level encoded information:

trait TypeLevelConfig {
    const NAME: &'static str;
    const AGE: u8;
    ...
}
macro_rules! type_level_config! {(
    $(
        $ident:ident : $value:expr
    ),* $(,)?
) => ({
    #[derive(Clone, Copy, Debug)]
    struct Config;

    impl TypeLevelConfig for Config {
        $(
             const $ident = $value;
        )*
    }

    Config
})}

fn with_config<C : TypeLevelConfig> (_: C)
{
    // use C::NAME and C::AGE
}

fn main ()
{
    const CONFIG1 = type_level_config! {
        NAME: "CONFIG1",
        AGE: 42,
    };
    with_config(CONFIG1);
}

Although const generics do fulfill that purpose, for the case where C may have runtime information on its own, having the freedom not to have to annotate the type of a constant is definitely something that will lead to more advanced constructs.

Let's at least have const FOO: _ = value; be allowed.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.