Live data from Hacker News

Native Reflection in Rust

jack.wrenn.fyi

21–30 of 68 posts

Re: Native Reflection in Rust

#21
post #9

Earlier quoted context omitted.

It's a fancy way of saying "every time this type is used, replace all the generic type params with what was used and generate code for it". It's how generics are implemented in Rust. If you have struct Foo (T); And you create Foo(42i32) and Foo(0.0f64), the compiler will create the equivalent to struct Fooi32(i32); struct Foof64(f64); In other languages like Java, generics are implemented the way that Rust does "trai…

Nice examples - you can also have languages (like SML) where monomorphization is simply an implementation detail. Some compilers (e.g., MLton) perform monomorphization and others don't.

I only recently realized that certain type system features, like polymorphic recursion, make monomorphization impossible in the general case. In Haskell for example, it’s by necessity only an optimization that’s used where applicable, and not the general strategy.

Re: Native Reflection in Rust

#22
post #9

Earlier quoted context omitted.

It's a fancy way of saying "every time this type is used, replace all the generic type params with what was used and generate code for it". It's how generics are implemented in Rust. If you have struct Foo (T); And you create Foo(42i32) and Foo(0.0f64), the compiler will create the equivalent to struct Fooi32(i32); struct Foof64(f64); In other languages like Java, generics are implemented the way that Rust does "trai…

Pretty sure that some usage patterns of polymorphic types can not be completely monomorphized. Here's example in Golang: package main import ( "fmt" ) type wrapper[T any] struct { Value T } func (w wrapper[T]) String() string { return fmt.Sprintf("{%v}", w.Value) } func stringWrapped[T any](n int, v T) string { if n == 0 { return fmt.Sprintf("%v", v) } return stringWrapped(n-1, wrapper[T]{Value: v}) } func main() { n…

Rust will complain about a recursion limit being reached during instantiation[1]. The solution in Rust is to use &dyn Trait or Box instead.[2]

[1]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

[2]: https://play.rust-lang.org/?version=stable&mode=debug&editio...

^ This blows the stack because it keeps calling itself with no break condition, but shows how the type system accepted the code.

Re: Native Reflection in Rust

#23
post #9

Earlier quoted context omitted.

It's a fancy way of saying "every time this type is used, replace all the generic type params with what was used and generate code for it". It's how generics are implemented in Rust. If you have struct Foo (T); And you create Foo(42i32) and Foo(0.0f64), the compiler will create the equivalent to struct Fooi32(i32); struct Foof64(f64); In other languages like Java, generics are implemented the way that Rust does "trai…

I think cpp does this too

It indeed does. The only difference is that Rust has traits (similar to C++'s concepts) which require explicit mention of what interface the type parameters have inside the function, whereas C++'s templates will have a compile error after instantiation if you passed something that didn't meet the expected contract. This is closer to Rust's macros in operation.

Given

    fn foo(a: T, b: T) -> T { a + b }
The compiler will complain that you should have been explicit on how T is going to be used:

    error[E0369]: cannot add `T` to `T`
     --> src/lib.rs:1:32
      |
    1 | fn foo(a: T, b: T) -> T { a + b }
      |                              - ^ - T
      |                              |
      |                              T
      |
    help: consider restricting type parameter `T`
      |
    1 | fn foo>(a: T, b: T) -> T { a + b }
      |         +++++++++++++++++
whereas in C++ this would have been accepted until you called foo with two things that couldn't be added together, like a Rust macro[1].

[1]: https://play.rust-lang.org/?version=nightly&mode=debug&editi...

Re: Native Reflection in Rust

#24
post #16

Earlier quoted context omitted.

To add to this, even the Foo-wrapper is gone, just the i32 remains. Rust values are amorphous data blobs at runtime.

Yes, that's true but that is an implementation detail that only comes into play when dealing with ABI, and then you should be using #[repr(transparent)] to ensure that the compiler won't do something else :)

