Wado

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:

There is no general-purpose mechanism for converting between arbitrary user-defined types. A structured conversion framework would enable:

  1. Ergonomic APIs that accept related types (e.g., String where &str suffices)
  2. Error type composition (prerequisite for the ? operator)
  3. Newtype interop beyond as casts
  4. 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:

In addition, Rust has borrowing conversion traits:

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

  1. Standardization: all Rust libraries agree on a single conversion protocol. No ad hoc from_foo() methods proliferating across crates.

  2. Composability with ?: From impls compose with the ? operator, enabling clean error propagation without manual matching.

  3. Generic API ergonomics: impl Into<T> as a parameter bound lets functions accept multiple input types while remaining type-safe.

  4. Zero-cost abstraction: monomorphized at compile time — no dynamic dispatch overhead.

  5. Discoverability: a well-known trait that IDE tooling can index. "What can I convert this type to/from?" becomes answerable via trait impls.

  6. Separation of fallible/infallible: From guarantees infallible conversion, while TryFrom makes fallibility explicit in the type system.

Cons

  1. Type inference failures with .into()

    The target type of Into is 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.

  2. 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.

  3. 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.

  4. 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 implement Into directly, which doesn't provide the reverse From impl — creating an asymmetric situation. The orphan rules still limit cross-crate conversion definitions.

  5. Trait family confusion

    Users must choose between From/Into, AsRef/AsMut, Borrow, Deref, and ToOwned. 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
    Deref Smart pointer transparency Wrapper types
    ToOwned Borrowed → owned (cloning) &strString

    This proliferation is a common complaint. New Rust users struggle to know which trait to implement for their conversion.

  6. Not dyn-compatible (not object-safe)

    From cannot be used as a trait object (dyn From<T>), limiting its use in dynamic dispatch scenarios.

  7. Effect system limitations

    From/Into cannot be made async or fallible (beyond TryFrom). In an effect-generic future, you'd need separate AsyncFrom, StreamFrom, etc. — an exponential explosion of trait variants.

  8. 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.

  9. 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. The momo crate exists specifically to mitigate this by splitting impl Into<T> functions into a thin generic wrapper + non-generic inner function. The standard library itself uses this "inner function" pattern.

  10. 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 like thiserror and anyhow exist — they automate what should arguably be a language-level solution. The coherence rules compound this: you cannot write a blanket impl<E: Error> From<E> for MyError because it conflicts with the reflexive impl<T> From<T> for T in std.

  11. TryFrom / FromStr design wart

    FromStr predates TryFrom and serves the same purpose as TryFrom<&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.

  12. 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 actual From implementation.

Community Opinions

The Rust community is broadly positive about From/Into as a framework, but has specific recurring complaints:


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:

  1. Invisible behavior: Conversions happen silently with no syntactic marker. Reading code reveals no hint that a conversion is occurring.

  2. Non-total conversions: Nothing prevents defining a conversion from String to Int that throws NumberFormatException at runtime. The type system says it's safe; it isn't.

  3. Import-triggered surprises: Importing a module can silently change the behavior of existing code by bringing new implicit conversions into scope.

  4. Poor error messages: When implicit resolution fails, the compiler reports generic type mismatches rather than explaining which conversion was expected.

  5. IDE masking: IDEs automatically suppress compiler warnings about implicit conversions, undermining the language's own safety mechanisms.

  6. 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


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

Weaknesses

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)

  1. Silent, surprising conversions: MyString s = "hello"; int n = s; compiles silently.
  2. Ambiguity in overload resolution: Multiple implicit conversion paths create compilation errors or, worse, select the wrong overload.
  3. The explicit keyword was added precisely because implicit conversions caused too many bugs. Modern C++ style guidelines recommend making all single-argument constructors explicit by default.
  4. The Safe Bool Problem: operator bool() allows implicit conversion to int via standard promotion, enabling nonsensical code like obj << 1 or int n = myStream. Before C++11, the workaround operator void*() allowed delete std::cout to compile.
  5. 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 int32int64 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

Weaknesses


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

Weaknesses


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

Weaknesses


Zig — Separated Conversion Builtins

Design

Zig uses distinct builtins for different conversion kinds:

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

Weaknesses

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

Weaknesses


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

  1. Trait-based conversion is the right abstraction level. It enables generic bounds, compiler verification, and ecosystem-wide standardization.

  2. The FromInto blanket impl is elegant. Users implement From (natural direction), get Into for free (ergonomic direction). This duality is genuinely useful.

  3. Separating infallible (From) and fallible (TryFrom) is valuable. It encodes conversion safety in the type system.

  4. Integration with ? is the killer feature. The From trait's greatest value is enabling automatic error type conversion via ?. Without this use case, From/Into would be merely convenient; with it, they're essential.

What to improve or reconsider

  1. 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
  2. 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.

  3. Wado's GC simplifies the picture. Rust needs AsRef/Borrow/Deref because 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.
  4. 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
  5. Wado already has literal coercion. Wado's existing SequenceLiteralBuilder and numeric literal coercion handle the "literal → type" case (like Swift's ExpressibleBy). A From/Into framework would handle the "value → value" case, complementing what exists.

  6. The ? operator dependency. If Wado plans to implement the ? operator (listed as "not yet implemented" in the spec), From is a prerequisite. The design of From and ? should be co-designed.

  7. 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

  1. Should Wado have both From and Into, or just one? The blanket impl trick works but adds complexity. An alternative: a single Convert<From, To> trait, or just From with compiler-provided .into() sugar.

  2. Should .into() be a method or syntax sugar? If it's syntax sugar (e.g., expr as T calling From::from), the turbofish problem disappears.

  3. How does this interact with newtypes? Currently newtypes use as for conversion. Should From be auto-derived for newtypes, or should as remain the only mechanism?

  4. Should conversions support effects? e.g., fn from(value: T) -> Self with FileSystem

  5. What about the ? operator? If Wado implements ?, the error conversion mechanism needs to be designed together with From.

  6. How many conversion traits? Possible minimal set:

    • From<T> — infallible value conversion
    • TryFrom<T> — fallible value conversion (returns Result)
    • Compiler-provided .into() sugar that calls From::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