Qual é o benefício de ter o operador de atribuição retornando um valor?


27

Estou desenvolvendo uma linguagem que pretendo substituir Javascript e PHP. (Não vejo nenhum problema com isso. Não é como se um desses idiomas tivesse uma grande base de instalação.)

Uma das coisas que eu queria mudar era transformar o operador de atribuição em um comando de atribuição, removendo a capacidade de fazer uso do valor retornado.

x=1;          /* Assignment. */
if (x==1) {}  /* Comparison. */
x==1;         /* Error or warning, I've not decided yet. */
if (x=1) {}   /* Error. */

Eu sei que isso significaria que aquelas funções de linha única que as pessoas C amam tanto não funcionariam mais. Imaginei (com poucas evidências além da minha experiência pessoal) que a grande maioria das vezes que isso acontecia, era realmente uma operação de comparação.

Ou é? Existem usos práticos do valor de retorno do operador de atribuição que não puderam ser reescritos trivialmente? (Para qualquer idioma que tenha esse conceito.)


12
JS e PHP não têm uma grande "base de instalação"?
MHR

47
@ Mri Eu suspeito de sarcasmo.
Andy Hunt

12
O único caso útil que me lembro é while((x = getValue()) != null) {}. As substituições serão mais feias, pois você precisará usar breakou repetir a x = getValuetarefa.
CodesInChaos

12
@mri Ooh não, eu ouvi dizer que essas duas línguas são coisas triviais sem nenhum investimento significativo. Uma vez que as poucas pessoas que insistem em usar JS vejam minha linguagem, elas mudarão para a minha e nunca mais precisarão escrever === novamente. Estou igualmente certo de que os fabricantes de navegadores lançarão imediatamente uma atualização que inclui meu idioma ao lado de JS. :)
billpg

4
Eu sugeriria que, se sua intenção é aprimorar um idioma existente e você pretender que ele seja amplamente adotado, é 100% compatível com o idioma existente. Veja o TypeScript como um exemplo. Se sua intenção é fornecer uma alternativa melhor a um idioma existente, você terá um problema muito mais difícil. Um novo idioma precisa resolver um problema realista existente muito melhor do que o idioma existente para pagar pelo custo da troca. Aprender um idioma é um investimento e precisa ser recompensado.
Eric Lippert

Respostas:


25

Tecnicamente, vale a pena manter um pouco de açúcar sintático, mesmo que possa ser substituído trivialmente, se melhorar a legibilidade de alguma operação comum. Mas a atribuição como expressão não se enquadra nisso. O perigo de digitá-lo no lugar de uma comparação significa que raramente é usado (às vezes até proibido por guias de estilo) e provoca uma visão dupla sempre que é usado. Em outras palavras, os benefícios de legibilidade são pequenos em número e magnitude.

Uma olhada nos idiomas existentes que fazem isso pode valer a pena.

  • Java e C # mantêm uma expressão de atribuição, mas removem a armadilha que você menciona, exigindo condições para avaliar os booleanos. Isso geralmente parece funcionar bem, embora as pessoas se queixem ocasionalmente de que isso não permite condições como if (x)no lugar if (x != null)ou if (x != 0)dependendo do tipo de x.
  • Python faz da atribuição uma declaração adequada em vez de uma expressão. As propostas para alterar isso ocasionalmente chegam à lista de discussão python-ideas, mas minha impressão subjetiva é que isso acontece mais raramente e gera menos ruído a cada vez em comparação com outros recursos "ausentes", como loops do-while, instruções de switch, lambdas de várias linhas, etc.

No entanto, o Python permite que um caso especial, atribuindo a vários nomes de uma só vez: a = b = c. Esta é considerada uma declaração equivalente a b = c; a = b, e é ocasionalmente usada, por isso pode valer a pena adicionar ao seu idioma também (mas eu não me preocuparia, pois essa adição deve ser compatível com versões anteriores).


5
+1 por apresentar a = b = cque as outras respostas não trazem realmente.
Leo

6
Uma terceira resolução é usar um símbolo diferente para atribuição. Pascal usa :=para atribuição.
Brian

5
@ Brian: De fato, assim como o C #. =é atribuição, ==é comparação.
Marjan Venema

3
Em C #, algo como if (a = true)lançará um aviso C4706 ( The test value in a conditional expression was the result of an assignment.). O GCC com C também lançará a warning: suggest parentheses around assignment used as truth value [-Wparentheses]. Esses avisos podem ser silenciados com um conjunto extra de parênteses, mas eles existem para incentivar explicitamente a indicação de que a atribuição foi intencional.
Bob

2
@delnan Apenas um comentário bastante genérica, mas foi provocada por "remover a armadilha que você menciona, exigindo condições de avaliar a booleans" - a = true se avaliar a um valor booleano e, portanto, não é um erro, mas também levanta uma advertência relacionada em C #.
Bob

11

Existem usos práticos do valor de retorno do operador de atribuição que não puderam ser reescritos trivialmente?

De um modo geral, não. A idéia de ter o valor de uma expressão de atribuição ser o valor atribuído significa que temos uma expressão que pode ser usada tanto para seu efeito colateral quanto para seu valor , e que é considerada por muitos confusa.

Usos comuns costumam tornar as expressões compactas:

x = y = z;

possui a semântica em C # de "converta z para o tipo de y, atribua o valor convertido para y, o valor convertido é o valor da expressão, converta isso para o tipo de x, atribua para x".

Mas já estamos no campo dos efeitos colaterais impertinentes em um contexto de declaração, então há realmente muito pouco benefício convincente nisso

y = z;
x = y;

Da mesma forma com

M(x = 123);

sendo uma abreviação de

x = 123;
M(x);

Novamente, no código original, estamos usando uma expressão para seus efeitos colaterais e seu valor, e estamos fazendo uma declaração que tem dois efeitos colaterais em vez de um. Ambos são fedorentos; tente ter um efeito colateral por instrução e use expressões para seus valores, não para seus efeitos colaterais.

Estou desenvolvendo uma linguagem que pretendo substituir Javascript e PHP.

Se você realmente deseja ser ousado e enfatizar que a atribuição é uma declaração e não uma igualdade, meu conselho é: torne claramente uma declaração de atribuição .

let x be 1;

O vermelho. Ou

x <-- 1;

ou melhor ainda:

1 --> x;

Ou melhor ainda

1 → x;

Não há absolutamente nenhuma maneira com que alguém seja confundido x == 1.


1
O mundo está pronto para símbolos Unicode não ASCII em linguagens de programação?
billpg

Por mais que eu goste do que você sugere, um dos meus objetivos é que a maioria dos JavaScript "bem escritos" possa ser portada com pouca ou nenhuma modificação.
billpg

2
@ billpg: O mundo está pronto ? Não sei - o mundo estava pronto para o APL em 1964, décadas antes da invenção do Unicode? Aqui está um programa no APL que seleciona uma permutação aleatória de seis números dos primeiros 40: o x[⍋x←6?40] APL exigia seu próprio teclado especial, mas era uma linguagem bem-sucedida.
Eric Lippert

@bpgpg: o Macintosh Programmer's Workshop usava símbolos não ASCII para coisas como tags regex ou redirecionamento de stderr. Por outro lado, o MPW tinha a vantagem de o Macintosh facilitar a digitação de caracteres não ASCII. Devo confessar alguma perplexidade sobre o motivo pelo qual o driver de teclado dos EUA não fornece nenhum meio decente de digitar caracteres não ASCII. A entrada do número Alt não exige apenas a busca de códigos de caracteres - em muitos aplicativos, ela nem funciona.
supercat

Hum, por que alguém preferiria atribuir "à direita" como a+b*c --> x? Isso me parece estranho.
Ruslan