Sure, it’s good to point out the difference between “the behavior of a typical optimizing compiler” and “things actually guaranteed by the language”. The context of the discussion was the former, I think. I’m not even that certain that monomorphization is actually required in theory.

Re: Native Reflection in Rust

#25
post #9

Earlier quoted context omitted.

What are monomorphic data types? What should be my first read on the subject?

It's a fancy way of saying "every time this type is used, replace all the generic type params with what was used and generate code for it". It's how generics are implemented in Rust. If you have struct Foo (T); And you create Foo(42i32) and Foo(0.0f64), the compiler will create the equivalent to struct Fooi32(i32); struct Foof64(f64); In other languages like Java, generics are implemented the way that Rust does "trai…

Not exactly the same thing but JITs can turn dynamic objects into structs if the structure is consistent. JS runtimes and Julia do this as far as I know.

Re: Native Reflection in Rust

#26
post #24

Earlier quoted context omitted.

Yes, that's true but that is an implementation detail that only comes into play when dealing with ABI, and then you should be using #[repr(transparent)] to ensure that the compiler won't do something else :)

Sure, it’s good to point out the difference between “the behavior of a typical optimizing compiler” and “things actually guaranteed by the language”. The context of the discussion was the former, I think. I’m not even that certain that monomorphization is actually required in theory.

Yes, monomorphization isn't needed in theory, as long as the user-visible behavior remains the same, and in practice the team is exploring options[1] to identify cases where the currently manual practice of writing

    pub fn foo>(x: T) {
        inner_foo(x.as_ref());
    }
    fn inner_foo(_: &X) { todo!() }
can be instead done by the compiler automatically (turning monomorphized code back into polymorphic code, hence the polimorphization hame).

[1]: https://rustc-dev-guide.rust-lang.org/backend/monomorph.html...

Re: Native Reflection in Rust

#28
post #14
post #8

Earlier quoted context omitted.

Except Rust has runtime: [0]. And so, usually, does C (in hosted implementations). [0] https://doc.rust-lang.org/reference/runtime.html

These are a couple of functions executables can call at run time, but they're more like an extra standard library. It's not a runtime in the same sense as a runtime in dynamic or GC languages that manages all objects and is able to know types of arbitrary objects and inspect/trace them. Rust has no run-time type information except limited downcasts via `dyn Any` or explicitly derived traits on per-type basis, and the…

Pretty sure you don’t need a runtime to track runtime type info. What we think of as a “runtime” in GC languages is usually several distinct things (a scheduler, a GC, and maybe some other stuff in the case of Java/.Net).

Re: Native Reflection in Rust

#29
post #16
post #9

Earlier quoted context omitted.

It's a fancy way of saying "every time this type is used, replace all the generic type params with what was used and generate code for it". It's how generics are implemented in Rust. If you have struct Foo (T); And you create Foo(42i32) and Foo(0.0f64), the compiler will create the equivalent to struct Fooi32(i32); struct Foof64(f64); In other languages like Java, generics are implemented the way that Rust does "trai…

To add to this, even the Foo-wrapper is gone, just the i32 remains. Rust values are amorphous data blobs at runtime.

ABI wise that is not true though. structs have struct ABI, even just a newtype struct around an integer will not use integer ABI unless annotated with #[repr(transparent)].

Re: Native Reflection in Rust

#30
"When you call .reflect on a dyn Reflect value, deflect figures out its concrete type in four steps:"

* invokes local_type_id to get the memory address of your value’s static implementation of local_type_id

* maps that memory address to an offset in your application’s binary

* searches your application’s debug info for the entry describing the function at that offset

* parses that debugging information entry (DIE) to determine the type of local_type_id’s &self parameter.

This is a rather strange thing to bolt onto a language. I could see this as an external tool. The use case seems to be programs which used "async" so much they can't figure out the resulting state machine. External debug tools to view and examine the async state machine might be helpful.

My experience with Rust has been that debugging of safe code is just not a problem. Print statements and logging are enough.

Post reply on HN