Por que tipos mutáveis e imutáveis nunca são subtipos um do outro
Um post que circulou no Hacker News revisita o princípio de substituição de Liskov para explicar uma regra que parece arbitrária em toda linguagem com tipos, inclusive Rust: dado mutável e dado imutável não podem formar uma hierarquia de subtipos.
Quem migra de Java↳Java42 conteúdosNovidades do Java 26 (para desenvolvedores)Dev (Back & Front) · mar 2026Visual Studio Code para Java: o guia completo (dicas, configuração e extensões)Dev (Back & Front) · out 2025Quarkus: Modernizando a linguagem Java para a era da nuvemDev (Back & Front) · nov 2020Ver tudo em Dev (Back & Front) →, Python ou JavaScript para Rust esbarra cedo numa estranheza: por que uma referência &mut T não pode simplesmente "ser" um &T em qualquer contexto, e vice-versa? Intuitivamente, um dado mutável parece ter tudo que um imutável tem, e mais um pouco. Um post do blog crumbles.blog, que subiu ao topo do Hacker News em setembro, ataca essa pergunta de frente, sem citar Rust nenhuma vez, mas respondendo exatamente ao que trava o sistema de tipos da linguagem para quem vem de fora dela.
O princípio que a maioria aprendeu errado
A resposta começa no princípio de substituição de Liskov, mas não na versão simplificada que costuma aparecer em curso de orientação a objetos. Formalmente, um tipo S é subtipo de T se um valor de S pode ser usado em todo contexto onde um valor de T é esperado. O post insiste no rigor da palavra "todo": não é "na maioria dos casos", é sempre, sem exceção, porque é isso que um verificador de tipos estático precisa provar antes de aceitar seu programa.
Essa exigência muda o que conta como contrato de um tipo. Não basta comparar a lista de operações disponíveis; é preciso comparar as garantias que cada operação promete manter.
O experimento do par imutável e mutável
O autor usa o exemplo mais simples possível: um par (cons/car/cdr, a dupla de valores da tradição Lisp/Scheme). A versão imutável oferece três operações: construir o par e ler cada um dos dois campos. A versão mutável adiciona duas: set-car! e set-cdr!, para trocar os valores depois de criado.
É óbvio que um par imutável não serve onde se espera um mutável: falta set-car!/set-cdr!, e o código quebra na primeira tentativa de chamá-los. O ponto sutil é o caminho inverso. Um par mutável tem todas as operações do imutável, então por que ele não poderia substituir um imutável em qualquer lugar?
A resposta está no contrato implícito de car e cdr num par imutável: chamado duas vezes sobre o mesmo par, o resultado é sempre igual. Esse contrato permite, por exemplo, calcular o hash do par uma vez e reutilizar o valor com segurança (a técnica de hash consing citada no post). Um par mutável não pode fazer essa promessa: alguém pode chamar set-car! entre as duas leituras. Por isso os dois têm que ser tipos completamente separados, mesmo compartilhando operações de leitura.
O que isso explica sobre &T e &mut T
Rust não modela pares Lisp, mas resolve exatamente esse problema com suas duas formas de referência. Uma &T (referência compartilhada) carrega o mesmo contrato do par imutável do post: enquanto ela existe, o borrow checker garante que ninguém mais vai mutar o valor apontado. Uma &mut T (referência exclusiva) é o par mutável: dá acesso de escrita, mas em troca exige exclusividade total, nenhuma outra referência, mutável ou não, pode coexistir com ela.
Isso explica por que o compilador aceita passar uma &mut T onde se espera uma &T (o chamado reborrow, algo como &*minha_ref_mut), mas nunca o contrário sem unsafe:
fn le(valor: &i32) -> i32 { *valor }
fn escreve(valor: &mut i32) { *valor += 1; }
let mut x = 10;
let r = &mut x;
le(r); // ok: &mut i32 vira &i32 nesse ponto
// escreve precisaria de &mut i32; um &i32 nunca vira isso de voltaNão é subtipagem clássica no sentido do post (Rust não tem hierarquia de classes para tipos primitivos), mas o raciocínio de contrato é o mesmo: quem tem &mut T pode abrir mão temporariamente da capacidade de escrever e se comportar como &T, porque isso só restringe o que pode fazer. O caminho inverso violaria a garantia que todo &T carrega, a de que o valor não muda enquanto a referência existir. É a mesma regra do car/cdr do post, com nomes diferentes.
Um efeito prático dessa regra aparece em HashMap. Se a chave usa RefCell ou outra forma de mutabilidade interna e alguém muda o valor depois de inserido, o hash calculado na inserção não bate mais com o hash recalculado na busca, e a entrada some silenciosamente. A documentação da própria std alerta sobre isso: é o hash consing do post quebrando na prática, décadas depois, num container bem mais moderno que um par de Scheme.
Traits em vez de hierarquia: a saída que o post já prevê
O artigo original também explica por que isso não condena a mutabilidade e a imutabilidade a viverem sem nenhum código compartilhado: linguagens estaticamente tipadas resolvem isso com classes de tipo (o trabalho de Wadler e Blott é citado diretamente), que o post equipara a interfaces, traits ou roles conforme a linguagem. Traits em Rust são exatamente esse mecanismo. Um trait pode declarar um método que recebe &self e outro, num trait separado, que recebe &mut self, sem que um dependa do outro formar uma cadeia de subtipos.
Index e IndexMut da std são o exemplo mais direto: Vec implementa os dois, mas são traits distintos, e uma função que só recebe &Vec nunca consegue indexar por IndexMut, porque o compilador nem oferece a assinatura. É o mesmo desenho do post, com car/cdr no papel de Index::index e set-car!/set-cdr! no papel de IndexMut::index_mut.
O que fica para quem vem de outra linguagem
Em Java ou TypeScript, é comum tratar "visão somente leitura de um objeto mutável" como se fosse um subtipo por convenção, um Readonly que o próprio desenvolvedor promete respeitar, sem o compilador provar nada. Rust recusa esse atalho: a separação entre &T e &mut T é imposta pelo borrow checker, não por boa vontade, e é ela que sustenta a promessa de concorrência sem data races que a linguagem vende.
Fica em aberto, e o post não entra nisso, como esse raciocínio interage com a variância de lifetimes, onde Rust de fato permite um tipo de subtipagem (&'long T pode substituir &'short T). É um eixo diferente do de mutabilidade, mas é o próximo lugar óbvio para quem quiser aprofundar como o sistema de tipos de Rust decide, formalmente, o que pode substituir o quê.
Fonte: Hacker News
Este artigo foi escrito por Redação iMasters. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.









