Rust: Problema de rastreamento para RFC de grampo

Criado em 26 ago. 2017  ·  101Comentários  ·  Fonte: rust-lang/rust

Rastreamento de problema para https://github.com/rust-lang/rfcs/pull/1961

PR aqui: # 44097 # 58710
PR de estabilização: https://github.com/rust-lang/rust/pull/77872

PENDÊNCIA:

  • [x] Faça o RFC passar no período de comentário final
  • [x] Implementar RFC
  • [] Estabilizar
B-unstable C-tracking-issue Libs-Tracked T-libs

Comentários muito úteis

Parece que estamos em uma situação muito ruim se algum método que as pessoas desejam o suficiente para definir uma característica de extensão nunca puder ser adicionado à biblioteca padrão.

Todos 101 comentários

Observação: isso quebrou o Servo e o Pathfinder.

cc @ rust-lang / libs, este é um caso semelhante a min / max , onde o ecossistema já estava usando o nome clamp e, portanto, adicioná-lo causou ambiguidade . Isso é permitido de acordo com a política de sempre, mas ainda assim está causando problemas posteriores.

Indicando-se para a reunião de triagem na terça-feira.

Algum pensamento nesse meio tempo?

Estou meio que com @bluss nisso, pois seria bom não repeti-lo. "Clamp" é provavelmente um ótimo nome, mas poderíamos contornar isso escolhendo um nome diferente?

restrict
clamp_to_range
min_max (porque é como combinar mínimo e máximo)
Isso pode funcionar. Podemos usar a cratera para determinar o quão ruim é realmente o impacto de clamp ? clamp é bem conhecido em várias linguagens e bibliotecas.

Se acharmos que podemos precisar renomear, provavelmente é melhor reverter o PR imediatamente e, em seguida, testar mais cuidadosamente com cratera etc. @Xaeroxe , está pronto para isso?

Certo. Nunca usei cratera antes, mas posso aprender.

@Xaeroxe ah, desculpe, eu quis dizer fazer um RP de reversão rapidamente. (Estou de férias hoje, então você pode precisar de outra pessoa em libs, como @BurntSushi ou @alexcrichton , para ajudar a conseguir isso).

Estou preparando o PR agora. Divirta-se em suas férias!

Poderia clamp_to_range(min, max) ser composto de clamp_to_min(min) e clamp_to_max(max) (com a afirmação adicional de min <= max ), mas essas funções também podem ser chamadas independentemente?

Suponho que essa ideia exige um RFC.

Devo dizer que estou trabalhando para obter uma função de 4 linhas na biblioteca std por 6 meses. Estou meio esgotado. A mesma função foi mesclada em num em 2 dias e isso é bom o suficiente para mim. Se alguém realmente quiser isso na biblioteca std, vá em frente, mas não estou pronto para mais 6 meses disso.

Estou reabrindo isso para que a indicação anterior de @aturon ainda seja vista.

Acho que isso deve ser feito conforme está escrito ou a orientação sobre as mudanças que podem ser feitas deve ser atualizada para evitar o desperdício de tempo das pessoas no futuro.

Ficou muito claro desde o início que isso poderia causar a quebra que causou. Pessoalmente, eu comparei com ord_max_min que quebrou um monte de coisas:

E a resposta para isso foi "A função Ord::min foi adicionada [...] A equipe de libs decidiu hoje que isso é uma quebra aceita". E esse era um recurso TMTOWTDI com um nome mais comum, enquanto clamp ainda não existia em std em uma forma diferente.

Parece, subjetivamente, para mim que se este RFC for revertido, a regra real é "Você basicamente não pode colocar novos métodos em traços em std, exceto talvez Iterator ".

Você também não pode realmente colocar novos métodos em tipos reais. Considere a situação em que alguém tinha um "traço de extensão" para um tipo em std. Agora std implementa um método que o traço de extensão fornecido como um método real neste tipo. Então, isso se torna estável, mas esse novo método ainda está atrás de um sinalizador de recurso. O compilador irá então reclamar que o método está atrás de um sinalizador de recurso e não pode ser usado com o conjunto de ferramentas estável, em vez de o compilador escolher o método do trait de extensão como antes, causando a quebra do compilador estável.

Também é importante notar: este não é apenas um problema de biblioteca padrão. A sintaxe de chamada de método torna realmente difícil evitar a introdução de mudanças significativas em qualquer lugar do ecossistema.

(meta) Apenas copiando meu comentário no irlo aqui.

Se concordarmos que # 44438 é justificado,

  1. Podemos precisar reconsiderar se a quebra de inferência de tipo garantido pode realmente ser desconsiderada como XIB.

    Atualmente, a alteração de inferência de tipo é considerada aceitável pelas RFCs 1105 e 1122, pois sempre se pode usar UFCS ou outras formas de forçar um tipo. Mas a comunidade realmente não gostou da quebra causada por # 42496 ( Ord::{min, max} ). Além disso, # 41336 (primeira tentativa de T += &T ) foi fechado "apenas" devido a 8 regressões de inferência de tipo.

  2. Sempre que adicionamos um método, deve haver uma passagem de cratera para garantir que o nome ainda não exista.

    Observe que adicionar métodos inerentes também pode causar falha de inferência - # 41793 foi causado pela adição dos métodos inerentes {f32, f64}::from_bits , que conflitam com o método ieee754::Ieee754::from_bits no traço downstream.

  3. Quando a caixa a jusante não especificou #![feature(clamp)] , o candidato Ord::clamp nunca deve ser considerado (um aviso compatível com o futuro ainda pode ser emitido) a menos que esta seja a solução única. Isso permitirá a introdução de novos métodos de características, não "quebra de instabilidade", mas o problema ainda voltará quando se estabilizar.

