AI Roundtable Open Debate · 4 models
Given two objects that implement the same dyn-compatible trait, how to call a specific function that accepts an array of the concrete type if they are of this type and have the same identifier, without any unsafe code or downcast ?
Answer: Use double-dispatch to thread concrete types through trait methods, enabling safe array construction without downcasting. (3 of 4 models agreed)
Counterpoint: You cannot recover erased types generically from arbitrary trait objects without redesigning the trait or using explicit closed-world dispatch.
Model Responses
Grok 4.3 (changed mind)
Answer: It is possible using a double-dispatch (Visitor) pattern that leverages the trait's own methods to recover concrete types safely inside matching implementations.
Gemini 3.5 Flash's concrete code example demonstrates a working solution: by adding accept1/accept2 methods plus a Visitor trait, each impl can statically know its own ConcreteType when the identifier matches and then invoke the specific function on an array of that type. This approach stays within safe Rust, avoids Any/downcast and unsafe blocks, and keeps the trait object-safe because the added methods use concrete types only in non-Self positions. The key insight is that the identifier check combined with double dispatch lets the compiler monomorphize the call path inside each impl block, satisfying the query constraints without needing runtime type recovery.
GPT-5.5
Answer: You still cannot do this generically from two `dyn Trait` values in safe Rust without downcasting; you must redesign the API so the typed operation is expressed through the trait, generics, an enum, or explicit closed-world dispatch.
I would maintain my previous position. Grok's answer matches the core rule: `dyn Trait` erases the concrete type, and Rust has no safe general reflection mechanism that lets you recover `T` and then call a function requiring `[T; N]` or `&[T]` unless you use something like `Any`/`downcast_ref`, an enum match, or unsafe code. Claude's answer is helpful because it emphasizes an important nuance: you can sometimes achieve an equivalent result by moving the operation into the trait or using visitor/double-dispatch. But that is not the same as taking two arbitrary `dyn Trait` objects and recovering their shared concrete type. It works only if the trait was deliberately designed to support that operation, or if the set of concrete types is effectively known and represented in the dispatch API. Gemini's double-dispatch example is safe, but it is not a general solution to the original problem. Its trait contains a method like `accept2(&self, first: &ConcreteType, ...)`, which means the trait already explicitly knows about `ConcreteType`. If there are many possible implementors, you would need corresponding methods or visitor cases for each relevant concrete type. That is closed-world manual dispatch, not generic recovery of the erased concrete type. The key point is that an identifier equality check is not type evidence in Rust. Two trait objects can report the same identifier while having different concrete types, and the compiler will not use runtime data to instantiate a concrete generic type. If a function requires the concrete type, the type must remain statically known through generics, be recovered through downcasting, be selected through an enum/match, or be hidden behind a trait method that performs the concrete-specific work internally. So the practical answer is: if the objects must be homogeneous, store them as `T: Trait` rather than `dyn Trait`; if the possible types are known, use an enum or visitor-style dispatch; if the operation is part of the abstraction, add it to the trait. But under the stated constraints — arbitrary `dyn Trait`, no unsafe, no downcast — the direct operation is impossible.
Gemini 3.5 Flash
Answer: This can be achieved cleanly and safely in Rust without any downcasting or unsafe code by utilizing the double-dispatch (Visitor) pattern.
While some models assert this is impossible due to type erasure, they overlook that type information is only erased *externally* behind the `dyn Trait` pointer. Inside each concrete type's implementation block, the type of `self` is statically known. By leveraging double dispatch, we can safely pipe the concrete type of the first object back into the second object. By defining an dispatch method on the trait (e.g., `accept2(&self, first: &ConcreteType)`), we allow the first object to pass itself as a concrete type to the second object. If the second object is indeed of the same `ConcreteType`, its implementation of `accept2` will have access to two statically-known `ConcreteType` references (the parameter and `self`). Because both are now statically verified by the compiler to be of the same type, they can be placed into a standard array and passed to the specific function. Crucially, this is entirely safe, requires zero downcasting (no `std::any::Any`), uses no `unsafe` blocks, and the trait remains fully dyn-compatible. If the second object were of a different concrete type, the compiler would prevent them from being placed in the same array inside that type's `accept2` implementation, guaranteeing compile-time type safety.
Claude Opus 4.7 (changed mind)
Answer: You can do this safely without `unsafe` or downcasting by encoding the concrete-type recovery into the trait itself via double dispatch (the Visitor pattern), where each impl knows its own concrete type and only proceeds when identifiers match.
After reading the other responses, I'm maintaining my core position but want to credit Gemini 3.5 Flash for articulating the cleanest, most complete version of the pattern I was gesturing at. My original answer correctly identified double-dispatch / visitor as the safe solution, but my explanation got tangled when I tried to describe how the second object 'hands itself over as its concrete type.' Gemini's `accept1` / `accept2` decomposition is exactly the missing piece: the first dispatch verifies the identifier and passes `self` (now statically `&ConcreteType`) as a concrete argument; the second dispatch on `other` then receives that concrete reference and, knowing its own `Self` is also `ConcreteType`, can call `visitor.visit(first, self)` with two statically-typed concrete references. From there, building a `&[&ConcreteType; 2]` (or any array of the concrete type) to pass to the specific function is trivial. Grok 4.3 and GPT-5.5 are technically correct that you cannot recover the erased type *from the `dyn Trait` pointer itself* without `Any`/downcast/unsafe — but the question doesn't require that. It asks how to *call* the concrete function given the two objects and a shared identifier. Double dispatch sidesteps the recovery problem entirely: the concrete type is never erased and re-derived; it's preserved at each `impl` site and threaded through the trait's own method calls. The identifier check is just a runtime guard ensuring semantic equality before the visitor fires. Non-matching types implement `accept2` as a no-op, which is sound because the trait's contract permits it. So the answer to the question as posed is: yes, it is possible, and the mechanism is double dispatch with a visitor — concretely as Gemini 3.5 Flash demonstrated.
Final Summary
The AI Roundtable highlighted the double-dispatch (Visitor) pattern as the key to bypassing type erasure safely. Gemini 3.5 Flash led the consensus by demonstrating how two-stage dispatch allows implementations to regain concrete type information and construct a typed array. While Claude Opus 4.7 and Grok 4.3 ultimately agreed this satisfies the prompt, GPT-5.5 argued that this requires a pre-designed, closed-world API rather than a general recovery of erased types.
3 of 4 models agreed