9

Muitas linguagens escolhem o caminho de tornar a atribuição uma declaração, e não uma expressão, incluindo Python:

foo = 42 # works
if foo = 42: print "hi" # dies
bar(foo = 42) # keyword arg

e Golang:

var foo int
foo = 42 # works
if foo = 42 { fmt.Printn("hi") } # dies

Outros idiomas não têm atribuição, mas ligações de escopo, por exemplo, OCaml:

let foo = 42 in
  if foo = 42 then
    print_string "hi"

No entanto, leté uma expressão em si.

A vantagem de permitir a atribuição é que podemos verificar diretamente o valor de retorno de uma função dentro do condicional, por exemplo, neste snippet Perl:

if (my $result = some_computation()) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}

O Perl também escopo a declaração apenas para essa condicional, o que a torna muito útil. Ele também avisará se você atribuir dentro de uma condicional sem declarar uma nova variável lá - if ($foo = $bar)avisará, if (my $foo = $bar)não avisará .

Fazer a atribuição em outra declaração geralmente é suficiente, mas pode trazer problemas de escopo:

my $result = some_computation()
if ($result) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}
# $result is still visible here - eek!

Golang depende muito dos valores de retorno para verificação de erros. Portanto, permite que um condicional leve uma instrução de inicialização:

if result, err := some_computation(); err != nil {
  fmt.Printf("Failed with %d", result)
}
fmt.Printf("We succeeded, and the result is %d\n", result)

Outros idiomas usam um sistema de tipos para proibir expressões não-booleanas dentro de um condicional:

int foo;
if (foo = bar()) // Java does not like this

É claro que isso falha ao usar uma função que retorna um booleano.

Vimos agora diferentes mecanismos de defesa contra atribuição acidental:

  • Proibir atribuição como expressão
  • Usar verificação de tipo estático
  • A atribuição não existe, temos apenas letligações
  • Permitir uma declaração de inicialização, não permitir atribuição de outra forma
  • Proibir atribuição dentro de um condicional sem declaração

Classifiquei-os em ordem crescente de preferência - atribuições dentro de expressões podem ser úteis (e é simples contornar os problemas do Python tendo uma sintaxe de declaração explícita e uma sintaxe de argumento nomeada diferente). Mas não há problema em impedi-los, pois há muitas outras opções para o mesmo efeito.

O código sem erros é mais importante que o código conciso.


+1 em "Proibir atribuição como expressão". Os casos de uso para atribuição como expressão não justificam o potencial de erros e problemas de legibilidade.
poke

7

Você disse: "Imaginei (com poucas evidências além da minha experiência pessoal) que a grande maioria das vezes que isso acontecia, era realmente uma operação de comparação".

Por que não corrigir o problema?

Em vez de = para atribuição e == para teste de igualdade, por que não usar: = para atribuição e = (ou mesmo ==) para igualdade?

Observar:

if (a=foo(bar)) {}  // obviously equality
if (a := foo(bar)) { do something with a } // obviously assignment

Se você quiser tornar mais difícil para o programador confundir atribuição com igualdade, torne mais difícil.

Ao mesmo tempo, se você REALMENTE quisesse corrigir o problema, removeria o crock C que afirmava que os booleanos eram apenas números inteiros com nomes simbólicos predefinidos de açúcar. Faça deles um tipo completamente diferente. Então, ao invés de dizer

int a = some_value();
if (a) {}

você força o programador a escrever:

int a = some_value();
if (a /= 0) {} // Note that /= means 'not equal'.  This is your Ada lesson for today.

O fato é que a atribuição como operador é uma construção muito útil. Não eliminamos as lâminas de barbear porque algumas pessoas se cortam. Em vez disso, o rei Gillette inventou o aparelho de barbear.


2
(1) :=para atribuição e =igualdade pode resolver esse problema, mas à custa de alienar todos os programadores que não cresceram usando um pequeno conjunto de linguagens não convencionais. (2) Outros tipos de bools permitidos em condições nem sempre são devidos à mistura de bools e números inteiros; é suficiente fornecer uma interpretação verdadeira / falsa para outros tipos. Linguagens mais recentes que não têm medo de se desviar de C fizeram isso com muitos tipos diferentes de números inteiros (por exemplo, o Python considera as coleções vazias falsas).

1
E com relação às lâminas de barbear: elas servem para um caso de uso que exige nitidez. Por outro lado, não estou convencido de que programar bem requer a atribuição de variáveis ​​no meio de uma avaliação de expressão. Se houvesse uma maneira simples, de baixa tecnologia, segura e econômica de fazer os pêlos do corpo desaparecerem sem arestas cortantes, tenho certeza de que as lâminas de barbear teriam sido deslocadas ou pelo menos tornadas muito mais raras.

1
@ delnan: Um homem sábio disse uma vez "Torne o mais simples possível, mas não mais simples". Se seu objetivo é eliminar a grande maioria dos erros a = b vs. a == b, restringir o domínio de testes condicionais a booleanos e eliminar as regras de conversão de tipo padrão para <other> -> booleano permitem que você atinja todo o caminho há. Nesse ponto, se (a = b) {} for sintaticamente legal apenas se aeb forem ambos booleanos e a for um valor legal.
John R. Strohm

Transformar uma atribuição em uma declaração é pelo menos tão simples quanto - sem dúvida , mais simples que - as mudanças que você propõe e alcança pelo menos tanto - sem dúvida ainda mais (nem mesmo permite if (a = b)lvalue a, boolean a, b). Em um idioma sem digitação estática, também fornece mensagens de erro muito melhores (em tempo de análise versus tempo de execução). Além disso, prevenir "a = b vs. a == b erros" pode não ser o único objetivo relevante. Por exemplo, eu também gostaria de permitir que o código if items:significasse if len(items) != 0, e que eu teria que desistir de restringir as condições aos booleanos.

1
@ delnan Pascal é um idioma não mainstream? Milhões de pessoas aprenderam a programação usando Pascal (e / ou Modula, que deriva de Pascal). E o Delphi ainda é comumente usado em muitos países (talvez não tanto no seu).
21714 jhenting

5

Para realmente responder à pergunta, sim, existem vários usos disso, embora sejam um pouco de nicho.

Por exemplo em Java:

while ((Object ob = x.next()) != null) {
    // This will loop through calling next() until it returns null
    // The value of the returned object is available as ob within the loop
}

A alternativa sem usar a atribuição incorporada requer o obdefinido fora do escopo do loop e dois locais de código separados que chamam x.next ().

Já foi mencionado que você pode atribuir várias variáveis ​​em uma etapa.

x = y = z = 3;

Esse tipo de coisa é o uso mais comum, mas programadores criativos sempre têm mais.


Essa condição do loop while se desalocaria e criaria um novo obobjeto com cada loop?
User3932000

@ user3932000 Nesse caso, provavelmente não, geralmente x.next () está repetindo alguma coisa. Certamente é possível que sim.
Tim B

1

Como você cria todas as regras, por que agora permitir que a atribuição gere um valor e simplesmente não permitir tarefas dentro de etapas condicionais? Isso fornece o açúcar sintático para facilitar as inicializações, enquanto ainda evita um erro comum de codificação.

Em outras palavras, torne isso legal:

a=b=c=0;

Mas torne isso ilegal:

if (a=b) ...

2
Parece uma regra ad-hoc. Tornar a atribuição uma declaração e estendê-la para permitir a = b = cparece mais ortogonal e mais fácil de implementar também. Essas duas abordagens discordam da atribuição nas expressões ( a + (b = c)), mas você não tomou partido delas, portanto, presumo que elas não importam.

"fácil de implementar" não deve ser muito considerado. Você está definindo uma interface do usuário - coloque as necessidades dos usuários em primeiro lugar. Você só precisa se perguntar se esse comportamento ajuda ou atrapalha o usuário.
Bryan Oakley

se você desaprovar a conversão implícita em bool, então você não precisa se preocupar com a atribuição em condições #
catraca freak

Mais fácil de implementar foi apenas um dos meus argumentos. E o resto? Do ponto de vista da interface do usuário, devo acrescentar que o design incoerente do IMHO e as exceções ad-hoc geralmente dificultam o usuário a grokking e a internalizar as regras.

@ratchetfreak você ainda pode ter um problema ao atribuir bools reais
jk.

0

Pelo que parece, você está no caminho de criar uma linguagem bastante rígida.

Com isso em mente, forçando as pessoas a escrever:

a=c;
b=c;

ao invés de:

a=b=c;

pode parecer uma melhoria para impedir que as pessoas façam:

if (a=b) {

quando eles pretendiam fazer:

if (a==b) {

mas, no final, esse tipo de erro é fácil de detectar e avisa se eles são ou não códigos legais.

No entanto, existem situações em que:

a=c;
b=c;

não significa que

if (a==b) {

será verdade.

Se c for realmente uma função c (), poderá retornar resultados diferentes sempre que for chamado. (também pode ser computacionalmente caro também ...)

Da mesma forma, se c é um ponteiro para o hardware mapeado na memória,

a=*c;
b=*c;

é provável que ambos sejam diferentes e também possam ter efeitos eletrônicos no hardware em cada leitura.

Existem muitas outras permutações de hardware nas quais você precisa ser preciso sobre quais endereços de memória são lidos, gravados e com restrições de tempo específicas, nas quais executar várias atribuições na mesma linha é rápida, simples e óbvia, sem os riscos de tempo que variáveis ​​temporárias introduzem


4
O equivalente a a = b = cnão é a = c; b = c, é b = c; a = b. Isso evita a duplicação de efeitos colaterais e também mantém a modificação ae bna mesma ordem. Além disso, todos esses argumentos relacionados ao hardware são meio estúpidos: a maioria das linguagens não são linguagens de sistema e não são projetadas para resolver esses problemas, nem estão sendo usadas em situações em que esses problemas ocorrem. Isso vale duplamente para uma linguagem que tenta substituir o JavaScript e / ou PHP.

delnan, a questão não era esses exemplos inventados, eles são. O argumento ainda é que eles mostram os tipos de lugares em que escrever a = b = c é comum e, no caso do hardware, é considerado uma boa prática, conforme solicitado pelo OP. Tenho certeza de que será capaz de considerar a sua relevância para o seu ambiente esperado
Michael Shaw

Olhando para trás, meu problema com esta resposta não é principalmente o foco nos casos de uso de programação do sistema (embora isso seja ruim o suficiente, da maneira como está escrito), mas depende de assumir uma reescrita incorreta. Os exemplos não são exemplos de lugares onde a=b=cé comum / útil, são exemplos de lugares onde a ordem e o número de efeitos colaterais devem ser tratados. Isso é totalmente independente. Reescreva a atribuição encadeada corretamente e as duas variantes estão igualmente corretas.

@ delnan: O rvalue é convertido para o tipo de bem uma temperatura e que é convertido para o tipo de aem outra temperatura. O tempo relativo de quando esses valores são realmente armazenados não é especificado. De uma perspectiva de design de linguagem, eu consideraria razoável exigir que todos os valores em uma declaração de atribuição múltipla tenham um tipo correspondente e possivelmente também exigir que nenhum deles seja volátil.
Supercat

0

O maior benefício para mim de ter uma tarefa como uma expressão é que ela permite que sua gramática seja mais simples se um de seus objetivos for "tudo é uma expressão" - um objetivo do LISP em particular.

Python não tem isso; possui expressões e declarações, sendo atribuição uma declaração. Mas como o Python define um lambdaformulário como sendo uma única expressão parametrizada , isso significa que você não pode atribuir variáveis ​​dentro de uma lambda. Às vezes, isso é inconveniente, mas não é um problema crítico, e é a única desvantagem na minha experiência de ter uma atribuição como uma declaração em Python.

Uma maneira de permitir que a atribuição, ou melhor, o efeito da atribuição, seja uma expressão sem introduzir o potencial de if(x=1)acidentes que C tem é usar uma letconstrução semelhante ao LISP , como a (let ((x 2) (y 3)) (+ x y))que pode ser avaliada no seu idioma como 5. Usando letdesta forma não precisa ser tecnicamente atribuição em tudo na sua língua, se você definir letcomo a criação de um escopo lexical. Definido dessa maneira, uma letconstrução pode ser compilada da mesma maneira que a construção e a chamada de uma função de fechamento aninhado com argumentos.

Por outro lado, se você está simplesmente preocupado com o if(x=1)caso, mas deseja que a atribuição seja uma expressão como em C, talvez apenas a escolha de tokens diferentes seja suficiente. Cessão: x := 1ou x <- 1. Comparação: x == 1. Erro de sintaxe: x = 1.


1
letdifere da atribuição de mais maneiras do que introduzir tecnicamente uma nova variável em um novo escopo. Para iniciantes, ele não tem efeito no código fora do letcorpo do e, portanto, requer aninhar todo o código (o que deve usar a variável), uma desvantagem significativa no código pesado de atribuição. Se alguém seguisse esse caminho, set!seria o melhor analógico Lisp - completamente diferente da comparação, mas não exigindo aninhamento ou um novo escopo.

@ delnan: eu gostaria de ver uma combinação de sintaxe declarar e atribuir que proibisse a reatribuição, mas permitiria a redeclaração, sujeita às regras de que (1) a redeclaração seria legal apenas para identificadores de declaração e atribuição e (2) ) a redeclaração "cancelaria" uma variável em todos os escopos anexos. Assim, o valor de qualquer identificador válido seria o que foi atribuído na declaração anterior desse nome. Isso parece um pouco melhor do que ter que adicionar blocos de escopo para variáveis ​​que são usadas apenas para algumas linhas ou ter que formular novos nomes para cada variável temporária.
precisa

0

Eu sei que isso significaria que aquelas funções de linha única que as pessoas C amam tanto não funcionariam mais. Imaginei (com poucas evidências além da minha experiência pessoal) que a grande maioria das vezes que isso acontecia, era realmente uma operação de comparação.

De fato. Isso não é novidade, todos os subconjuntos seguros da linguagem C já chegaram a essa conclusão.

MISRA-C, CERT-C e assim por diante, todas as atribuições de proibição dentro de condições, simplesmente por serem perigosas.

Não existe nenhum caso em que o código que depende de atribuição dentro de condições não possa ser reescrito.


Além disso, esses padrões também alertam contra a criação de código que depende da ordem da avaliação. Várias atribuições em uma única linha x=y=z;é o caso. Se uma linha com várias atribuições contiver efeitos colaterais (chamar funções, acessar variáveis ​​voláteis etc.), não será possível saber qual efeito colateral ocorrerá primeiro.

Não há pontos de sequência entre a avaliação dos operandos. Portanto, não podemos saber se a subexpressão yé avaliada antes ou depois z: é um comportamento não especificado em C. Portanto, esse código é potencialmente não confiável, não portátil e não conforme com os subconjuntos seguros mencionados de C.

A solução teria sido substituir o código por y=z; x=y;. Isso adiciona um ponto de sequência e garante a ordem da avaliação.


Portanto, com base em todos os problemas que isso causou em C, qualquer linguagem moderna faria bem em proibir a atribuição dentro de condições, bem como em várias atribuições em uma única linha.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.