Research: From/Into Conversion Trait Framework
Survey of type conversion trait designs, with Rust's From/Into as the central case study.
Background for a potential Wado WEP on structured type conversions.
Motivation
Wado currently supports:
ascasts — explicit primitive-to-primitive and newtype conversions- Literal coercion — numeric/string literals adapt to the target type context
null→Option<T>coercion- Builder traits —
SequenceLiteralBuilder,KeyValueLiteralBuilderfor collection literals FromIterator— collecting iterators into collections
There is no general-purpose mechanism for converting between arbitrary user-defined types. A structured conversion framework would enable:
- Ergonomic APIs that accept related types (e.g.,
Stringwhere&strsuffices) - Error type composition (prerequisite for the
?operator) - Newtype interop beyond
ascasts - Standardized patterns for library authors
Rust — From / Into / TryFrom / TryInto
Design
Rust's conversion framework (RFC 529, stabilized in Rust 1.0) consists of four traits in
std::convert:
trait From<T> {
fn from(value: T) -> Self;
}
trait Into<T> {
fn into(self) -> T;
}
trait TryFrom<T> {
type Error;
fn try_from(value: T) -> Result<Self, Self::Error>;
}
trait TryInto<T> {
type Error;
fn try_into(self) -> Result<T, Self::Error>;
}
Key relationships:
- Blanket impl:
impl<T, U> Into<U> for T where U: From<T>— implementingFromautomatically providesInto. - Reflexive impl:
impl<T> From<T> for T— every type can convert from itself. This enablesfn new(s: impl Into<String>)to accept bothStringand&str. - Try variants (stabilized in Rust 1.34): fallible conversions returning
Result.
In addition, Rust has borrowing conversion traits:
AsRef<T>: cheap reference-to-reference conversion (e.g.,String→&str)AsMut<T>: mutable reference-to-reference conversionBorrow<T>: likeAsRefbut with a hash/eq consistency contract
Usage Patterns
Pattern 1: Flexible function parameters
fn greet(name: impl Into<String>) {
let name: String = name.into();
println!("Hello, {}!", name);
}
greet("world"); // &str → String via From
greet(String::from("world")); // String → String via reflexive From
Pattern 2: Error type composition with ?
impl From<io::Error> for AppError { ... }
impl From<ParseIntError> for AppError { ... }
fn process() -> Result<(), AppError> {
let data = std::fs::read_to_string("file.txt")?; // io::Error → AppError
let n: i32 = data.trim().parse()?; // ParseIntError → AppError
Ok(())
}
The ? operator calls From::from() to convert the error type, making From the backbone
of Rust's error handling ergonomics.
Pattern 3: Newtype wrapping
struct UserId(u64);
impl From<u64> for UserId {
fn from(id: u64) -> Self { UserId(id) }
}
let id: UserId = 42u64.into();
Pros
-
Standardization: all Rust libraries agree on a single conversion protocol. No ad hoc
from_foo()methods proliferating across crates. -
Composability with
?:Fromimpls compose with the?operator, enabling clean error propagation without manual matching. -
Generic API ergonomics:
impl Into<T>as a parameter bound lets functions accept multiple input types while remaining type-safe. -
Zero-cost abstraction: monomorphized at compile time — no dynamic dispatch overhead.
-
Discoverability: a well-known trait that IDE tooling can index. "What can I convert this type to/from?" becomes answerable via trait impls.
-
Separation of fallible/infallible:
Fromguarantees infallible conversion, whileTryFrommakes fallibility explicit in the type system.
Cons
-
Type inference failures with
.into()The target type of
Intois on the trait (Into<T>), not the method. Turbofish cannot be used:.into::<String>()is a compile error. The caller must provide type context through variable annotations or other means.// Fails: cannot infer type let x = some_value.into(); // Must write: let x: String = some_value.into(); // Or use From directly: let x = String::from(some_value);This asymmetry is a frequent source of confusion.
From::from()is often preferred for readability precisely because the target type is visible at the call site. -
Readability and code navigation
.into()hides the target type, making it harder to understand code without IDE support. "Go to definition" on.into()leads to the blanket impl, not the actual conversion logic. This is a well-known complaint in medium-to-large Rust codebases. -
Hidden performance costs
The compiler has no knowledge of the performance characteristics of a given
impl From. It might require allocation, syscalls, or I/O. Making conversions syntactically cheap (.into()) can mask expensive operations. -
Orphan rule complications
Before Rust 1.41, if neither the source nor target type was local, you couldn't implement
From. This forced some users to implementIntodirectly, which doesn't provide the reverseFromimpl — creating an asymmetric situation. The orphan rules still limit cross-crate conversion definitions. -
Trait family confusion
Users must choose between
From/Into,AsRef/AsMut,Borrow,Deref, andToOwned. The distinctions are subtle:Trait Semantics When to use From<T>Owned → owned (consuming) Type construction AsRef<T>Borrowed → borrowed (cheap) Read-only access Borrow<T>Like AsRef + hash/eq contract HashMap keys DerefSmart pointer transparency Wrapper types ToOwnedBorrowed → owned (cloning) &str→StringThis proliferation is a common complaint. New Rust users struggle to know which trait to implement for their conversion.
-
Not dyn-compatible (not object-safe)
Fromcannot be used as a trait object (dyn From<T>), limiting its use in dynamic dispatch scenarios. -
Effect system limitations
From/Intocannot be made async or fallible (beyondTryFrom). In an effect-generic future, you'd need separateAsyncFrom,StreamFrom, etc. — an exponential explosion of trait variants. -
Semantic guarantees are conventions only
The documentation states conversions should be "infallible," "value-preserving," and "obvious" — but the compiler cannot enforce these. Misuse (e.g., lossy conversions via
From) is possible and occurs in practice. -
Monomorphization / compile-time cost
Using
impl Into<T>as a function parameter causes monomorphization — each unique caller type generates a new copy of the function body. Themomocrate exists specifically to mitigate this by splittingimpl Into<T>functions into a thin generic wrapper + non-generic inner function. The standard library itself uses this "inner function" pattern. -
Error handling boilerplate
Writing
impl From<IoError> for MyError,impl From<ParseError> for MyError, etc. for every error type is tedious. This is why crates likethiserrorandanyhowexist — they automate what should arguably be a language-level solution. The coherence rules compound this: you cannot write a blanketimpl<E: Error> From<E> for MyErrorbecause it conflicts with the reflexiveimpl<T> From<T> for Tin std. -
TryFrom/FromStrdesign wartFromStrpredatesTryFromand serves the same purpose asTryFrom<&str>. Both exist in the standard library with no clear migration path. This illustrates the risk of introducing conversion traits incrementally without a unified design. -
IDE support degrades with
.into()rust-analyzer cannot determine the resulting type after
.into()in some contexts, breaking autocompletion. "Go to definition" navigates to the blanket impl in std, not the actualFromimplementation.
Community Opinions
The Rust community is broadly positive about From/Into as a framework, but has specific
recurring complaints:
-
Reddit/forums: ".into() is write-only code" is a common refrain. Experienced users often prefer
Type::from(x)for clarity. -
"What is wrong with auto .into?" (internals.rust-lang.org, 2022): A highly-discussed thread proposing automatic
.into()calls. Key arguments from participants:- ekuber: "The compiler has no knowledge on the performance characteristics of a given
impl From. It might require allocation, it might require invoking syscalls, it might even require IO." - afetisov: Implicit conversions would break generic functions —
opt.unwrap_or_else(|| 3)would become ambiguous if3could auto-convert to multiple types. - Multiple participants: cite C++ and Scala as warnings about implicit conversion pitfalls.
- Even the author of the
auto_intoproc-macro crate concluded it was "an anti-pattern."
- ekuber: "The compiler has no knowledge on the performance characteristics of a given
-
"Implicit into() on return" (internals.rust-lang.org, 2022): Proposed that return statements automatically apply
.into()when types mismatch. Key opposition:- SkiFire13: "A hidden function call makes readability worse because now you have to ask yourself if there's some kind of hidden conversion going on."
- scottmcm: Would be a breaking change —
fn foo() -> u8 { 0 }would no longer compile because0could.into()multiple types. - mjbshaw: Some conversions like
&[T]toArc<[T]>"require cloning all the elements" — hiding that is dangerous.
-
The
?operator inconsistency: The?operator already performs an implicitFrom::from()on errors. If Rust already hidesFromconversions inside?, why not elsewhere? The counterargument:?is visually explicit (you see the?character) and confined to error propagation, a well-understood pattern. -
Effective Rust (David Drysdale): Recommends preferring
From/Intooverascasts, andTryFrom/TryIntoover potentially lossyas. Notes the "inner function" pattern to avoid generic code bloat. -
Community sentiment summary: Pragmatists want less boilerplate (auto-
.into(), operators, implicit conversions in limited contexts). Purists value explicitness (hidden conversions hide performance costs, break type inference). The Rust language team has consistently sided with explicitness, with?as the sole exception.
Scala — Implicit Conversions → given/using (Scala 3)
Design
Scala 2 had implicit def for automatic type conversions:
implicit def intToString(x: Int): String = x.toString
val s: String = 42 // compiler inserts intToString(42)
Scala 3 replaced this with the Conversion type class and given/using keywords:
given Conversion[Int, String] = _.toString
What Went Wrong
Scala's implicit conversions are widely considered the language's biggest design mistake.
Martin Odersky himself called them "evil." The implicit keyword was overloaded for three
distinct features (arguments, conversions, extensions), confusing beginners and experts alike:
-
Invisible behavior: Conversions happen silently with no syntactic marker. Reading code reveals no hint that a conversion is occurring.
-
Non-total conversions: Nothing prevents defining a conversion from
StringtoIntthat throwsNumberFormatExceptionat runtime. The type system says it's safe; it isn't. -
Import-triggered surprises: Importing a module can silently change the behavior of existing code by bringing new implicit conversions into scope.
-
Poor error messages: When implicit resolution fails, the compiler reports generic type mismatches rather than explaining which conversion was expected.
-
IDE masking: IDEs automatically suppress compiler warnings about implicit conversions, undermining the language's own safety mechanisms.
-
Debugging difficulty: In large codebases, tracking which implicit conversion is being applied at a given call site requires deep understanding of the implicit scope rules.
Lessons for Wado
- Implicit conversions without syntactic markers are harmful. Scala 3 effectively admits this by deprecating the old approach.
- Conversions should be explicitly requested at the call site — either through
.into(),as, or at minimum through a visible trait bound in the function signature. - Non-total conversions need a separate type (like Rust's
TryFromreturningResult).
Swift — ExpressibleBy Protocols
Design
Swift uses protocol conformance for literal-to-type conversion:
protocol ExpressibleByStringLiteral {
init(stringLiteral value: String)
}
struct UserID: ExpressibleByStringLiteral {
let raw: String
init(stringLiteral value: String) { self.raw = value }
}
let id: UserID = "abc123" // compiler calls init(stringLiteral:)
For non-literal conversions, Swift relies on explicit initializers:
let x = Double(42) // explicit, not implicit
let s = String(describing: someValue)
Strengths
- Literals are ergonomic: Custom types feel native when they can be expressed as literals.
- Non-literal conversions are explicit: No hidden
.into()— you call an initializer. - Progressive disclosure: Simple code stays simple; conversion machinery only appears when needed.
Weaknesses
- Limited scope: Only works for literals, not arbitrary value-to-value conversion.
- No general conversion trait: Swift has no equivalent of
From/Intofor non-literal contexts.
Relevance to Wado
Wado's existing SequenceLiteralBuilder and KeyValueLiteralBuilder are conceptually similar
to Swift's ExpressibleBy protocols — they handle literal-to-type conversion. A From/Into
framework would complement this by handling non-literal conversions.
C++ — Implicit Conversion Operators
Design
C++ allows implicit conversions through converting constructors and conversion operators:
class MyString {
MyString(const char* s); // implicit converting constructor
operator int() const; // implicit conversion to int
explicit operator bool() const; // explicit only
};
Problems (Well-documented)
- Silent, surprising conversions:
MyString s = "hello"; int n = s;compiles silently. - Ambiguity in overload resolution: Multiple implicit conversion paths create compilation errors or, worse, select the wrong overload.
- The
explicitkeyword was added precisely because implicit conversions caused too many bugs. Modern C++ style guidelines recommend making all single-argument constructorsexplicitby default. - The Safe Bool Problem:
operator bool()allows implicit conversion tointvia standard promotion, enabling nonsensical code likeobj << 1orint n = myStream. Before C++11, the workaroundoperator void*()alloweddelete std::coutto compile. - Performance: Temporary objects created by implicit conversions are a hidden cost.
Google's C++ style guide and the C++ Core Guidelines forbid implicit conversions by default.
Lessons for Wado
C++ is the strongest cautionary tale: implicit conversions without opt-in at the call site
are a proven source of bugs. The explicit keyword was a retroactive fix for a design
mistake.
Go — Explicit Conversions Only
Design
Go has no implicit conversions whatsoever. Even int32 → int64 requires an explicit cast:
var x int32 = 42
var y int64 = int64(x) // must be explicit
Rationale
Go's designers explicitly chose this to avoid the "complexity and confusion" that implicit conversions cause in C/C++. The FAQ states: "The convenience of automatic conversion between numeric types in C is outweighed by the confusion it causes."
Strengths
- No surprises: Code does exactly what it says.
- Simple mental model: No conversion rules to memorize.
Weaknesses
- Verbose: Numeric code becomes cluttered with casts.
- No generic conversion protocol: No way to express "any type convertible to X" as a bound.
Haskell — Type Classes
Design
Haskell uses type classes for conversion, but there is no single unified From/Into:
class Num a where
fromInteger :: Integer -> a
class Real a => Integral a where ...
fromIntegral :: (Integral a, Num b) => a -> b
Conversions are always explicit function calls, but polymorphic literals (like 42) are
implicitly converted via fromInteger.
Strengths
- Polymorphic literals:
42works as anyNumtype without explicit casts. - Type class coherence: Global uniqueness of instances prevents ambiguity.
Weaknesses
- No unified conversion trait:
fromIntegral,toInteger,fromRationalare all separate functions with no shared structure. - Partial functions:
fromIntegralcan overflow silently (noTryFromequivalent in base).
Python — Dunder Methods
Design
Python uses special methods for type conversion:
class Celsius:
def __init__(self, value): self.value = value
def __float__(self): return self.value
def __int__(self): return int(self.value)
def __str__(self): return f"{self.value}°C"
Conversions are explicit at the call site: float(celsius), int(celsius), str(celsius).
Strengths
- Explicit call sites:
float(x)is clear about what's happening. - Standardized protocol: Well-known dunder methods that all Python developers understand.
Weaknesses
- No static type checking: Wrong dunder method signatures are discovered at runtime.
- Limited to built-in types: No way to define
__mytype__for custom-to-custom conversion.
Zig — Separated Conversion Builtins
Design
Zig uses distinct builtins for different conversion kinds:
@as(T, x)— safe, lossless coercion (e.g.,u8→u32)@truncate(x)— explicit bit-dropping (narrowing with silent data loss)@intCast(x)— checked narrowing (panics on overflow in safe builds)
const x: u32 = @as(u32, some_u16); // safe widening
const y: u8 = @truncate(some_u32); // explicit: drops high bits
const z: u8 = @intCast(some_u32); // checked: panics if > 255
Strengths
- Each conversion kind has its own named builtin — code is greppable and auditable.
@asonly works for guaranteed-safe coercions; unsafe casts require a different builtin.- Forces the programmer to choose explicitly: silent data loss vs. checked conversion.
- Follows Zig's "no hidden control flow" and "favor reading over writing" principles.
Weaknesses
- Verbose for arithmetic mixing different integer sizes.
- No user-defined conversion mechanism (no traits/protocols).
Relevance to Wado
Zig's approach of naming each conversion kind separately is worth considering. Rather than
one From trait for all conversions, Wado could distinguish lossless widening (automatic),
checked narrowing (explicit), and semantic conversion (trait-based).
Kotlin — Extension Functions and Smart Casts
Design
Kotlin uses explicit conversion methods (toInt(), toString(), etc.) and extension
functions for custom conversions:
fun String.toUserId(): UserId = UserId(this)
val id = "abc123".toUserId()
Smart casts narrow types after is checks but don't perform value conversion.
Strengths
- Discoverable:
.to*()methods are visible in autocomplete. - Explicit: No hidden conversions.
- Extensible: Extension functions allow adding conversions without modifying types.
Weaknesses
- No generic bound: Cannot express "any type convertible to X" in a generic context.
- Naming convention only: No compiler-enforced trait/interface for conversions.
Cross-Language Summary
| Language | Mechanism | Implicit? | Generic bound? | Fallible variant? |
|---|---|---|---|---|
| Rust | From/Into traits |
No (explicit .into()) |
Yes (impl Into<T>) |
TryFrom/TryInto |
| Scala 2 | implicit def |
Yes | Via implicits | No (runtime exn) |
| Scala 3 | Conversion typeclass |
Semi (requires import) | Yes | No |
| Swift | ExpressibleBy + init |
Literals only | No | init? (failable) |
| C++ | Converting constructors | Yes (unless explicit) |
No | No |
| Go | Explicit cast syntax | No | No | No |
| Haskell | Per-domain type classes | No | Yes | Partial |
| Python | Dunder methods | No | No (dynamic) | Exceptions |
| Zig | @as |
No | No | No |
| Kotlin | .to*() extensions |
No | No | Nullable return |
Key Takeaways for Wado
What works well in Rust's design
-
Trait-based conversion is the right abstraction level. It enables generic bounds, compiler verification, and ecosystem-wide standardization.
-
The
From→Intoblanket impl is elegant. Users implementFrom(natural direction), getIntofor free (ergonomic direction). This duality is genuinely useful. -
Separating infallible (
From) and fallible (TryFrom) is valuable. It encodes conversion safety in the type system. -
Integration with
?is the killer feature. TheFromtrait's greatest value is enabling automatic error type conversion via?. Without this use case,From/Intowould be merely convenient; with it, they're essential.
What to improve or reconsider
-
The
.into()inference problem needs addressing. The inability to turbofish.into()is a fundamental ergonomic issue. Possible solutions:- Make the target type a method parameter:
fn into<T>(self) -> T - Prefer
Type::from(x)style (which Wado can optimize for) - Provide syntax sugar that makes the target type visible
- Make the target type a method parameter:
-
The trait family is too large. Rust has
From,Into,TryFrom,TryInto,AsRef,AsMut,Borrow,BorrowMut,ToOwned,Deref,DerefMut— all related to "getting one type from another." Wado should aim for fewer, more orthogonal traits. -
Wado's GC simplifies the picture. Rust needs
AsRef/Borrow/Derefbecause of ownership and borrowing semantics. With GC-based memory management, Wado doesn't need most of these. The core need is:- Value conversion (consuming):
From<T>/Into<T> - Fallible conversion:
TryFrom<T>/TryInto<T> - Reference-based conversions (
AsRef) may be less critical.
- Value conversion (consuming):
-
Implicit vs. explicit is a spectrum. The consensus across languages is:
- Fully implicit (Scala 2, C++) → proven harmful
- Fully explicit (Go) → safe but verbose
- Explicit with trait-based generic bounds (Rust) → good middle ground
- Call-site-visible but minimal-syntax → optimal target for Wado
-
Wado already has literal coercion. Wado's existing
SequenceLiteralBuilderand numeric literal coercion handle the "literal → type" case (like Swift'sExpressibleBy). AFrom/Intoframework would handle the "value → value" case, complementing what exists. -
The
?operator dependency. If Wado plans to implement the?operator (listed as "not yet implemented" in the spec),Fromis a prerequisite. The design ofFromand?should be co-designed. -
Effect interaction. Wado has an effect system. A conversion trait should consider whether conversions can have effects (e.g., a conversion that requires I/O). This is something Rust cannot express.
Open Questions for Wado
-
Should Wado have both
FromandInto, or just one? The blanket impl trick works but adds complexity. An alternative: a singleConvert<From, To>trait, or justFromwith compiler-provided.into()sugar. -
Should
.into()be a method or syntax sugar? If it's syntax sugar (e.g.,expr as TcallingFrom::from), the turbofish problem disappears. -
How does this interact with newtypes? Currently newtypes use
asfor conversion. ShouldFrombe auto-derived for newtypes, or shouldasremain the only mechanism? -
Should conversions support effects? e.g.,
fn from(value: T) -> Self with FileSystem -
What about the
?operator? If Wado implements?, the error conversion mechanism needs to be designed together withFrom. -
How many conversion traits? Possible minimal set:
From<T>— infallible value conversionTryFrom<T>— fallible value conversion (returnsResult)- Compiler-provided
.into()sugar that callsFrom::from()
Relevant Rust RFCs
RFC 529: Conversion Traits (Accepted, Rust 1.0)
The foundational RFC establishing From, Into, AsRef, AsMut. Motivated by the
proliferation of ad hoc conversion traits (FromStr, AsSlice, FromError). Established
the principle that conversions are distinguished by cost (free reference vs. owned allocation)
and consumption (borrowing vs. moving). Also proposed a To trait for reference-to-value
conversions, which was deferred and never implemented.
RFC 1542: TryFrom / TryInto (Stabilized in Rust 1.34)
Added fallible conversion traits. Motivated by C FFI interop and platform-dependent integer
sizes. The blanket impl impl<T, U> TryFrom<U> for T where U: Into<T> causes coherence
conflicts when users try to implement TryFrom for their own types — a well-known pain point.
RFC 1210: Specialization (Accepted, Never Stabilized)
Would allow overlapping trait implementations with a "most specific wins" rule. Relevant
because it would enable specialized, more efficient From implementations alongside blanket
impls. However, specialization is unsound as demonstrated by Ralf Jung. min_specialization
is used internally by the standard library but remains unstable. As of 2025, stabilization
is not being actively pursued.
No Active RFC to Redesign From/Into
There is no active RFC proposing fundamental changes to the From/Into design. The traits
are deeply embedded in the ecosystem. Improvements focus on surrounding infrastructure (trait
solver, coherence rules) rather than the traits themselves. This means Wado has a unique
opportunity to learn from Rust's design without being constrained by backward compatibility.
Cross-Cutting Themes
| Theme | Languages | Lesson |
|---|---|---|
| Implicit conversions cause bugs | C++, Scala 2, JavaScript | Every language with broad implicit conversion has regretted or constrained it |
| Separate infallible from fallible | Rust, Haskell (Witch lib), Zig | Community converges on distinguishing "always works" from "might fail" |
| Explicit is safer but verbose | Go, OCaml, Zig | Full explicitness trades ergonomics for clarity |
| Literal coercion is the sweet spot | Go (untyped consts), Swift (ExpressibleBy), Zig (comptime), Wado | Literals molding to the target type is universally accepted as safe and ergonomic |
| Smart/flow-sensitive casts | Kotlin | Compiler-driven narrowing after type checks is well-loved and low-risk |
| Trait-based conversion (user-extensible) | Rust, Haskell, Swift | Gives users the mechanism without the implicit danger |
| Name each conversion kind | Zig (@as/@truncate/@intCast) |
Separating safe widening, checked narrowing, and lossy truncation aids auditability |
References
- RFC 529: Conversion Traits
- RFC 401: Coercions
- Effective Rust — Item 5: Understand Type Conversions
- "What is wrong with auto .into?" — Rust Internals
- "Implicit into() on return" — Rust Internals
- "Traits as Implicit Conversions" — Isaac Clayton
- Scala 3 Implicit Redesign — Baeldung
- "Can We Wean Scala Off Implicit Conversions?" — Scala Contributors
- Scala Best Practices: Avoid Implicit Conversions
- Swift ExpressibleBy Protocols Internals
- Rust API Design: AsRef, Into, Cow
- "From & Into Confusion — Why Do We Need Both?" — Rust Users Forum
- Implicit Numeric Widening Proposal — Rust Internals
- Cast Haskell Values with Witch — Rust-inspired From/TryFrom for Haskell
- Swift Evolution SE-0115: Rename ExpressibleBy Protocols
- C++ Explicit Conversion Operators (N2333)
- Zig's Integer Casting for C Programmers
- Kotlin Type Checks and Casts
- "Does From Only Exist Because of the Orphan Rule?" — Rust Users Forum
- RFC 1542: TryFrom
- RFC 1210: Specialization
- Specialization Is Unsound (PR #71420) — Ralf Jung
- Monomorphization Bloat — Andrew Lilley Brinker
- Defaults Affect Inference — Faultlore
- Winning the Fight Against Coherence — Ohad Ravid
- Haskell Coercible for Newtype Conversions — HaskellWiki
- Equivalent of Rust's From/Into in Haskell? — Haskell Discourse