Parece que estamos em uma situação muito ruim se algum método que as pessoas desejam o suficiente para definir uma característica de extensão nunca puder ser adicionado à biblioteca padrão.

Máx / min atingiu um ponto particularmente ruim no que diz respeito ao uso de nomes de métodos comuns em um traço comum. O mesmo não precisa se aplicar à braçadeira.

Eu ainda quero dizer sim, mas @sfackler , nós realmente temos que adicionar métodos em uma característica que é tão comumente implementada, por diversos tipos? Precisamos ter cuidado ao adicionar à API de todos os tipos que aderiram a uma característica existente.

Com a especialização chegando, não perdemos nada colocando métodos de extensão em um traço de extensão.

Uma parte irritante é que, se o novo método std quebrar seu código: ele aparecerá muito antes de você poder realmente usá-lo, pois é instável. Fora isso, não é tão ruim se o conflito for com um método que tem o mesmo significado.

Acho que dar um nome diferente a essa função para evitar quebras é uma solução ruim. Enquanto funciona, ele está otimizando, não quebrando alguns engradados (todos optando pelo noturno), em vez de otimizar para legibilidade futura de qualquer código que use esse recurso.

Tenho algumas preocupações com as quais algumas não são preocupantes.

  • o nome e o sombreamento não são ideais, mas funcionam
  • para vetores e matrizes numéricos, acho que max / min / clamp não são ideais, mas isso é resolvido por não usar Ord. Ndarray gostaria de fazer clamps de argumento elementwise e genérico (escalar ou array), mas Ord não é usado por nós ou por bibliotecas semelhantes. Portanto, não se preocupe.
  • Tipos de compostos existentes que não são numéricos: BtreeMap obterá um clamp de método com esta mudança. Isso faz sentido em geral? Ele pode implementar um significado razoável para ele além do padrão?
  • o modo de chamada por valor não se ajusta a todas as implementações. Novamente, BtreeMap. O clamp deve consumir 3 mapas e retornar um deles?

tipos compostos

Acho que faz tanto sentido quanto BtreeSet<BtreeSet<impl Ord>>::range . Mas há casos específicos que podem até ser úteis, como Vec<char> .

modo de chamada por valor

Quando isso apareceu na RFC, a resposta foi apenas usar o Cow .

Claro, poderia ser algo assim , para reutilizar o armazenamento:

    fn clamp<T>(mut self, low: &T, high: &T) -> Self
        where T: ?Sized + ToOwned<Owned=Self> + Ord, Self : Borrow<T>
    {
        assert!(low <= high);
        if self.borrow() < &low {
            low.clone_into(&mut self);
        } else if self.borrow() >= &high {
            high.clone_into(&mut self);
        }
        self
    }

Qual https://github.com/rust-lang/rfcs/pull/2111 pode tornar ergonômico chamar.

A equipe de libs discutiu isso durante a triagem alguns dias atrás e a conclusão foi que deveríamos fazer uma corrida de cratera para ver qual é a quebra no ecossistema para essa mudança. Os resultados disso determinariam que ação deveria ser tomada precisamente sobre esta questão.

Há uma série de recursos de linguagem futuros possíveis que poderíamos adicionar para facilitar a adição de apis como este, como características de baixa prioridade ou usando características de extensão de uma maneira mais saborosa. Não queremos necessariamente bloquear isso em avanços como esses, no entanto.

Já aconteceu uma corrida de cratera para este recurso?

Eu pretendo reviver o método clamp() depois que # 48552 for mesclado. No entanto, RangeInclusive vai ser estabilizado antes disso, o que significa que a alternativa baseada em intervalo agora é viável para consideração (que é na verdade a proposta original, mas retirada porque ..= era muito instável 😄):

// Current
trait Ord {
    fn clamp(self, min: Self, max: Self) -> Self { ... }
}
assert_eq!(9.clamp(6, 7), 7);


// Alternative
trait Ord {
    fn clamp(self, range: RangeInclusive<Self>) -> Self { ... }
}
assert_eq!(9.clamp(6..=7), 7);

Um RangeInclusive estável também abre outras possibilidades, como inverter as coisas (o que permite algumas possibilidades interessantes com o autoref e evita as colisões de nomes por completo):

impl<T: Ord + Clone> RangeInclusive<T> {
    fn clamp(&self, mut x: T) -> T {
        if x < self.start { x.clone_from(&self.start); }
        else if x > self.end { x.clone_from(&self.end); }
        x
    } 
} 

    assert_eq!((1..=10).clamp(11), 10);

    let strings = String::from("aa")..=String::from("b");
    assert_eq!(strings.clamp(String::from("a")), "aa");
    assert_eq!(strings.clamp(String::from("aaa")), "aaa");

https://play.rust-lang.org/?gist=38def79ba2f3f8380197918377dc66f5&version=nightly

Ainda não decidi se acho isso melhor ...

Eu usaria um nome diferente se usado como método de intervalo.

Certamente eu adoraria ter o recurso mais cedo ou mais tarde, não importa a forma.

Qual é o status atual?
Parece-me que existe um consenso de que adicionar uma pinça ao RangeInclusive pode ser uma alternativa melhor.
Alguém precisa escrever um RFC?

Provavelmente, um RFC completo não é necessário neste momento. Apenas uma decisão de qual grafia escolher:

  1. value.clamp(min, max) (siga o RFC no estado em que se encontra)
  2. value.clamp(min..=max)
  3. (min..=max).clamp(value)

A opção 2 ou 3 permitiria uma fixação parcial mais fácil. Você poderia fazer value.clamp(min..) ou value.clamp(..=max) , sem a necessidade de métodos clamp_to_start ou clamp_to_end especiais.

@egilburg : já temos esses métodos especiais: clamp_to_start é max e clamp_to_end é min : wink:

A consistência é boa.

@egilburg Rust não suporta sobrecarga direta. Para que a opção 2 funcione com sua sugestão, precisaremos implementar um novo traço para RangeInclusive , RangeToInclusive e RangeFrom , que parecem muito pesados.

Eu acho que a opção 3 é a melhor opção.

1 ou 2 são os menos surpreendentes. Eu ficaria com 1, já que muito código teria menos a fazer para substituir a implementação local pela padrão.

Acho que devemos planejar usar _todos_ os tipos de intervalo * ou _nenhum_ deles.

Claro, isso é mais difícil para coisas como Range que para RangeInclusive . Mas há algo bom sobre (0.0..1.0).clamp(2.0_f32) => 0.99999994_f32 .

@kennytm Então, se eu abrisse uma solicitação pull com a opção 3, você acha que ela seria mesclada?
Ou o que você acha sobre como proceder a seguir?

@EdorianDark Para isso, precisamos perguntar a @ rust-lang / libs 😃

Eu pessoalmente gosto da opção 2, com RangeInclusive apenas. Conforme mencionado, a "fixação parcial" já existe com min e max .

Concordo com @SimonSapin , embora também concorde com a opção 1. Com a opção 3, provavelmente não usaria a função porque me parece ao contrário. Nas outras linguagens / bibliotecas com clamp que @kennytm pesquisou anteriormente , 5 de 7 (todos exceto Swift e Qt) têm o valor primeiro, depois o intervalo.

A braçadeira está agora no modo master novamente!

Estou satisfeito, embora ainda esteja tentando descobrir o que mudou que tornou isso aceitável agora, enquanto não estava em # 44097

Agora temos um período de aviso devido ao # 48552, em vez de interromper a inferência instantaneamente antes mesmo da estabilização.

Ótima notícia, obrigado!

@kennytm Eu só quero agradecer a você pelo trabalho braçal que você fez para fazer o # 48552 acontecer, e @EdorianDark pelo seu interesse e por implementá-lo. É maravilhoso ver isso finalmente fundido.

https://rust.godbolt.org/z/JmLWJi

pub fn clamped(a: f32) -> f32 {
   a.clamp(0.,255.)
}

Compila para:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm1, xmm0
  vmovss xmm1, dword ptr [rip + .LCPI0_0]
  vminss xmm0, xmm1, xmm0

o que não é tão ruim ( vmaxss e vminss são usados), mas:

pub fn maxmined(a: f32) -> f32 {
   (0f32).max(a).min(255.)
}

usa uma instrução a menos:

  vxorps xmm1, xmm1, xmm1
  vmaxss xmm0, xmm0, xmm1
  vminss xmm0, xmm0, dword ptr [rip + .LCPI1_0]

Isso é inerente à implementação do grampo ou apenas uma peculiaridade da otimização do LLVM?

@kornelski clamp ing a NAN deve preservar aquele NAN , o que maxmined não preserva, porque max / min preserva o _non_- NAN .

Seria ótimo encontrar uma implementação que atendesse às expectativas da NAN e fosse mais curta. E seria bom para os doctests mostrar o manuseio de NAN. Parece que o PR original tinha alguns:

https://github.com/rust-lang/rust/blob/b762283e57ff71f6763effb9cfc7fc0c7967b6b0/src/libstd/f32.rs#L1089 -L1094

Por que os flutuadores de fixação entram em pânico se min ou max é NaN? Eu mudaria a afirmação de assert!(min <= max) para assert!(!(min > max)) , de forma que um mínimo ou máximo de NaN não tivesse efeito, assim como nos métodos max e min.

NAN para min ou max no clamp é provavelmente indicativo de um erro de programação, e concluímos que era melhor entrar em pânico antes do que possivelmente enviar dados não fixados para o IO. Se você não quer um limite superior ou inferior, esta função não é para você.

Você sempre pode usar INF e -INF se não quiser um limite superior ou inferior, certo? O que também faz sentido matemático, ao contrário do NaN. Mas na maioria das vezes é melhor usar max e min para isso.

@Xaeroxe Obrigado pela implementação.

Talvez isso possa ir na próxima edição, se quebrar o código estável?

Uma coisa que IMO vale a pena considerar com mais detalhes é a fixação unilateral de f32 / f64 . A discussão parece ter tocado neste tópico brevemente, mas não o considerado em detalhes.

Na maioria dos casos, se a entrada para um grampo unilateral for NAN, é mais útil que o resultado seja NAN do que o resultado seja o limite de fixação. Portanto, as funções f32::min e f64::max não funcionam bem para este caso de uso. Precisamos de função (ões) separada (s) para fixação unilateral. (Veja rust-num / num-traits # 122.)

O motivo pelo qual trago isso à tona é que isso afeta o projeto dos dois lados clamp , uma vez que seria bom que os grampos de dois lados e um lado tivessem uma interface consistente. Algumas opções são:

  1. input.clamp(min, max) , input.clamp_min(min) e input.clamp_max(max)
  2. input.clamp(min..=max) , input.clamp(min..) , input.clamp(..=max)
  3. input.clamp(min, max) , input.clamp(min, std::f64::INFINITY) , input.clamp(std::f64::NEG_INFINITY, max)

Com a implementação atual ( min e max como parâmetros f32 / f64 separados), teríamos que escolher a opção 1, que acho perfeitamente razoável , ou opção 3, que é muito prolixo. Apenas devemos estar cientes de que o sacrifício é ter que adicionar funções clamp_min e clamp_max separadas ou exigir que o usuário escreva infinito positivo / negativo.

Também é importante notar que podemos fornecer

impl f32 {
    pub fn clamp<T>(self, bounds: T) -> f32
    where
        T: RangeBounds<f32>,
    {
         // ...
    }
}

// and for f64

já que para f32 / f64 nós realmente sabemos como lidar com limites exclusivos, ao contrário do geral Ord . Claro, então provavelmente desejaríamos mudar Ord::clamp para obter um argumento RangeInclusive para consistência. Parece que não havia uma opinião forte sobre se preferir dois argumentos ou um único argumento RangeInclusive para Ord::clamp .

Se este já for um problema resolvido, sinta-se à vontade para ignorar meu comentário. Eu só queria trazer essas coisas à tona porque não as vi na discussão anterior.

Triagem: as APIs abaixo atualmente são instáveis ​​e apontam aqui. Existem questões a serem consideradas além do manuseio de NaN? Vale a pena estabilizar Ord::clamp primeiro sem bloqueá-lo no manuseio de NaN?

`` `ferrugem
Pub trait Ord: Eq + PartialOrd{
//…
fn clamp (self, min: Self, max: Self) -> Self onde Self: Sized {...}
}
impl f32 {
pinça pub fn (próprio, mín .: f32, máx .: f32) -> f32 {…}
}
impl f64 {
pub fn clamp (próprio, mín .: f64, máx .: f64) -> f64 {…}
}

@SimonSapin eu ficaria feliz em estabilizar a coisa toda pessoalmente

+1, isso passou por um RFC completo e não acho que haja nada de material que surgiu desde então. Por exemplo, o tratamento de NaN surgiu em detalhes no IRLO e na discussão RFC .

Tudo bem, isso parece justo.

@rfcbot fcp merge

O membro da equipe @SimonSapin propôs fundir isso. A próxima etapa é a revisão pelo restante dos membros da equipe marcados:

  • [x] @Amanieu
  • [] @Kimundi
  • [x] @SimonSapin
  • [x] @alexcrichton
  • [x] @dtolnay
  • [] @sfackler
  • [] @withoutboats

Nenhuma preocupação listada atualmente.

Assim que a maioria dos revisores aprovar (e no máximo 2 aprovações pendentes), isso entrará em seu período final para comentários. Se você identificar uma questão importante que não foi levantada em qualquer ponto deste processo, fale!

Consulte este documento para obter informações sobre quais comandos os membros da equipe marcados podem me dar.

Foi tomada uma decisão sobre x.clamp(7..=13) vs x.clamp(7, 13) ? https://github.com/rust-lang/rust/issues/44095#issuecomment -533764997 menciona que o primeiro pode ser melhor para consistência com um futuro potencial f64::clamp .

Eu diria que é uma resolução bastante infeliz, já que .min e .max freqüentemente causam bugs, pois você usa .min(...) para especificar o limite superior e .max(...) para especificar o limite inferior. Isso é incrivelmente confuso e já vi muitos bugs nisso. .clamp(..1.0) e .clamp(0.0..) são muito mais claros.

@CryZe mostra um ponto muito bom: mesmo que você nunca cometa um erro com min = limite superior, max = limite inferior , você ainda precisa fazer ginástica mental para lembrar qual usar. Essa carga cognitiva seria mais bem gasta em qualquer problema que você esteja tentando resolver.

Eu sei que x.clamp(y, z) é mais esperado, mas talvez esta seja uma oportunidade para inovar;)

Eu experimentei um pouco com intervalos nos estágios iniciais e até adiei o RFC por vários meses para que pudéssemos experimentar intervalos inclusivos. (Isso foi iniciado antes de serem estabilizados)

Eu descobri que não era possível implementar clamp para faixas exclusivas em números de ponto flutuante. Suportar apenas alguns tipos de intervalo, mas não outros, foi um resultado muito surpreendente, então, embora eu tenha atrasado o RFC vários meses para experimentá-lo dessa forma, no final decidi que intervalos não eram a solução.

@ m-ou-se Veja a discussão começando com # 44095 (comentário) e também # 58710 (revisão).

Editar: conforme apontado abaixo, a discussão na solicitação pull (# 58710) contém mais discussão sobre a decisão de design do que o problema de rastreamento. É uma pena que isso não tenha sido comunicado aqui, que é onde as discussões de design normalmente acontecem, mas foi discutido.

Suportar apenas alguns tipos de intervalo, mas não outros, foi um resultado muito surpreendente

Rust já trata alguns intervalos de maneira diferente de outros (por exemplo, usando-os para iteração), então permitir apenas alguns intervalos como argumentos clamp não me parece nada surpreendente.

Aqui está a análise mais útil disso: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351

@Xaeroxe Suportar apenas alguns tipos de intervalo, mas não outros, foi um resultado muito surpreendente

Se você estava pensando sobre isso antes de eles se estabilizarem, o tempo e o uso geral mudaram sua opinião ou você acha que ainda é o caso?

Eu diria que intervalos exclusivos nunca devem ser implementados para flutuantes de qualquer maneira, porque eles teriam comportamento diferente para inteiros (o intervalo 0..10 inclui o limite inferior e exclui o limite superior, então por que deveria o intervalo hipotético 0.0...10.0 excluir ambos?). Não acho que seria surpreendente, pelo menos para mim.

@varkor Mas isso foi alterado depois de um único comentário na revisão, sem qualquer discussão sobre o problema de rastreamento.

Isso pode soar como um confronto excessivo, tente algo como "quando eu examinei a conversa, não encontrei um argumento convincente sobre por que não deveríamos usar intervalos, alguém pode me indicar isso?".

Suspeito que o argumento que você está procurando está aqui: https://github.com/rust-lang/rfcs/pull/1961#issuecomment -302600351

EDIT @Xaeroxe antes de mim :)

o tempo e o uso geral mudaram sua opinião

Até agora não foi, mas intervalos são algo que uso com pouca frequência em minha codificação diária. Estou aberto a ser persuadido por exemplos de código e APIs existentes com suporte de intervalo parcial. No entanto, mesmo se resolvermos essa questão, ainda existem vários outros pontos excelentes que scottmcm traz à tona no comentário da RFC que precisam ser abordados. Por exemplo, Step não é implementado em tantos tipos quanto Ord , esta pequena alteração sintática vale a pena perder esses tipos? Além disso, há um caso de uso para grampos de alcance não inclusivos? Pelo que eu posso dizer, nenhuma outra linguagem ou estrutura sentiu a necessidade de oferecer suporte a clamp de alcance exclusivo, então que benefício ganhamos com isso? Os intervalos eram muito mais difíceis de implementar de maneira satisfatória e apresentavam muitas desvantagens e poucos benefícios.

Se eu fosse implementar isso usando intervalos, ficaria assim.

Portanto, há alguns motivos pelos quais não devemos adotar essa abordagem.

  1. A seleção de intervalos necessários é nova o suficiente para exigir uma nova característica e exclui especificamente o intervalo mais comum, Range .

  2. Já estamos tão adiantados no processo de RFC e a única coisa que o padrão ganha com isso é outra maneira de escrever .max() ou .min() . Eu realmente não quero retroceder o RFC para o início do processo, a fim de implementar algo que já podemos fazer no Rust.

  3. Ele dobra a quantidade de ramificações que acontecem na função para acomodar um caso de uso que ainda não temos certeza se existe. Não consigo fazer com que isso apareça nos benchmarks.

Necessidade de operações de fixação unilateral

... a única coisa que std ganha com isso é outra maneira de escrever .max() ou .min() .

O ponto principal que eu estava tentando fazer é que eu vi esta aparente equivalência entre .min() / .max() e grampos unilaterais surgindo várias vezes na discussão, mas as operações não são NAN .

Por exemplo, considere input.max(0.) como uma expressão para fixar números negativos a zero. Se input não for NAN , funciona bem. No entanto, quando input é NAN , ele é avaliado como 0. . Quase nunca é o comportamento desejado; a fixação unilateral deve preservar os valores de NAN . (Veja este comentário e este comentário .) Em conclusão, .max() funciona bem para pegar o maior dos dois números, mas não funciona bem para fixação unilateral.

Portanto, precisamos de operações de fixação unilateral (separadas de .min() / .max() ) para números de ponto flutuante. Outros apresentaram bons argumentos para a utilidade das operações de fixação unilateral para tipos de ponto não flutuante também. A próxima pergunta é como queremos expressar essas operações.

Como expressar operações de fixação unilateral

.clamp() com INFINITY

Em outras palavras, não adicione uma operação de fixação unilateral; apenas diga aos usuários para usar .clamp() com limites de INFINITY ou NEG_INFINITY . Por exemplo, diga aos usuários para escrever input.clamp(0., std::f64::INFINITY) .

Isso é muito prolixo, o que levará os usuários a usar o .min() / .max() incorreto se não estiverem cientes das nuances do manuseio de NAN . Além disso, isso não ajuda para T: Ord e, IMO, é menos claro do que as alternativas.

.clamp_min() e .clamp_max()

Uma opção razoável é adicionar os métodos .clamp_min() e .clamp_max() , o que não exigiria nenhuma mudança na implementação proposta atualmente. Acho que essa é uma abordagem razoável; Eu só queria ter certeza de que estamos cientes de que teremos que usar essa abordagem se estabilizarmos a implementação atualmente proposta de clamp .

Argumento de alcance

Outra opção é fazer clamp usar um argumento de intervalo. @Xaeroxe mostrou uma maneira de implementar isso, mas essa implementação tem algumas desvantagens, como ele mencionou. Uma maneira alternativa de escrever a implementação é semelhante à maneira como o fatiamento é implementado atualmente (o traço SliceIndex ). Isso resolve todas as objeções que vi na discussão, exceto a preocupação em fornecer implementações para um subconjunto de tipos de intervalo e a complexidade adicional. Eu concordo que adiciona alguma complexidade, mas IMO não é muito pior do que adicionar .clamp_min() / .clamp_max() . Por Ord , sugiro algo assim:

pub trait Ord: Eq + PartialOrd<Self> {
    // ...

    fn clamp<B>(self, bounds: B) -> B::Output
    where
        B: Clamp<Self>,
    {
        bounds.clamp(self)
    }
}

pub trait Clamp<T> {
    type Output;
    fn clamp(self, input: T) -> Self::Output;
}

impl<T> Clamp<T> for RangeFull {
    type Output = T;
    fn clamp(self, input: T) -> T {
        input
    }
}

impl<T: Ord> Clamp<T> for RangeFrom<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input < self.start {
            self.start
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeToInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        if input > self.end {
            self.end
        } else {
            input
        }
    }
}

impl<T: Ord> Clamp<T> for RangeInclusive<T> {
    type Output = T;
    fn clamp(self, input: T) -> T {
        assert!(self.start <= self.end);
        let mut x = input;
        if x < self.start { x = self.start; }
        if x > self.end { x = self.end; }
        x
    }
}

Algumas reflexões sobre isso:

  • Poderíamos adicionar implementações para intervalos exclusivos onde T: Ord + Step .
  • Poderíamos manter o traço Clamp apenas todas as noites, semelhante ao traço SliceIndex .
  • Para apoiar f32 / f64 , poderíamos

    1. Relaxe as implementações para T: PartialOrd . (Não sei por que a implementação atual de clamp está em Ord vez de PartialOrd . Talvez eu tenha perdido algo na discussão? Parece que PartialOrd seria suficiente.)

    2. ou escreva implementações especificamente para f32 e f64 . (Se desejar, podemos sempre mudar para a opção i mais tarde, sem uma alteração significativa.)

    e então adicionar

    impl f32 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
    impl f64 {
      // ...
      fn clamp<B>(self, bounds: B) -> B::Output
      where
          B: Clamp<Self>,
      {
          bounds.clamp(self)
      }
    }
    
  • Poderíamos implementar Clamp para intervalos exclusivos com f32 / f64 mais tarde, se desejado. ( @scottmcm comentou que isso não é simples porque std intencionalmente não tem f32 / f64 operações predecessoras. Não sei por que std não tem essas operações; talvez problemas com números desordenados? De qualquer forma, isso pode ser resolvido mais tarde.)

    Mesmo se não adicionarmos implementações de Clamp para intervalos exclusivos com f32 / f64 , discordo que isso seria muito surpreendente. Como @varkor aponta, Rust já trata vários tipos de intervalo de maneira diferente com o propósito de Copy e Iterator / IntoIterator . (IMO, esta é uma verruga de std , mas é pelo menos uma instância em que os tipos de intervalo são tratados de forma diferente.) Além disso, se alguém tentasse usar um intervalo exclusivo, a mensagem de erro seria fácil de entender ( "o traço vinculado a std::ops::Range<f32>: Clamp<f32> não está satisfeito").

  • Eu fiz Output um tipo associado para flexibilidade máxima para adicionar mais implementações no futuro, mas isso não é estritamente necessário.

Basicamente, essa abordagem nos permite a flexibilidade que desejamos com relação aos limites das características. Também torna possível começar com um conjunto minimamente útil de Clamp implementações e adicionar mais implementações posteriormente sem interromper as alterações.

Comparando as opções

A abordagem "use .clamp() com INFINITY " tem desvantagens substanciais, conforme mencionado acima.

A abordagem " .clamp " + .clamp_min() + .clamp_max() tem as seguintes desvantagens:

  • É mais detalhado, por exemplo, input.clamp_min(0) vez de input.clamp(0..) .
  • Ele não oferece suporte a intervalos exclusivos.
  • Não podemos adicionar mais implementações de .clamp() no futuro (sem adicionar ainda mais métodos). Por exemplo, não podemos apoiar a fixação de um u32 com limites u8 , que é um recurso solicitado na discussão RFC . Esse exemplo particular pode ser melhor tratado com uma função .saturating_into() , mas pode haver outros exemplos onde mais implementações de fixação seriam úteis.
  • Alguém pode ficar confuso entre .min() , .max() , .clamp_min() e .clamp_max() para fixação unilateral. (Fixar com .clamp_min() é semelhante a usar .max() , e fixar com .clamp_max() é semelhante a usar .min() .) Podemos evitar esse problema principalmente nomeando o operações de fixação unilateral .clamp_lower() / .clamp_upper() ou .clamp_to_start() / .clamp_to_end() vez de .clamp_min() / .clamp_max() , embora isso seja ainda mais detalhado ( input.clamp_lower(0) versus input.clamp(0..) ).

A abordagem do argumento de alcance tem as seguintes desvantagens:

  • A implementação é mais complexa do que adicionar .clamp_min() / .clamp_max() .
  • Se decidirmos não implementar ou não puder implementar Clamp para os tipos de intervalo exclusivos, isso pode ser surpreendente.

Não tenho uma opinião forte sobre a abordagem " .clamp " + .clamp_min() + .clamp_max() +

@Xaeroxe Ele dobra a quantidade de ramificações que acontecem na função para acomodar um caso de uso que ainda não temos certeza se existe. Não consigo fazer com que isso apareça nos benchmarks.

Talvez o branch extra seja otimizado pelo LLVM?

Na fixação unilateral

Como a fixação é inclusiva em ambos os lados, pode-se apenas especificar o mín. / Máx. À esquerda / direita para obter o comportamento de fixação apenas em um lado. Acho que isso é perfeitamente aceitável e indiscutivelmente melhor do que .clamp((Bound::Unbounded, Inclusive(3.2))) onde não há um tipo Range* que se encaixe de qualquer maneira:

x.clamp(i32::MIN, 10);
x.clamp(-f32::INFINITY, 10.0);

Não há perda de desempenho, já que o LLVM é trivialmente capaz de otimizar o lado morto: https://rust.godbolt.org/z/l_uBLO

A sintaxe de intervalo seria legal, mas clamp é básica o suficiente para que dois argumentos separados sejam bons e fáceis de entender.

Talvez o tratamento de min / max NaN possa ser corrigido por si mesmo, por exemplo, alterando a implementação dos métodos inerentes de f32 ? Ou se especializando em PartialOrd::min/max ? (com um sinalizador de edição, supondo que Rust consiga encontrar uma maneira de alternar as coisas na libstd).

@scottmcm você deve verificar RangeToInclusive .

Depois de ponderar mais um pouco, ocorreu-me que a estabilidade é para sempre, então não devemos considerar "redefinir o processo RFC" como um motivo para não fazer uma alteração.

Para esse fim, quero voltar à mentalidade que tive ao implementar isso. O Clamp opera conceitualmente em uma faixa, então faz sentido usar o vocabulário que Rust já possui para expressar faixas. Essa foi minha reação automática, e parece ser a reação de muitas outras pessoas. Portanto, vamos reiterar os argumentos para não fazer isso dessa maneira e ver se podemos refutá-los.

  • A seleção de intervalos necessários é nova o suficiente para exigir uma nova característica e exclui especificamente o intervalo mais comum, Range .

    • Usando a nova implementação fornecida por @ jturner314 , agora temos a capacidade de adicionar mais limitações em tipos Range* específicos, como Ord + Step , a fim de retornar valores corretamente para intervalos exclusivos. Portanto, embora o grampo de alcance exclusivo muitas vezes não seja realmente necessário, podemos aceitar toda a gama de intervalos aqui, sem comprometer a interface de intervalos que não têm essas limitações técnicas.
  • Podemos apenas usar Infinity / Min / Max para fixação unilateral.

    • Isso é verdade, e grande parte do motivo pelo qual essa mudança não é realmente um mandato forte, na minha opinião. Na verdade, só tenho uma resposta para isso: a sintaxe Range* envolve menos caracteres e menos importações para esse caso de uso.

Agora que refutamos os motivos para não fazer isso, este comentário carece de qualquer tipo de motivação para fazer a mudança, pois as opções parecem equivalentes. Vamos encontrar alguma motivação para fazer a mudança. Eu só tenho um motivo, que é que a opinião geral neste tópico parece ser que a abordagem baseada em intervalo melhora a semântica da linguagem. Não apenas para o grampo de alcance duplo inclusivo, mas também para funções como .min() e .max() .

Estou curioso para saber se essa linha de pensamento tem alguma força com outros que são a favor de estabilizar a RFC como está.

Acho que seria melhor deixar o Clamp na forma atual, porque agora é muito semelhante a outras linguagens.
Quando trabalhei em minha solicitação pull # 58710, tentei usar uma implementação baseada em intervalo.
Mas rust-lang / rfcs # 1961 (comentário) ) me convenceu de que é melhor na forma padrão.

Acho que seria lógico ter um atributo #[must_use] na função, para não confundir as pessoas que não estão acostumadas com o funcionamento dos números da ferrugem. Ou seja, eu poderia facilmente perceber alguém escrevendo o seguinte código (incorreto):

let mut x: f64 = some_number_source();
x.clamp(0.0, 1.0);
//Proceeds to assume that 0.0 <= x <= 1.0

Em geral, a ferrugem adota uma abordagem (number).method() para números (enquanto outras linguagens usam Math.Method(number) ), mas mesmo tendo isso em mente, seria uma suposição lógica que isso poderia modificar number . Isso é mais uma questão de qualidade de vida do que qualquer outra coisa.

O atributo [must_use] foi adicionado recentemente .
@ Xaeroxe Você veio com algo para braçadeira baseada em alcance?
Acho que a função como está agora se encaixaria melhor nas outras funções numéricas da ferrugem e gostaria de começar a estabilizá-la novamente.

No momento, não vejo nenhuma razão para escolher uma pinça baseada em faixa. Sim, vamos adicionar o atributo must_use e trabalhar para a estabilização.

@SimonSapin @scottmcm Podemos reiniciar o processo de estabilização?

Como @ jturner314 disse, seria ótimo ter um grampo em PartialOrd, em vez de Ord, então ele também pode ser usado em flutuadores.

Já temos os f32::clamp e f64::clamp nesta edição.

Isso é o que estou tentando fazer:

use num_traits::float::FloatCore;

struct Foo<T> (T);

impl<T: FloatCore> Foo<T> {
    fn foo(&self) -> T {
        self.0.clamp(1, 10)
    }
}

fn main() {
    let foo = Foo(15.3);
    println!("{}", foo.foo())
}

Link para o playground.

PartialOrd não é um traço somente float. Ter um método específico de float não torna o grampo disponível para tipos PartialOrd .

A implementação atual requer Eq , embora não o use.

A principal preocupação com PartialOrd era que ele fornece garantias mais fracas, o que por sua vez enfraquece as garantias de grampo. Os usuários que desejam isso em PartialOrd podem estar interessados ​​em outra função que escrevi https://docs.rs/num/0.2.1/num/fn.clamp.html

Quais são essas garantias?

Uma expectativa bastante natural é que iff x.clamp(a, b) == x então a <= x && x <= b . Isso não é garantido com PartialCmp onde x pode ser incomparável com a ou b .

Vim aqui hoje procurando por clamp() vagamente lembrados e li a discussão com interesse.

Eu sugeriria usar o "truque da opção" como um meio-termo entre permitir intervalos arbitrários e ter várias funções nomeadas. Eu sei que isso não é popular entre alguns, mas parece capturar a semântica desejada muito bem aqui:

#![allow(unstable_name_collisions)]

pub trait Clamp: Sized {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>;
}

impl Clamp for f32 {
    fn clamp<L, U>(self, lower: L, upper: U) -> Self
    where
        L: Into<Option<Self>>,
        U: Into<Option<Self>>,
    {
        let below = match lower.into() {
            None => self,
            Some(lower) => self.max(lower),
        };
        match upper.into() {
            None => below,
            Some(upper) => below.min(upper),
        }
    }
}

#[test]
fn test_clamp() {
    assert_eq!(1.0, f32::clamp(2.0, -1.0, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, 1.0));
    assert_eq!(1.0, f32::clamp(2.0, None, 1.0));
    assert_eq!(-1.0, f32::clamp(-2.0, -1.0, None));
    assert_eq!(2.0, f32::clamp(2.0, -1.0, None));
    assert_eq!(-2.0, f32::clamp(-2.0, None, 1.0));
}

Se isso fosse incluído em std uma implementação geral também poderia ser incluída para T: Ord , que cobriria as preocupações levantadas sobre uma implementação geral de PartialOrd .

Dado que definir uma função clamp() no código do usuário atualmente gera um aviso do compilador sobre colisões de nomes instáveis ​​por padrão, acho que o nome "clamp" é adequado para esta função.

Eu acho que clamp(a,b,c) deve se comportar da mesma forma que min(max(a,b), c) .
Visto que max e min não são implementados para PartialOrd nem deveriam clamp .
O problema com NaN já foi discutido .

@EdorianDark eu concordo. min, max também deve exigir apenas PartialOrd.

@noonien min e max são definidos desde Rust 1.0 e requerem Ord e têm uma definição para f32 e f64 .
Este não é o lugar certo para discutir essas funções.
Aqui, podemos apenas cuidar para que min , max e clamp se comportem de forma comparável e não surpreendente.
Edit: Eu não gosto da situação com PartialOrd e preferia que float implementasse Ord , mas isso não é mais possível mudar depois de 1.0.

Ele está mesclado e instável há cerca de um ano e meio. Como nos sentimos em relação a estabilizar isso?

Eu adoraria estabilizar isso!

Se clamp conflito de nome de método parece um problema, sugeri alterar a resolução de nome em um ponto em https://github.com/rust-lang/rust/pull/66852#issuecomment -561667812, e isso ajudaria com isso também.

@Xaeroxe Eu acho que o processo é enviar o PR de estabilização e pedir o consenso da equipe de libs sobre isso. Parece que o t-libs está sobrecarregado e não consegue acompanhar as coisas não fcped.

@matklad na verdade, uma proposta FCP já começou no ano passado em https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395, mas está travada porque há uma caixa de seleção restante.

Nesse caso, acho que receber um ping uma vez por ano sobre um problema é bastante tolerável.

@Kimundi
@sfackler
@withoutboats

https://github.com/rust-lang/rust/issues/44095#issuecomment -544393395 ainda está aguardando sua atenção

A equipe de libs mudou bastante desde que o FCP foi iniciado. O que vocês acham de apenas começar um novo FCP no PR de estabilização? Parece que não deve demorar mais do que esperar pelas caixas de seleção restantes aqui.

@LukasKalbertodt tudo bem por mim, você se importaria de começar?

Cancelando o FCP aqui, porque aquele FCP agora aconteceu no PR de estabilização: https://github.com/rust-lang/rust/pull/77872#issuecomment -722982535

@fcpbot cancel

Uh

@rfcbot cancel

@ m-ou-se proposta cancelada.

Esta página foi útil?
0 / 5 - 0 avaliações