WEP: Conversion Traits (From, TryFrom, ? operator)
Context
Wado currently has two conversion mechanisms:
ascasts — explicit primitive-to-primitive (e.g.,i32→f64,char→i32) and newtype casts (e.g.,Meters→f64)- Literal coercion — numeric and string literals adapt to the target type context (e.g.,
let x: i64 = 42)
There is no general-purpose mechanism for converting between user-defined types, and the ? operator (already reserved in the spec) has no implementation. The two key motivations for this WEP are:
- Error type composition: Enable the
?operator for ergonomic error propagation with automatic error conversion - Structured conversions: Provide a standardized protocol for user-defined type conversions
Language Survey
See Research: From/Into Conversion Trait Framework for the full survey. Key findings:
| Approach | Languages | Outcome |
|---|---|---|
| Implicit conversions | C++, Scala 2 | Universally regretted; causes bugs, hides costs |
| Fully explicit | Go, Zig | Safe but verbose; no generic conversion protocol |
| Trait-based, call-site-explicit | Rust | Good balance; From/? integration is the standout success |
| Literal coercion only | Swift | Elegant for literals, insufficient for value-to-value |
Rust's From/Into framework is the primary reference design. Its strengths: standardized conversion protocol, ? operator integration, separation of infallible/fallible. Its weaknesses:
.into()hides the target type — turbofish doesn't work, IDE "go to definition" leads to blanket implInto<T>exists mainly for orphan rule workarounds — fixed in Rust 1.41impl Into<T>parameters cause monomorphization bloat — themomocrate exists to mitigate this- Too many conversion traits —
From,Into,TryFrom,TryInto,AsRef,AsMut,Borrow,Deref,ToOwnedconfuse users
Design Goals
- Support the
?operator withFrom-based error conversion - Keep target types visible at every call site
- Minimize the number of conversion traits (Wado's GC eliminates the need for
AsRef/Borrow/Deref) - Support compiler-generated impls for variants and newtypes
- No implicit conversions beyond existing literal coercion
Decision
1. From<T> Trait
Infallible conversion trait in core:prelude:
#[comp_feature("from")]
pub trait From<T> {
fn from(value: T) -> Self;
}
Usage is always explicit via Type::from(value):
impl From<i32> for String {
fn from(value: i32) -> String {
return `${value}`;
}
}
let s = String::from(42); // "42"
The #[comp_feature("from")] attribute registers From in the compiler's TypeTable, enabling the ? operator to look up From impls for error conversion.
Reflexive impl: The compiler provides impl From<T> for T for all types (identity conversion). This is needed for ? to work when the error type already matches.
2. TryFrom<T> Trait
Fallible conversion trait returning Result:
pub trait TryFrom<T> {
type Err: Error;
fn try_from(value: T) -> Result<Self, Self::Err>;
}
The associated type is Err, as FromStr's is: it names the Result's Err
side, and type Error would give the name two meanings inside an impl that
also has the Error trait in scope.
Err: Error makes every conversion failure reportable. A caller reaching the
failure through the bound has no impl to read, so without it the value it holds
says nothing:
fn reason<T: TryFrom<i32>>(value: i32) -> String {
return match T::try_from(value) {
Ok(_) => "converted",
Err(e) => `${e}`, // `Error: Display`
};
}
impl TryFrom<i64> for u8 {
type Err = ConvertError;
fn try_from(value: i64) -> Result<u8, ConvertError> {
if value < 0 || value > 255 {
return Result::Err(ConvertError::new(`${value} out of range for u8`));
}
return Result::Ok(value as u8);
}
}
let n = u8::try_from(42 as i64)?; // Ok(42)
let n = u8::try_from(999 as i64)?; // Err: a ConvertError reading "999 out of range for u8"
ConvertError is what every stdlib impl uses. An impl may define its own error
type instead, which needs an impl Display and the empty impl Error for T;.
3. ? Operator
The ? postfix operator (already reserved in the spec's operator table) provides early return for Result and Option.
On Result<T, E>: Unwrap Ok(v) → v, or convert the error and return early:
// Desugars to:
// match expr {
// Ok(v) => v,
// Err(e) => { return Result::Err(From::from(e)); },
// }
fn process() -> Result<i32, AppError> {
let data = read_file("input.txt")?; // IoError → AppError via From
let n = parse_int(data)?; // ParseError → AppError via From
return Result::<i32, AppError>::Ok(n);
}
The error conversion From::from(e) is the key integration point: ? calls From to convert the inner error type to the enclosing function's error type.
On Option<T>: Unwrap Some(v) → v, or return null (which is Option::None):
// Desugars to:
// match expr {
// Some(v) => v,
// None => { return null; },
// }
fn find_user_name(id: i32) -> Option<String> {
let user = find_user(id)?; // None → return null
let profile = user.profile()?; // None → return null
return Option::<String>::Some(profile.name);
}
Requirements:
- The enclosing function must return
Result<T, E>(forResultoperand) orOption<T>(forOptionoperand) - Using
?onResultin a function returningOption, or vice versa, is a compile error ?cannot be used inside closures that don't returnResult/Optionthemselves
Effect interaction: The ? operator performs early return but does not introduce effects. The function's with clause is unaffected.
4. No Into<T> Trait
Wado does not provide an Into<T> trait. The reasons:
-
.into()hides the target type. Code likelet x = value.into()requires type context to understand.String::from(value)is always clear. -
Orphan rules make
Intounnecessary. Rust neededIntobecause pre-1.41 orphan rules preventedimpl From<ForeignType> for LocalTypein some cases. Wado uses Rust 1.41+ style orphan rules where having a local type in any trait parameter position suffices. WithFrom<LocalType> for ForeignTypeallowed, there's no need forInto. -
impl Into<T>parameters cause monomorphization bloat. Each unique caller type generates a copy of the function body. The Rust ecosystem created themomocrate specifically to work around this problem.
Users write Type::from(value) at call sites. This is slightly more verbose than .into() but always makes the target type visible.
5. No as Extension for From
The as operator retains its current semantics:
- Primitive casts:
42 as f64,'A' as i32 - Newtype casts:
meters as f64,value as Meters
as does not call From::from(). The semantic distinction:
| Mechanism | Semantics | Cost |
|---|---|---|
as |
Bit reinterpretation / cast | Zero-cost, always succeeds |
From::from() |
Semantic conversion | May allocate, always succeeds |
TryFrom::try_from() |
Fallible conversion | May allocate, may fail |
This keeps as simple and predictable. Developers know x as T is always a trivial operation.
6. Variant From Auto-Derive
Variant types can opt into compiler-generated From impls using the existing auto-derive syntax:
variant AppError {
Io(IoError),
Parse(ParseError),
Validation(String),
}
impl From<IoError> for AppError; // generates: AppError::Io(value)
impl From<ParseError> for AppError; // generates: AppError::Parse(value)
impl From<String> for AppError; // generates: AppError::Validation(value)
Each impl From<T> for Variant; (with no body) instructs the compiler to generate:
impl From<IoError> for AppError {
fn from(value: IoError) -> AppError {
return AppError::Io(value);
}
}
This follows the existing pattern used by impl Serialize for Type; and impl Deserialize for Type;.
Ambiguity rule: If multiple variant cases have the same payload type, the compiler rejects the auto-derive with a clear error message. The user must write the From impl manually.
This pattern is especially powerful with ?:
fn process() -> Result<String, AppError> {
let data = read_file("input.txt")?; // IoError → AppError::Io via From
let parsed = parse(data)?; // ParseError → AppError::Parse via From
return Result::<String, AppError>::Ok(parsed);
}
7. Newtype From Auto-Generation
The compiler automatically generates bidirectional From impls for newtypes:
type UserId = u64;
// compiler auto-generates:
// impl From<u64> for UserId { fn from(value: u64) -> UserId { return value as UserId; } }
// impl From<UserId> for u64 { fn from(value: UserId) -> u64 { return value as u64; } }
This means both as and From::from() work for newtypes:
let id: UserId = 42; // literal coercion (existing)
let id2 = 42 as UserId; // as cast (existing)
let id3 = UserId::from(42 as u64); // From (new, auto-generated)
let raw = u64::from(id); // From (new, auto-generated)
The auto-generated From impls enable newtypes to participate in generic code:
fn print_id<T: From<u64>>(raw: u64) {
let id = T::from(raw);
println(`${id}`);
}
print_id::<UserId>(42);
8. Standard Library From Impls
The standard library provides From impls for common lossless conversions:
Numeric widenings (infallible, lossless):
| From | To |
|---|---|
i8 |
i16, i32, i64 |
i16 |
i32, i64 |
i32 |
i64 |
u8 |
u16, u32, u64, i16, i32, i64 |
u16 |
u32, u64, i32, i64 |
u32 |
u64, i64 |
f32 |
f64 |
Other conversions:
impl From<char> for String— single character to stringimpl From<bool> for i32—false→0,true→1
TryFrom impls for narrowing conversions:
impl TryFrom<i64> for i32— checked narrowingimpl TryFrom<i32> for u32— checked sign conversion- (and similar for other narrowing/sign combinations)
9. Interaction with Existing Features
| Feature | Status | Interaction |
|---|---|---|
as casts |
Unchanged | as = bit cast; From = semantic conversion |
| Literal coercion | Unchanged | Literals still coerce to target type without From |
null → Option::None |
Unchanged | Syntactic sugar, not a From call |
| Template strings | Unchanged | \{x}`usesDisplay, notFrom` |
Newtype as |
Unchanged | as still works; From is additionally auto-generated |
Consequences
Benefits
- Ergonomic error handling:
?with automaticFromconversion eliminates manualmatchonResultfor error propagation - Explicit target types: No
.into()means the target type is always visible at the call site - Minimal trait surface: Two traits (
From,TryFrom) instead of Rust's ten+ conversion traits - Variant error composition:
impl From<E> for AppError;auto-derive +?makes error handling concise - GC-simplified design: No need for
AsRef,Borrow,Deref,ToOwned— Wado's GC handles reference semantics
Trade-offs
- More verbose than
.into():String::from(value)is longer thanvalue.into(). This is intentional — explicitness over brevity. - No generic "accepts any convertible type" parameter: Without
impl Into<T>, functions cannot accept multiple types via a single parameter. Users callType::from(value)at the call site instead. This avoids monomorphization bloat but requires the caller to be explicit.
Future Considerations
into T Operator
If the community finds Type::from(value) too verbose in practice, an into T postfix operator could be added:
let s = 42 into String; // equivalent to String::from(42)
let id = raw_id into UserId; // equivalent to UserId::from(raw_id)
This preserves target type visibility (unlike .into()) while being more concise than Type::from(value). It is explicitly out of scope for the initial release — we start with From::from() and evaluate community feedback.
from T Parameter Modifier
A parameter modifier that performs conversion at the call site without monomorphization:
fn greet(name: String from &str) with Stdout {
println(`Hello, ${name}!`);
}
greet("world"); // compiler inserts String::from("world") at call site
This is reserved as a future possibility but not included in this proposal.
