wandres.dev
TRAITS · comportamiento compartido

Coherencia y la regla del huérfano: el patrón newtype

Por qué solo puedes implementar un trait para un tipo si posees el trait o el tipo, no un tipo ajeno con un trait ajeno. La coherencia como garantía global de que cada par tipo-trait tiene una sola implementación, y el patrón newtype para sortear la regla sin coste.

⏱ 18 min

Llega el momento en que quieres darle una capacidad ajena a un tipo ajeno: implementar Display para Vec<String>, digamos. Y el compilador te lo prohíbe con el error E0117. No es un capricho ni una limitación temporal: es la regla del huérfano, la guardiana de una propiedad más profunda llamada coherencia, sin la cual los traits no podrían ser un fundamento fiable para el código genérico. Entender por qué existe —y cómo el patrón newtype la sortea limpiamente y sin coste— cierra tu comprensión de los traits.

🎯 Al terminar esta lección sabrás
  • Enunciar la coherencia: como mucho una implementación por cada par tipo-trait en todo el programa.
  • Aplicar la regla del huérfano: implementar un trait exige poseer el trait o el tipo.
  • Reconocer y escribir el patrón newtype para envolver un tipo ajeno.
  • Entender qué cuesta el newtype y por qué recupera toda la expresividad.

La regla del huérfano en acción

Prueba a dar formato humano a un vector de la biblioteca estándar y el compilador te frena en seco:

use std::fmt;

// NO COMPILA (error E0117): ni Display ni Vec son tuyos.
impl fmt::Display for Vec<String> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.join(", "))
    }
}

La regla que se incumple es la regla del huérfano (orphan rule): solo puedes escribir impl Trait for Tipo si posees el trait o posees el tipo —es decir, si al menos uno de los dos está definido en tu propio crate—. Aquí Display pertenece a la biblioteca estándar y Vec también: ambos te son ajenos, así que la implementación sería “huérfana”, sin ningún padre local que la reclame, y queda prohibida.

Cuando posees uno de los dos, todo funciona. Puedes implementar un trait ajeno para un tipo tuyo:

struct Temperatura(f64);

impl std::fmt::Display for Temperatura {   // trait ajeno, tipo propio: OK
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{} grados", self.0)
    }
}

…o un trait tuyo para un tipo ajeno:

trait Resumible { fn resumen(&self) -> String; }

impl Resumible for String {   // trait propio, tipo ajeno: OK
    fn resumen(&self) -> String {
        format!("cadena de {} bytes", self.len())
    }
}

Lo único vetado es la combinación doble-ajeno: trait ajeno y tipo ajeno a la vez.

Por qué existe la regla: la coherencia

La regla del huérfano no protege un capricho estético, sino una propiedad global llamada coherencia: en todo el programa, cada par (tipo, trait) tiene como mucho una implementación. Nunca dos. Gracias a ello, cuando escribes x.resumen(), el significado de esa llamada es único y no depende de qué crate la observe ni de qué módulos estén importados.

Imagina que la regla no existiera. El crate colores podría implementar Display para Vec<String> de una forma, y el crate tablas podría implementarlo de otra. Si tu programa dependiera de ambos, habría dos implementaciones de Display for Vec<String> compitiendo, y ni el compilador ni tú sabríais cuál usa mi_vec.to_string(). Peor aún: añadir una dependencia inofensiva rompería, a distancia, código que ya funcionaba. La regla del huérfano corta ese escenario de raíz con una condición puramente sintáctica —¿es local el trait o el tipo?— que dos crates cualesquiera nunca pueden satisfacer sobre el mismo par ajeno a la vez.

ℹ️
La forma precisa de la regla

“Posee el trait o el tipo” es la versión de bolsillo. El enunciado exacto permite impl TraitAjeno for TipoLocal y, con matices, casos como impl TraitAjeno<TipoLocal> for TipoAjeno: basta con que algún tipo local aparezca en los argumentos del trait antes que cualquier parámetro de tipo genérico no cubierto. Para casi todo el código diario, la regla mental “uno de los dos debe ser tuyo” es suficiente; la formulación completa solo importa cuando defines traits genéricos con varios parámetros de tipo.

flowchart TB
q[quieres implementar un trait para un tipo] --> d[posees el trait o el tipo]
d -->|al menos uno es local| ok[permitido]
d -->|ambos son externos| no[la regla del huerfano lo prohibe con E0117]
no --> nt[envuelve el tipo externo en un newtype local]
nt --> ok

El patrón newtype: recuperar la propiedad

¿Y si de verdad necesitas Display para algo parecido a un Vec<String>? La salida canónica es el patrón newtype: envuelves el tipo ajeno en una tupla-struct de un solo campo definida por ti. Ese envoltorio es tuyo, así que recuperas el derecho a implementar cualquier trait sobre él.

use std::fmt;

struct Lista(Vec<String>);   // newtype: el envoltorio es local, ya es tuyo

impl fmt::Display for Lista {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let l = Lista(vec![String::from("a"), String::from("b")]);
    println!("{l}");   // [a, b]
}

Accedes al valor envuelto con self.0, el primer (y único) campo de la tupla. El coste en memoria es nulo: un newtype de un solo campo tiene exactamente la misma representación que su contenido, y el compilador lo trata igual de eficientemente; la envoltura existe solo en el sistema de tipos, no en el binario.

Para que el newtype sea cómodo, reexpones los métodos del interior que de verdad necesites, cada uno delegando en el valor envuelto:

impl Lista {
    fn agregar(&mut self, s: String) { self.0.push(s); }  // delega en Vec::push
    fn cuantos(&self) -> usize { self.0.len() }            // delega en Vec::len
}
⚠️
El newtype no hereda los métodos del interior

Al envolver un Vec<String> en Lista, pierdes acceso directo a los métodos del vector: lista.push(...) no compila, porque Lista no es un Vec. Tienes tres opciones: reexponer a mano los métodos que necesites (impl Lista { fn push(&mut self, s: String) { self.0.push(s) } }), implementar Deref para delegar automáticamente al interior —cómodo pero discutido, porque difumina la frontera del newtype—, o acceder al campo con .0 cuando haga falta. Reexponer lo justo suele ser lo más honesto: el newtype existe precisamente para no ser el tipo interior.

El newtype más allá de la coherencia

Sortear la regla del huérfano es solo uno de los usos del newtype. El mismo patrón —envolver un valor en un tipo local de coste cero— sirve también para dar significado a datos que, desnudos, serían intercambiables. Un f64 puede representar metros o segundos, pero el compilador no lo sabe: sumar unos con otros, o pasarlos en el orden equivocado, compila aunque sea un disparate físico. Envuélvelos y el sistema de tipos empieza a vigilar:

struct Metros(f64);
struct Segundos(f64);

fn velocidad(distancia: Metros, tiempo: Segundos) -> f64 {
    distancia.0 / tiempo.0
}

fn main() {
    let v = velocidad(Metros(100.0), Segundos(9.58));
    println!("{v:.2} m/s");
    // velocidad(Segundos(9.58), Metros(100.0)); // ERROR: los tipos no encajan
}

Invertir los argumentos ya no compila: Metros y Segundos son tipos distintos aunque ambos envuelvan un f64. Es la misma técnica de la coherencia puesta al servicio de la corrección lógica: un envoltorio sin coste que convierte un error de razonamiento en un error de tipos, atrapado en compilación. Reconocerás en ello el espíritu de todo Rust —hacer inexpresables los estados inválidos—, y el newtype es una de sus herramientas más baratas para lograrlo.

Coherencia: la resolución de traits es una función, no una relación

La coherencia es la propiedad de la que depende, en silencio, toda la solidez del sistema de traits: garantiza que la resolución de traits sea una función —cada par (tipo, trait) se aplica a lo sumo a una implementación— y no una relación con varias respuestas posibles. Esto es lo que permite que un genérico fn f<T: Trait> confíe en que existe un único significado de Trait para cada T; sin coherencia, el compilador no podría razonar sobre código genérico, porque “usar la capacidad de T” sería ambiguo. La regla del huérfano es la aproximación modular y compilable por separado a esa garantía: en lugar de exigir que el compilador vea el programa entero a la vez para comprobar que no hay dos implementaciones en colisión —imposible con compilación separada y crates distribuidos—, impone una condición local, sintáctica y conservadora (¿es local el trait o el tipo?) que por construcción ningún par de crates puede violar sobre el mismo par ajeno. Es un intercambio deliberado: renuncias a algo de expresividad —genuinamente no puedes añadir una implementación ajena a un tipo ajeno— a cambio de una propiedad global que sobrevive a la evolución semántica del ecosistema; un crate del que dependes puede publicar nuevas implementaciones sin miedo a romper las tuyas, porque la regla le impedía pisar tu terreno desde el principio. Y el patrón newtype revela la naturaleza exacta de la restricción: al acuñar un tipo local nuevo recuperas la propiedad de uno de los dos lados y, con ella, el derecho a implementar. Que un envoltorio de coste cero baste para restaurar toda la expresividad no es casualidad, sino la prueba de que la regla del huérfano restringe la identidad de los tipos, no su representación en memoria. Las typeclasses de Haskell viven la misma tensión —sus orphan instances son un peligro conocido y desaconsejado—; Rust simplemente ascendió esa disciplina de recomendación de estilo a regla dura del compilador, y a cambio te regala una garantía que ningún ensamblaje de crates puede quebrar.

📝
Lo esencial

La regla del huérfano permite impl Trait for Tipo solo si posees el trait o el tipo; nunca un trait ajeno sobre un tipo ajeno (error E0117). Existe para preservar la coherencia: un único significado por par tipo-trait en todo el programa, garantía que hace posible el código genérico y sobrevive a la compilación separada. Cuando necesites implementar un trait ajeno sobre un tipo ajeno, envuélvelo en un newtype —una tupla-struct local de un campo—: recuperas la propiedad, implementas lo que quieras y no pagas nada en memoria, a cambio de reexponer los métodos del interior que necesites.

⚔️ Sortea la regla con propiedad
  1. Intenta impl std::fmt::Display for Vec<i32> y lee el error E0117 completo; identifica cuál de los dos —trait o tipo— te falta poseer.
  2. Define struct MiVec(Vec<i32>) e implementa Display para él mostrando los elementos separados por guiones.
  3. Comprueba que mi_vec.len() no compila directamente y añade un método que lo reexponga con self.0.len().
  4. Implementa un trait tuyo Estadisticas con un método media para Vec<f64> directamente y explica por qué aquí sí lo permite la regla.
  5. Argumenta con un ejemplo de dos crates por qué, sin la regla del huérfano, una simple cargo add podría romper la compilación de código ajeno.