Hacker News new | past | comments | ask | show | jobs | submit login
A Mirror for Rust: Compile-Time Reflection Report (soasis.org)
98 points by todsacerdoti on May 1, 2023 | hide | past | favorite | 9 comments



Complicated, a bit reminiscent of the C++ reflection ideas. I wonder if there are some design decisions in the front end that make reflection awkward to implement. The general case of deriving code from other code implies an iterative fixpoint in the front end which might otherwise be built as a linear sequence.

Similar to the comment from duped, reflection is not a clear win. In particular, optimisation is essentially changing programs in non-observable fashion into equivalent programs.

Exposing more information about the program is in conflict with that goal. You either have to avoid changing things, which usually implies slower codegen, or do both the program transform and maintain what it used to look like to satisfy the reflection API which is confusing and error prone.


The reference to C++03 having all you need is kind of odd because C++03 did not have variadic templates. Instead, you had to use typelists to abstract over tuple-like types. But typelists are available in Rust as well. Instead of (T, U, V), you'd have to write Cons<T, Cons<U, Cons<V, Nil>>>. Then structural compile-time recursion should work, subject to the usual Rust constraints.


I take issue with the motivating example.

    pub struct Point {
      pub x: i32,
      pub y: i32,
    }
I take it that the authors would wish it to be possible for the consumers of the crate that defines this struct to also be consumers of `serde` and allow it to be serializable, even though the authors of `Point` have not marked the struct `#[derive(Serialize, Deserialize)]`.

This is not a good thing, because it means that consumers of `Point` have observable behavior not intended by the authors of `Point` and makes it possible for previously unbreaking changes to become breaking. For example, say `Point` now has state that gets cached:

    pub struct Point<'a> {
      pub x: i32,
      pub y: i32,
      cache: &'a Cache, // where cache is not serializable or observable
    }
Under the current regime this is not a breaking change because the authors do not intend `Point` to be serializable and do not want its consumers to serialize it directly. Under the design of the authors it would be possible for other code to make bad assumptions about its inputs. This is perhaps a weak example, but say `Cache` contained functions or other non-serializable data. Now the versioning of the crate containing `Point` is meaningless.

The point is: `#[derive(Serialize, Deserialize)` is a good thing: it is not boilerplate. It is annotating the contract the code has with its consumers, and adding or removing from it is potentially a breaking change.

---

Reading a bit further: you need to expand on the reason the Orphan Rule is a Problem That Has To Be Solved.

While it's true that the resulting headaches are because of a conservative choice, the reason that we need to deal with it is because without such a constraint, future code may lead to ambiguity that cannot be resolved by the compiler or by a programmer. I don't see any reasoning in this post about how reflection would mitigate the same problem:

How do you prevent reflection from creating ambiguity in type/method resolution and avoid inadvertent breaking changes in the ecosystem?

Because the Coherence problem is the actual Problem To Be Solved.

---

There are a lot of interesting applications of compile-time introspection on code, and I've had the experience of writing a lot of metaprogramming with it (and runtime introspection!).

The problem is that this is mostly an anti feature, because you do not want to have to evaluate code to understand code. It makes programs harder to reason about and fundamentally more buggy. In a language designed around explicitness and correctness like Rust, the proposal is naturally going to meet deaf ears. This belongs in a LISP dialect more than Rust, imho (as one who loves both!)


> Under the current regime this is not a breaking change because the authors do not intend `Point` to be serializable and do not want its consumers to serialize it directly.

Does your example not show a breaking change? With the old version, all fields are public, so users can construct Point values themselves, and fully deconstruct them with pattern matching. With the new version, the struct has a private field, so users cannot construct Point values directly. (Also, it has an extra lifetime parameter which users must specify!)

That is to say, the authors do not intend to allow access to private fields (they say that repeatedly), so something that wouldn't ordinarily be a breaking change would not become a breaking change in the presence of this feature. Any additional behavior defined by consumers must be built on top of the provider's contract. In this case, either the additional serialization behavior would have to require all fields to be public, or it would have to ignore any private fields. And additional deserialization behavior would similarly have to either require that all fields are public or only allow filling values into existing structs.


> Under the current regime this is not a breaking change

It is. It would not be if the original `Point` definition was annotated with the `#[non_exhaustive]` attribute.

Or actually, that still wouldn't be enough, because the new definition introduces a lifetime parameter. You could call the new definition `struct NewPoint` and introduce a `type Point = NewPoint<'static>` alias to preserve backward compatibility.


TIL. I’ve not seen this annotation on any rust crates I’ve looked at. Might have missed it of course but this feels like a niche annotation and everyone relies on semver instead.


#[non_exhaustive] didn't get stabilized for downstream use until several years after Rust 1.0, which probably is why it isn't super commonly seen in the wild still, but I think the standard library has been using it since 1.0 for things like `std::io::ErrorKind`.


> Under the current regime this is not a breaking change because the authors do not intend `Point` to be serializable and do not want its consumers to serialize it directly.

If there's a central part of Rust that will prevent it becoming as ubiquitous as C, I believe its this philosophy. It's not just a focus on "safety" but this absolutist position on some idea of correctness, which may or may not even useful.

Preventing programmers from doing things like, gosh, printing struct's because it might possibly somehow create a compile time issue. There's plenty of ways to let a programmer handle ambiguity issues, even on a per crate level, etc.

Glad to see some folks are at least considering compile time reflection. Rust for me feels like an archaic language due to its weak compile time programming. It prevents many powerful safety constructs where a programmer can add compile time checks specific to a program domain.


I think letting ' (named lifetime or loop label) into the language does not help readability at all.




Join us for AI Startup School this June 16-17 in San Francisco!

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

Search: