1. 31
  1.  

  2. 8

    OCaml’s generics also work kind of like Java’s (it’s a bit different because some primitive types that can fit in 31/63 bits are unboxed, but most aren’t), so only one version of each polymorphic function gets compiled in, and this is one of the reasons why OCaml’s compiler is fast. But there’s a proposal to add layouts, which are kind of like types for generics defining their size in memory. When you define a generic type you could also annotate it with its layout, which would give you a choice between having boxed types but smaller binary size and faster compilation, or unboxed types but bigger binary size and slower compilation.

    I don’t know if this will ever be upstreamed (I think currently Jane Street has a fork of the OCaml compiler that implements this proposal for their own internal code) but I think it’s an interesting approach to think about for language designers.

    Here’s the proposal and a talk about it:

    1. 6

      C# has generics very similar to rust (fully reified/momomorphized) and also offers value types (which Java is finally getting). In fact, C# came very close to offering what rust’s strongly typed system offers, but then gave up on covariance/contravariance once async entered the pictures. (Async has a penchant for ruining languages with smart and capable strongly typed models.)

      1. 3

        C# came very close to offering what rust’s strongly typed system offers, but then gave up on covariance/contravariance once async entered the pictures. (Async has a penchant for ruining languages with smart and capable strongly typed models.)

        Could you expand on that?

        1. 5

          You used to be able to do something like this:

          abstract class Animal { 
              public abstract Animal Clone();
          }
          
          class Penguin : Animal {
              public override Penguin Clone() { ... }
          }
          

          But you cannot do that with async, because Task<Foo> is not covariant, i.e. once Animal::Clone() inevitably (because async is viral) becomes async Task<Animal> CloneAsync(), you can no longer implement it in Penguin as public override Task<Penguin> CloneAsync() and you lose the ability to directly return a more specific type in a child implementation of a base function. In rust, you can use associated types to specify the type that an implementation of an interface (trait) function will return (or in this case, just Self), but C# doesn’t have that. The language developers completely broke an incredibly sophisticated type system with that lack of foresight, and now claim that they have no way of adding contravariance back in because it would break compatibility.

          (Yes, this is a completely contrived example and the actual cases typically involve returning a class different than the implementing class.)

          1. 1

            What compatibility would it break?

            1. 2

              Generic covariance is only available for interfaces and delegates, but Task<T> is a heap-allocated class. They had an opportunity to fix this when they added ValueTask (which is stack-allocated) by adding a covariant ITask<T> interface, but instead punted and added just the single struct.

              1. 1

                I still don’t get what working code it would break. I guess code that truly, truly relies on it being invariant?

            2. 1

              Very good point! Thanks, I hail from a community that generally had their shit together when it came to correctly sorting out variance, so it’s always surprising to me so see these kinds of spectacular failures.

        2. 5

          I feel like the important detail missing here about how Java does stuff differently is that Java is effectively “everything is a reference”(ignoring primitives), so you don’t need the type information for much of anything. Whereas rust is “everything is a value by default”, which is super unconventional in most popular languages (like, what, C/++ and Pascal? Definitely showing my ignorance here)

          “I need a box for 3 Ts” is what you do in Rust. So you need to know how big T is! Like you’re can’t get around that issue at runtime. You need to know the size of T somehow. So there’s stuff

          “I need a box for 3 pointers to T” is what Java does. Well pointers are all the same size so you’re all good, one structure will work for everything. You basically just want it for like…. type checking, at a first approximation. And yeah, maybe if you want a default instatiator for it you can add that to the structure. Not erasing would be weird!

          I feel like passing by value is the biggest source of “oh I gotta think differently here” for non-systems programmers coming to Rust

          1. 5

            Whereas rust is “everything is a value by default”, which is super unconventional in most popular languages (like, what, C/++ and Pascal? Definitely showing my ignorance here)

            My initial thought here was “What? Its weird to treat everything as a reference!” but I actually think you make a good point and I agree with you! C/++ and Rust do end up being the “weird” ones given the state of programming languages today, which is funny because passing by value used to be the conventional way.

            1. 2

              To bring this full circle, my first language was actually C++! I remember suffering a lot through arrays and references, and yeah , arguments are copied.

              To be honest I think that pass by value by default is super messy in a world of structured data. Like I have a vector, the data is managed by it, so passing the vector around should let me operate on the same data? But it doesn’t, while arrays(pointers) do… it’s really hard cuz you need to dig into implementations or docs to figure out what your data structure does on a copy.

              I get it but it’s definitely tougher than like… the Python object model