Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> For the disjoint field issues raised, it’s not that the borrow checker can’t “reason across functions,” it’s that the field borrows are done through getter functions which themselves borrow the whole struct mutably

Right, and even more to the point, there's another important property of Rust at play here: a function's signature should be the only thing necessary to typecheck the program; changes in the body of a function should not cause a caller to fail. This is why you can't infer types in function signatures and a variety of other restrictions.



Exactly. We've talked about fixing this, but doing so without breaking this encapsulation would require being able to declare something like (syntax is illustrative only) `&mut [set1] self` and `&mut [set2] self`, where `set1` and `set2` are defined as non-overlapping sets of fields in the definition of the type. (A type with private fields could declare semantic non-overlapping subsets without actually exposing which fields those subsets consist of.)


You could it in a more limited fashion by allowing fields of a struct to be declared "const", which would have similar semantics to Java's final. If you add the ability to return const references, you get the ability to have read only and mutable references to stuff within a struct co-exist.

For example, this won't compile:

    struct Something { z: usize }
    struct Foo<'a> { x: usize, y: &'a Something }
    impl<'a> Foo<'a> {
        fn bar(&mut self) -> &Something
        { let something = self.bar(); self.x += something.z; something }
    }
But if you could tell the borrow checker the mutable borrow of self can never modify z, then it would be safe. This would achieve that:

    struct Something { z: usize }
    struct Foo<'a> { x: usize, y: &'a const Something }
    impl<'a> Foo<'a> {
        fn bar(&mut self) -> &const Something
        { let something = self.bar(); self.x += something.z; something }
    }
I've now had several instances where they would have let me win a battle with the borrow checker succinctly rather than the long work around I was forced to adopt. Const struct members allow you implement read only fields with having to hide them, and provide getters is icing on the cake.



This seems to be a golden rule of many languages? `return 3` in a function with a signature that says it's going to return a string is going to fail in a lot of places, especially once you exclude bolted-on-after-the-fact type hinting like what Python has.

It's easier to "abuse" in some languages with casts, and of course borrow checking is not common, but it also seems like just "typed function signatures 101".

Are there common exceptions to this out there, where you can call something that says it takes or returns one type but get back or send something entirely different?


Many functional and ML-based languages, such as Haskell, OCaml, F#, etc. allow the signature of a function to be inferred, and so a change in the implementation of a function can change the signature.


In C++, the signature of a function template doesn't necessarily tell you what types you can successfully call it with, nor what the return type is.

Much analysis is delayed until all templates are instantiated, with famously terrible consequences for error messages, compile times, and tools like IDEs and linters.

By contrast, rust's monomorphization achieves many of the same goals, but is less of a headache to use because once the signature is satisfied, codegen isn't allowed to fail.


> In C++, the signature of a function template doesn't necessarily tell you what types you can successfully call it with, nor what the return type is.

That's the whole point of Concepts, though.


Concepts are basically a half solution - they check that a type has some set of properties, but they don't check that the implementation only uses those properties. As a result, even with concepts you can't know what types will work in a template without looking at the implementation as well.

Example [0]:

    #include <concepts>

    template<typename T>
    concept fooable = requires(T t) {
        { t.foo() } -> std::same_as<int>;
    };

    struct only_foo {
        int foo();
    };

    struct foo_and_bar {
        int foo();
        int bar();
    };

    template<fooable T>
    int do_foo_bar(T t) {
        t.bar(); // Compiles despite fooable not specifying the presence of bar()
        return t.foo();
    }

    // Succeeds despite fooable only requiring foo()
    template int do_foo_bar<foo_and_bar>(foo_and_bar t);

    // Fails even though only_foo satisfies fooable
    template int do_foo_bar<only_foo>(only_foo t);
[0]: https://cpp.godbolt.org/z/jh6vMnajj


> they check that a type has some set of properties, but they don't check that the implementation only uses those properties.

I'd say that's a mistake of the person who wrote the template then.

Also, there are Concepts where you absolutely know which types are allowed, e.g. std::same_as, std::integral, std::floating_point, etc.


> I'd say that's a mistake of the person who wrote the template then.

The fact that it's possible to make that mistake is basically the point! If "the whole point of concepts" were to "tell you what types you can successfully call it with" then that kind of mistake should not be possible.

It's true that there are certain cases where you know the full set of types you can use, but I'd argue that those are the less interesting/useful cases, anyways.


My interpretation of the post is that the rule is deeper than that. This is the most important part:

> Here is the most famous implication of this rule: Rust does not infer function signatures. If it did, changing the body of the function would change its signature. While this is convenient in the small, it has massive ramifications.

Many languages violate this. As another commenter mentioned, C++ templates are one example. Rust even violates it a little - lifetime variance is inferred, not explicitly stated.


Lifetimes for a function signature in Rust are never inferred from the function code. Rather Rust has implicit lifetime specs with straightforward rules to recover the explicit full signature.


I was speaking about variance specifically. They are not inferred from function bodies, but I think it's fair to say that it's a soft violation of the golden rule because variance has a lot of spooky action at a distance (changing a struct definition can change its variance requirements, which then has ripple effects over all type signatures that mention that struct)


> Are there common exceptions to this out there, where you can call something that says it takes or returns one type but get back or send something entirely different?

I would personally consider null in Java to be an exception to this.


There are languages with full inference that break this rule.

Moreover, this rule is more important for Rust than other languages because Rust makes a lot of constraints visible in function signatures.

But the most important purpose of the rule is communicating that this is a deliberate design decision and a desireable property of code. Unfortunately, there's an overwhelming lack of taste and knowledge when it comes to language design, often coming from the more academic types. The prevailing tasteless idea is that "more is better" and therefore "more type inference is better", so surely full type inference is just better than the "limited" inference Rust does! Bleh.




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

Search: