Comment by pkolaczk

3 years ago

> A libraries next version which switches up some internal representations memory handling should ideally not mess up your application

It doesn't have to because internal memory representation can and should be abstracted out, and Rust gives a plethora of tools to do that.

Your argument works against against static typing in general. The next version changes the address representation from String to Address (in managed language) and messes up your app. That's the same thing.

If version 1 of your library has a function that returns a reference to T, then that constrains your choices of implementation for the underlying data structures containing Ts more than they would be constrained in a garbage-collected language. So unless there is to be a ban on functions returning simple references, Rust is always going to be a little less flexible in this respect. That is fine and expected given the overall design goals of the language, but there's no point pretending that it's not the case.

The simplest concrete example, I guess, would be a function that returns &'str. There are plenty of Rust APIs out that have functions with this type signature.

  • > If version 1 of your library has a function that returns a reference to T, then that constrains your choices of implementation for the underlying data structures containing Ts more than they would be constrained in a garbage-collected language

    This is IMHO a good thing. If it returns a reference to T, it means it still owns it and allows only temporary usage of it. This is semantics of ownership and does not have anything to do with memory management. If you wanted to allow sharing for unspecified lifetime, sure, you can. There is Rc/Arc.

    I've fixed plenty of bugs in code written in managed languages, where a reference to T was handed out from a library (because there is no other choice - everything is a reference) and then someone stored it for longer than it was valid, leading to a logical equivalent of use-after-free.

    E.g. get an entity object managed by Hibernate. Pass it up outside of the context of Hibernate session. It will likely blow up because the object references a session that's now closed. Rust ownership model would prevent exactly that problem.

    I find this "flexibility" of managed languages actually a problem in large codebases, similarly how flexibility of goto is universally considered bad. It severely hinders maintainablity. It allows to pass references freely and create implicit, complex, often cyclic, reference graphs which are very hard to reason about.

    In my Rust code 99% of objects don't need shared ownership. But managed languages make shared ownership the default, optimizing for the edge-case.

    BTW, your statement can be rephrased to: "If version 1 of your library has a function that accepts a reference to T, then that constrains your choices of values of T more than it would be constrained in a dynamically typed language."

    You may say that you can use Any / Variant / Object in a statically typed language to overcome that limitation. True, and similarly you can use Rc/Arc/Copy types in Rust.

    This is all the same thing. It just takes static typing to the next level. Not only it allows to express constraints on values, but it also allows to express constraints on time they can be used.

    • > This is a good thing.

      This is dogmatic. In can be a good thing sometimes, in some domains. In other instances it can just be a pain.

      Let's say I'm using a 'names' library that provides the following utility function:

          first_name<'a>(name: &'a str) -> &'a str
      

      My code happily uses this function (which we can assume is not performance critical). At some point, the author of the library notices that there are cultures where the nearest analogue of a 'first name' is not always a contiguous substring of the full name. To fix this bug they must change the function's type. They may choose e.g.:

          first_name<'a>(name: &'a str) -> Cow<'a, str>
      

      I'd like to update my version of the library to get the bug fix, but can't do this for free. I have to update the calling code, and possibly even change some of my own internal data representations. This contrasts with pretty much any GCed language. For example, in Go the type of the function would just be

          func firstName(name string) string
      

      and the bug fix would require no change in the API.

      Now let's relate this example back to what you originally said in response to kaba0:

      >> [kaba0:] A libraries next version which switches up some internal representations memory handling should ideally not mess up your application

      > It doesn't have to because internal memory representation can and should be abstracted out, and Rust gives a plethora of tools to do that.

      The above is a simple example of why this is not true. Any time you write a function that returns a reference, you are limiting the changes you can make to internal representations (both in the library itself and the calling code) without making a breaking API change.

      Please don't respond by saying "this API was badly designed in the first place!" Most languages don't give you the opportunity to design APIs badly in this particular way. If all APIs were perfectly designed on day one then of course we'd never have to worry about API changes.

      Again, none of this is to bash Rust. I just think it is important to be realistic about the downsides as well as the upsides of Rust's ownership system.

      6 replies →

> Your argument works against against static typing in general

I don’t think this argument works. Ad absurdum a very strong type system would even specify the implementation itself, making any change breaking — is that good? No, it isn’t as the useful property of the type system is no longer there. I don’t agree that this usefulness line is behind “ownership annotations” — that’s what you would have to convince me of.