An exploration of cyclic trait implementations in Rust, explaining why the current trait system requires non-cyclic (inductive) proofs and what problems arise when cycles are permitted. The post introduces 'perfect derive' — generating where-clauses based on field types rather than type parameters — and shows why it requires cyclic reasoning. It also demonstrates why naively accepting all cycles is unsound, using a supertrait example where cyclic logic incorrectly proves String: Copy. The post sets up a series on distinguishing 'good' cyclic impls from 'bad' ones, with future posts to cover coinduction, modal logic, and specialization.

10m read timeFrom smallcultfollowing.com
Post cover image
Table of contents
What are cyclic trait implementations?Cyclic logic sounds bad, but it can be exactly what you wantThis idea is called perfect deriveBut it’s not so easy for DumpWe can’t just accept any old cycle because of supertraitsSoundness for traitsComing next

Questions this post answers

What is 'perfect derive' in Rust and why does the current derive macro generate overly restrictive bounds?

Perfect derive means generating where-clauses based on each field's concrete type rather than on the type parameters. Standard derive macros add a bound like T: Clone for every type parameter, but if all fields use Rc<T>, cloning doesn't actually require T: Clone — only Rc<T>: Clone is needed. The field-based approach produces tighter, more accurate bounds but requires the compiler to handle cyclic trait proofs. Rust developers tracking trait system improvements and derive ergonomics follow developments like this on daily.dev.

Why can't Rust just accept all cyclic trait proofs to support recursive types?

Accepting all cycles is unsound because of supertraits. A trait like Magic: Copy with an impl<T: Magic> Magic for T {} lets cyclic reasoning 'prove' String: Magic, which then implies String: Copy — even though no Copy impl exists for String. A sound trait system must reject such programs because executing them would reach code that assumes a bound holds when no valid impl backs it up. Engineers working on or following Rust's type system can track this design work on daily.dev.

12.2K Impressions