Estou trabalhando em um projeto no qual estou construindo um tipo de vetor de bits em Rust e estou explorando se é possível restringir genéricos constantes usando limites de desigualdade.
Meu objetivo é projetar uma API que evite a verificação de limites em tempo de execução usando garantias em tempo de compilação baseadas em genéricos constantes, além de uma variante verificada em tempo de execução. Minha esperança é que essas funções sejam simples o suficiente para que o compilador possa embuti-las, de modo que seja uma abstração verdadeiramente de custo zero.
Aqui está um exemplo de código simplificado que demonstra o que estou tentando fazer. Este código não compila, mas mostra o tipo de restrição que desejo:
/// Idea: A function with multiple generic parameters where a
/// trait bound is used to establish an ordering between them.
fn add_ordered<const N1: usize, const N2: usize>() -> usize
where N2 > N1 {
N1 + N2
}
fn main() {
let n = add_ordered::<1, 2>();
println!("{n}");
}
Se eu remover a cláusula where N2 > N1, o código será compilado, mas não manterá o tipo de invariante que estou tentando manter aqui (nesse caso, que N2 é maior que N2).
Minhas perguntas:
- Existe atualmente uma maneira em Rust de escrever uma restrição genérica const como
where N2 > N1
? - Caso contrário, há alguma solução alternativa ou recurso de linguagem (noturno ou não) que permita algo semelhante?
- Se realmente não há como fazer isso agora, há algum motivo para que não possa ser feito, além do fato de que simplesmente não foi feito? Isso poderia levar a uma possível RFC?
Pesquisei um pouco, mas não consegui encontrar muita coisa sobre isso. Quaisquer dicas ou explicações serão muito apreciadas. Obrigado!
Por que esse código não compila
where
As cláusulas em Rust participam do sistema de tipos – o compilador tenta provar em tempo de compilação que qualquer uso de um tipo, característica, método, etc. está em conformidade com suaswhere
cláusulas e rejeita o programa se isso não acontecer.Além de ser uma restrição sobre quando você pode usar o código, uma
where
cláusula também fornece uma suposição que o compilador pode usar para provar que seu programa está correto. Por exemplo, esta função hipotética não compila:porque o compilador não consegue provar que os requisitos para a chamada
t.clone()
foram atendidos. No entanto, se você adicionar umawhere
cláusula apropriada:então o código agora compila corretamente, porque o compilador pode usar a
where
cláusula como uma prova de que a chamadat.clone()
é válida.Um dos problemas básicos com o código que você está escrevendo é que, ao escrever uma
where
cláusula, você está dizendo ao Rust para usá-la tanto para verificar se as chamadas para a função que você está definindo estão corretas quanto para verificar se as chamadas feitas pela função que você está definindo estão corretas. Atualmente, o Rust não suporta esse tipo de raciocínio sobre os valores possíveis para parâmetros constantes – por exemplo, se essa sintaxe fosse aceita, os programadores poderiam esperar usar a combinação das cláusulaswhere A > B
ewhere B > C
para atender a umwhere A > C
requisito, o que significaria que alguém precisaria implementar um código que pudesse raciocinar sobre as propriedades relevantes do>
operador.Acontece que mesmo o caso mais simples desse tipo de coisa ainda não foi implementado:
where N1 == N2
também é rejeitado (com a mensagem de erro "restrições de igualdade ainda não são suportadas emwhere
cláusulas" e um link para o problema Rust #20041 ). Acontece que é difícil implementar até mesmo restrições relativamente simples no provador do sistema de tipos. Uma boa maneira de pensar sobre isso é que, para compilar um genérico, o compilador Rust precisa basicamente atuar como um provador de teoremas e produzir uma prova de que a compilação está correta; e quaisquer restrições adicionais que você possa impor a um genérico que possa participar da prova precisam ser implementadas como algo com que o provador de teoremas seja capaz de trabalhar, o que geralmente é bastante difícil (e provavelmente impossível no caso geral).A sintaxe específica que você estava tentando usar tem outro problema: após uma
where
cláusula, o Rust normalmente espera ver o nome de um tipo e, em locais onde os tipos são esperados,<
funcionam>
como colchetes e correspondem uns aos outros. Portanto, o Rust interpreta o "the">
como um colchete de fechamento sem correspondência, em vez de um operador "maior que", e é por isso que a mensagem de erro que você recebe parece confusa e sem relação.Se você não precisa de uma prova de nível de tipo
Todo esse problema acontece basicamente porque
where
as cláusulas criam tanto uma obrigação de prova para o compilador provar algo durante a verificação de tipos, quanto uma suposição que o verificador de tipos pode usar para provar coisas. É bem possível que isso não seja exatamente o que você tinha em mente, e o requisito que você está tentando expressar seja, na verdade, apenas um requisito de segurança/correção, e não algo que participe das provas em nível de tipo – você quer que o compilador o verifique, mas não se importa que seja verificado especificamente no verificador de tipos ou que seja utilizável como uma suposição para provar outras coisas.Se você quiser que algo seja verificado em tempo de compilação, mas não necessariamente pelo verificador de tipos, não use a
where
palavra-chave : em vez disso, a palavra-chave mais geral para avaliação em tempo de compilação éconst
. Em versões recentes do Rust (1.79 ou posterior – a 1.79 foi lançada em 13 de junho de 2024, então algumas pessoas ainda usarão versões mais antigas), você pode escrever umaconst
asserção no corpo da sua função:Isso será verificado em tempo de compilação e causará um erro em tempo de compilação se houver alguma chamada para
add_ordered
exist ondeN2 > N1
não for válido (a mensagem de erro informa a condição que falhou, a localização doconst { … }
bloco e a localização da chamada para a função). Ao contrário de umawhere
cláusula, ela não participará da verificação de tipo; o compilador verifica issoN2 > N1
porque você solicitou, mas não usa as informações para nenhum outro propósito além de produzir o erro em tempo de compilação se não for válido.Espero que isso seja bom o suficiente para o que você tinha em mente; não é bom o suficiente para provas de nível de tipo, mas ainda é bom o suficiente para, por exemplo, verificar uma invariante de solidez ou detectar usos acidentais sem sentido da API.
Infelizmente, isso não é possível no modo estável.
Uma maneira de expressar isso no modo noturno seria:
(sim, a sintaxe é estranha)
No entanto, o recurso está incompleto e possivelmente falho.
Além disso, pelo que ouvi, não há chance de que isso se estabilize tão cedo.