Por que os operadores definidos pelo usuário não são mais comuns?

92

Um recurso que sinto falta em linguagens funcionais é a ideia de que os operadores são apenas funções, portanto, adicionar um operador personalizado geralmente é tão simples quanto adicionar uma função. Muitas linguagens procedurais permitem sobrecargas de operadores, então, em certo sentido, os operadores ainda são funções (isso é muito verdadeiro em D , onde o operador é passado como uma string em um parâmetro de modelo).

Parece que, quando a sobrecarga do operador é permitida, muitas vezes é insignificante adicionar mais operadores personalizados. Eu encontrei esta postagem do blog , que argumenta que os operadores personalizados não funciona muito bem com a notação infixada por causa das regras de precedência, mas o autor fornece várias soluções para esse problema.

Eu olhei em volta e não encontrei nenhuma linguagem procedural que suportasse operadores personalizados no idioma. Existem hacks (como macros em C ++), mas isso não é o mesmo que suporte a idiomas.

Como esse recurso é bastante simples de implementar, por que não é mais comum?

Eu entendo que isso pode levar a algum código feio, mas isso não impediu que os designers de linguagem no passado adicionassem recursos úteis que podem ser facilmente abusados (macros, operadores ternários, ponteiros inseguros).

Casos de uso reais:

  • Implemente operadores em falta (por exemplo, Lua não tem operadores bitwise)
  • ~ de Mimic D (concatenação de matriz)
  • DSLs
  • Use | como açúcar de sintaxe estilo pipe Unix (usando coroutines / geradores)

Também estou interessado em idiomas que fazem permitir operadores personalizados, mas estou mais interessado em por que ele foi excluído. Pensei em bifurcar uma linguagem de script para adicionar operadores definidos pelo usuário, mas me interrompi quando percebi que não a via em nenhum lugar, então provavelmente há uma boa razão para os projetistas de idiomas terem sido mais inteligentes do que eu.

    
por beatgammit 29.12.2012 / 19:23
fonte

16 respostas

131

Existem duas escolas de pensamento diametralmente opostas no design da linguagem de programação. Uma é que os programadores escrevem um código melhor com menos restrições e o outro é que escrevem código melhor com mais restrições. Na minha opinião, a realidade é que bons programadores experientes florescem com menos restrições, mas essas restrições podem beneficiar a qualidade do código dos iniciantes.

Os operadores definidos pelo usuário podem criar um código muito elegante em mãos experientes e um código extremamente ruim para um iniciante. Então, se a sua linguagem inclui ou não depende da escola de pensamento do seu designer de idioma.

    
por 29.12.2012 / 22:05
fonte
83

Tendo em vista a escolha entre concatenar arrays com ~ ou com "myArray.Concat (secondArray)", eu provavelmente preferiria o último. Por quê? Porque ~ é um personagem completamente sem sentido que só tem o seu significado - o de concatenação de array - dado no projeto específico onde foi escrito.

Basicamente, como você disse, os operadores não são diferentes dos métodos. Mas, embora os métodos possam receber nomes compreensíveis e legíveis, que contribuem para o entendimento do fluxo do código, os operadores são opacos e situacionais.

É por isso que eu também não gosto% operador% co_de do PHP (concatenação) ou a maioria dos operadores em Haskell ou OCaml, embora neste caso, algumas normas universalmente aceites estão surgindo para linguagens funcionais.

    
por 29.12.2012 / 19:31
fonte
70

Since this feature is pretty trivial to implement, why isn't it more common?

Sua premissa está errada. Não é "bastante trivial de implementar". Na verdade, isso traz um monte de problemas.

Vamos dar uma olhada nas "soluções" sugeridas no post:

  • Sem precedência . O próprio autor diz que "não usar regras de precedência simplesmente não é uma opção".
  • Análise semântica . Como o artigo diz, isso exigiria que o compilador tivesse muito conhecimento semântico. O artigo na verdade não oferece uma solução para isso e deixe-me dizer, isso simplesmente não é trivial. Compiladores são projetados como um trade-off entre poder e complexidade. Em particular, o autor menciona uma etapa de pré-análise para coletar as informações relevantes, mas a pré-análise é ineficiente e os compiladores se esforçam muito para minimizar as passagens de análise.
  • Nenhum operador infixo personalizado . Bem, isso não é uma solução.
  • Solução híbrida . Essa solução carrega muitas (mas não todas) as desvantagens da análise semântica. Em particular, como o compilador tem que tratar os tokens desconhecidos como potencialmente representando operadores personalizados, ele geralmente não pode produzir mensagens de erro significativas. Pode também exigir que a definição do dito operador prossiga com a análise (para recolher informações de tipo, etc.), necessitando mais uma vez de um passo de análise adicional.

Em suma, esse é um recurso caro para implementar, tanto em termos de complexidade do analisador quanto em termos de desempenho, e não está claro que isso traria muitos benefícios. Claro, há alguns benefícios para a capacidade de definir novos operadores, mas mesmo esses são controversos (basta olhar para as outras respostas argumentando que ter novos operadores não é uma coisa boa).

    
por 30.12.2012 / 13:38
fonte
25

Vamos ignorar todo o argumento "operadores são abusados para prejudicar a legibilidade" no momento e focar nas implicações do design de linguagem.

Os operadores do Infix têm mais problemas do que regras de precedência simples (embora, para ser franco, o link que você faz referência trivialize o impacto dessa decisão de design). Uma delas é a resolução de conflitos: o que acontece quando você define a.operator+(b) e b.operator+(a) ? Preferir um sobre o outro leva a quebrar a propriedade comutativa esperada daquele operador. Lançar um erro pode levar a módulos que, de outra forma, ficariam quebrados quando juntos. O que acontece quando você começa a lançar tipos derivados no mix?

O fato é que os operadores não são apenas funções. As funções são independentes ou pertencem à sua classe, o que fornece uma clara preferência sobre qual parâmetro (se houver) possui o despacho polimórfico.

E isso ignora os vários problemas de empacotamento e resolução que surgem dos operadores. A razão pela qual os projetistas de linguagens (em geral) limitam a definição do operador infixo é porque ele cria uma pilha de problemas para a linguagem enquanto fornece benefícios discutíveis.

E, francamente, porque eles são não triviais para implementar.

    
por 30.12.2012 / 00:08
fonte
19

Acho que você ficaria surpreso com a frequência com que as sobrecargas do operador são implementadas alguma forma. Mas eles não são comumente usados em muitas comunidades.

Por que usar ~ para concatenar em uma matriz? Por que não use < < como o Ruby faz ? Porque os programadores com quem você trabalha provavelmente não são programadores Ruby. Ou programadores de D. Então, o que eles fazem quando se deparam com o seu código? Eles têm que ir e procurar o que o símbolo significa.

Eu costumava trabalhar com um ótimo desenvolvedor C # que também gostava de linguagens funcionais. Do nada, ele começou a introduzir monads em C # por meio de métodos de extensão e usando a terminologia padrão de mônadas. Ninguém poderia contestar que parte de seu código era mais rápido e mais legível, uma vez que você soubesse o que significava, mas significava que todos precisavam aprender terminologia antes que o código fizesse sentido.

Justo, você acha? Foi apenas uma pequena equipe. Pessoalmente, eu discordo. Todo novo desenvolvedor estava destinado a ser confundido por essa terminologia. Não temos problemas suficientes para aprender um novo domínio?

Por outro lado, terei todo o prazer em usar o operador ?? em C # porque Espero que outros desenvolvedores de C # saibam o que é, mas eu não iria sobrecarregá-lo em uma linguagem que não suportava isso por padrão.

    
por 29.12.2012 / 21:25
fonte
11

Posso pensar em algumas razões:

  • Eles não são triviais para implementar - permitir que operadores personalizados arbitrários tornem seu compilador muito mais complexo, especialmente se você permitir regras de precedência, fixidez e aridade definidas pelo usuário. Se a simplicidade é uma virtude, a sobrecarga do operador está tirando você do bom design da linguagem.
  • Eles são abusados - principalmente por codificadores que acham que é "legal" redefinir operadores e começar a redefini-los para todos os tipos de classes personalizadas. Em pouco tempo, seu código está repleto de símbolos personalizados que ninguém mais pode ler ou entender porque os operadores não seguem as regras convencionais bem compreendidas. Eu não compro o argumento "DSL", a menos que seu DSL seja um subconjunto da matemática: -)
  • Elas prejudicam a legibilidade e a facilidade de manutenção - se os operadores forem substituídos regularmente, pode se tornar difícil detectar quando essa instalação está sendo usada e os programadores são forçados a se perguntar continuamente o que um operador está fazendo. É muito melhor dar nomes de função significativos. Digitar alguns caracteres extras é barato, problemas de manutenção a longo prazo são caros.
  • Eles podem quebrar as expectativas implícitas de desempenho . Por exemplo, eu normalmente esperaria que a pesquisa de um elemento em uma matriz fosse O(1) . Mas com a sobrecarga do operador, someobject[i] poderia facilmente ser uma operação O(n) dependendo da implementação do operador de indexação.

Na realidade, há poucos casos em que a sobrecarga do operador tenha usos justificáveis em comparação com o uso de funções regulares. Um exemplo legítimo pode ser o projeto de uma classe numérica complexa para uso por matemáticos, que entendem as formas bem compreendidas de que operadores matemáticos são definidos para números complexos. Mas isso não é um caso muito comum.

Alguns casos interessantes a considerar:

  • Lisps : em geral, não distingue entre operadores e funções - + é apenas uma função regular. Você pode definir funções da maneira que preferir (normalmente, há uma maneira de defini-las em namespaces separados para evitar conflitos com o + incorporado), incluindo os operadores. Mas há uma tendência cultural para usar nomes de função significativos, então isso não é muito abusado. Além disso, na notação de prefixo Lisp tende a ser usada exclusivamente, portanto, há menos valor no "açúcar sintático" que as sobrecargas de operador fornecem.
  • Java - não permite sobrecarga do operador. Isso é ocasionalmente irritante (para coisas como o caso do número Complex), mas, em média, é provavelmente a decisão de design correta para Java, que é uma linguagem OOP simples e de propósito geral. O código Java é realmente muito fácil para os desenvolvedores de baixa / média habilidade manterem como resultado dessa simplicidade.
  • O C ++ possui sobrecarga de operador muito sofisticada. Às vezes isso é abusado ( cout << "Hello World!" alguém?), Mas a abordagem faz sentido, considerando o posicionamento do C ++ como uma linguagem complexa que permite a programação de alto nível enquanto ainda permite que você fique muito próximo do metal para obter desempenho. escreva uma classe numérica complexa que se comporte exatamente como você quer, sem comprometer o desempenho. Entende-se que é sua responsabilidade se você se atirar no pé.
por 02.01.2013 / 06:17
fonte
8

Since this feature is pretty trivial to implement, why isn't it more common?

não é trivial de implementar (a menos que seja implementado de forma trivial). Ele também não te ajuda muito, mesmo se implementado de maneira ideal: os ganhos de legibilidade por tese são compensados pelas perdas de legibilidade por falta de familiaridade e opacidade. Em suma, é incomum, porque geralmente não vale o tempo dos desenvolvedores ou dos usuários.

Dito isso, posso pensar em três idiomas que fazem isso e eles o fazem de maneiras diferentes:

  • Racket, um esquema, quando não está sendo todo S-expression-y, permite e espera que você escreva o que equivale ao parser para qualquer sintaxe que você queira estender (e fornece ganchos úteis para tornar isso tratável). / li>
  • O Haskell, uma linguagem de programação puramente funcional, permite definir qualquer operador que consiste apenas em pontuação e permite fornecer um nível de fixidez (10 disponível) e uma associatividade. Os operadores ternários etc. podem ser criados a partir de operadores binários e funções de ordem superior.
  • Agda, uma linguagem de programação de tipo dependente, é extremamente flexível com os operadores (em inglês) aqui ) permitindo que ambos if-then e if-then-else sejam definidos como operadores no mesmo programa, mas seu lexer, parser e avaliador são todos strongmente acoplados como resultado.
por 30.12.2012 / 19:14
fonte
7

Uma das principais razões pelas quais os operadores personalizados são desencorajados é porque qualquer operador pode dizer / pode fazer qualquer coisa.

Por exemplo, cstream é muito criticado como sobrecarga de deslocamento à esquerda.

Quando uma linguagem permite sobrecargas do operador, geralmente há um encorajamento para manter o comportamento do operador similar ao comportamento base para evitar confusão.

Além disso, os operadores definidos pelo usuário dificultam muito a análise, especialmente quando há também regras de preferências personalizadas.

    
por 29.12.2012 / 20:01
fonte
4

Não usamos operadores definidos pelo usuário pelo mesmo motivo que não usamos palavras definidas pelo usuário. Ninguém chamaria sua função de "sworp". A única maneira de transmitir seu pensamento para outra pessoa é usar a linguagem compartilhada. E isso significa que tanto palavras quanto sinais (operadores) devem ser conhecidos pela sociedade para quem você está escrevendo seu código.

Portanto, os operadores que você vê em uso em linguagens de programação são aqueles que foram ensinados na escola (aritmética) ou aqueles que foram estabelecidos na comunidade de programação, como operadores booleanos.

    
por 30.12.2012 / 19:32
fonte
4

Quanto às linguagens que suportam essa sobrecarga: o Scala, na verdade, de uma maneira muito mais limpa e melhor, pode C ++. A maioria dos caracteres pode ser usada em nomes de funções, assim você pode definir operadores como! + * = ++, se quiser. Há suporte embutido para infix (para todas as funções que levam um argumento). Eu acho que você pode definir a associatividade de tais funções também. Você não pode, no entanto, definir a precedência (apenas com truques feios, veja aqui ).

    
por 23.05.2017 / 14:40
fonte
4

Uma coisa que ainda não foi mencionada é o caso do Smalltalk, onde tudo (incluindo operadores) é um envio de mensagem. "Operadores" como + , | e assim por diante são métodos realmente unários.

Todos os métodos podem ser sobrescritos, então a + b significa adição de inteiro se a e b forem inteiros, e significa adição de vetor se ambos forem OrderedCollection s.

Não há regras de precedência, pois são apenas chamadas de método. Isso tem uma implicação importante para a notação matemática padrão: 3 + 4 * 5 significa (3 + 4) * 5 , não 3 + (4 * 5) .

(Este é um grande obstáculo para iniciantes do Smalltalk. Quebrar regras matemáticas remove um caso especial, de modo que toda a avaliação do código prossegue uniformemente da esquerda para a direita, tornando a linguagem muito mais simples.)

    
por 02.01.2013 / 08:53
fonte
3

Você está lutando contra duas coisas aqui:

  1. Por que os operadores existem em idiomas em primeiro lugar?
  2. Qual é a virtude dos operadores sobre funções / métodos?

Na maioria das linguagens, os operadores não são realmente implementados como funções simples. Eles podem ter alguma função de scaffolding, mas o compilador / tempo de execução é explicitamente ciente de seu significado semântico e como traduzi-los eficientemente em código de máquina. Isso é muito mais verdadeiro, mesmo quando comparado às funções internas (e é por isso que a maioria das implementações também não inclui toda a sobrecarga da chamada de função em sua implementação). A maioria dos operadores são abstrações de nível superior em instruções primitivas encontradas em CPUs (o que é em parte o motivo pelo qual a maioria dos operadores é aritmética, booleana ou bit a bit). Você poderia modelá-las como funções "especiais" (chamá-las de "primitivas" ou "builtins" ou "nativas" ou qualquer outra), mas fazer isso genericamente requer um conjunto muito robusto de semânticas para definir essas funções especiais. A alternativa é ter operadores integrados que se parecem semanticamente com operadores definidos pelo usuário, mas que, de outra forma, invocam caminhos especiais no compilador. Isso entra em conflito com a resposta à segunda questão ...

Além do problema de tradução automática mencionado acima, em um nível sintático, os operadores não são realmente diferentes das funções. Eles são características distintivas que tendem a ser que eles são concisos e simbólicos, o que sugere uma característica adicional significativa que deve ter para ser útil: eles devem ter significado / semântica amplamente compreendido para os desenvolvedores. Símbolos curtos não transmitem muito significado a menos que seja curto para um conjunto de semânticas que já são compreendidas. Isso torna os operadores definidos pelo usuário inerentemente inúteis, pois, por sua própria natureza, eles não são tão amplamente compreendidos. Eles fazem tanto sentido quanto nomes de função de uma ou duas letras.

As sobrecargas do operador do C ++ fornecem um terreno fértil para examinar isso. A maioria dos "abusos" de sobrecarga do operador vem na forma de sobrecargas que quebram alguns dos contratos semânticos que são amplamente compreendidos (um exemplo clássico é uma sobrecarga do operador + tal que a + b! = B + a, ou onde + modifica qualquer um dos seus operandos).

Se você olhar para o Smalltalk, que permite a sobrecarga do operador e operadores definidos pelo usuário, você pode ver como uma linguagem pode ser usada e quão útil seria. Em Smalltalk, os operadores são meramente métodos com propriedades sintáticas diferentes (a saber, eles são codificados como binários infix). A linguagem usa "métodos primitivos" para operadores e métodos acelerados especiais. Você descobre que poucos operadores, se definidos pelo usuário, são criados e, quando o são, tendem a não se acostumar tanto quanto o autor provavelmente pretendia que fossem usados. Mesmo o equivalente de uma sobrecarga de operador é raro, porque é principalmente uma perda de rede para definir uma nova função como um operador em vez de um método, pois o último permite uma expressão da semântica da função.

    
por 30.12.2012 / 21:14
fonte
1

Eu sempre achei sobrecargas de operador em C ++ para ser um atalho conveniente para uma equipe de um único desenvolvedor, mas que causa todo tipo de confusão a longo prazo simplesmente porque as chamadas de método estão sendo "ocultas" de uma maneira que não é Não é fácil para ferramentas como o doxygen separar, e as pessoas precisam entender os idiomas para usá-los apropriadamente.

Às vezes é muito mais difícil entender o que você espera, mesmo. Era uma vez, em um grande projeto C ++ multiplataforma, decidi que seria uma boa idéia normalizar a maneira como os caminhos foram construídos, criando um objeto FilePath (semelhante ao objeto File do Java), que teria operador / usado para concatenar outra parte do caminho (assim você poderia fazer algo como File::getHomeDir()/"foo"/"bar" e faria a coisa certa em todas as nossas plataformas suportadas). Todo mundo que viu, essencialmente, disse: "Que diabos? Divisão de cordas? ... Oh, isso é fofo, mas eu não confio para fazer a coisa certa."

Da mesma forma, há muitos casos em programação gráfica ou outras áreas onde a matemática vetorial / matriz acontece muito onde é tentador fazer coisas como Matriz * Matriz, Vector * Vector (ponto), Vector% Vector (cruz), Matriz * Vector (matriz transformada), Matriz ^ Vetor (transformada de matriz de caso especial ignorando a coordenada homogênea - útil para normais de superfície), e assim por diante, mas enquanto economiza um pouco de tempo de análise para a pessoa que escreveu a biblioteca matemática de vetores, só acaba confundindo a questão mais para os outros. Não vale a pena.

    
por 26.05.2013 / 00:07
fonte
0

As sobrecargas do operador são uma má ideia pelo mesmo motivo que as sobrecargas de método são uma má ideia: o mesmo símbolo na tela teria diferentes significados dependendo do que está ao redor dele. Isso dificulta a leitura casual.

Como a legibilidade é um aspecto crítico da manutenção, você deve sempre evitar a sobrecarga (exceto em alguns casos muito especiais). É muito melhor que cada símbolo (operador ou identificador alfanumérico) tenha um significado único que se sustente sozinho.

Para ilustrar: ao ler um código desconhecido, se você encontrar um novo identificador de alfanum que não conhece, pelo menos você tem a vantagem que você sabe que não o conhece . Você pode, então, ir procurar. Se, no entanto, você vir um identificador ou operador comum do qual sabe o significado, é muito menos provável que você perceba que ele realmente foi sobrecarregado para ter um significado completamente diferente. Para saber quais operadores foram sobrecarregados (em uma base de código que fez uso generalizado de sobrecarga), você precisaria de um conhecimento prático do código completo, mesmo que você queira apenas ler uma pequena parte dele. Isso dificultaria a introdução de novos desenvolvedores nesse código e a impossibilidade de trazer pessoas para um pequeno trabalho. Isso pode ser bom para a segurança do trabalho do programador, mas se você é responsável pelo sucesso da base de código, evite essa prática a todo custo.

Como os operadores são pequenos em tamanho, sobrecarregar os operadores permitiria um código mais denso, mas tornar o código denso não é um benefício real. Uma linha com o dobro da lógica leva o dobro do tempo para ler. O compilador não se importa. O único problema é a legibilidade humana. Como fazer o código compacto não aumenta a legibilidade, não há benefício real para a compactação. Vá em frente e pegue o espaço, e dê às operações exclusivas um identificador único, e seu código terá mais sucesso a longo prazo.

    
por 01.01.2013 / 03:47
fonte
-1

Dificuldades técnicas para lidar com precedência e análise complexa deixadas de lado, eu acho que existem alguns aspectos do que uma linguagem de programação é que tem que ser considerada.

Os operadores são tipicamente construções lógicas curtas que são bem definidas e documentadas na linguagem principal (compare, atribua ..). Eles também são normalmente difíceis de entender sem documentação (compare a^b com xor(a,b) , por exemplo). Há um número bastante limitado de operadores que podem fazer sentido na programação normal (>, & lt ;, =, + etc ..).

Minha idéia é que é melhor se ater a um conjunto de operadores bem definidos em uma linguagem - então permitir a sobrecarga de operadores desses operadores (dada uma recomendação suave de que os operadores devem fazer a mesma coisa, mas com um tipo de dados personalizado) .

Seus casos de uso de ~ e | seriam realmente possíveis com a simples sobrecarga do operador (C #, C ++ etc.). DSL é uma área de uso válida, mas provavelmente uma das únicas áreas válidas (do meu ponto de vista). Eu, no entanto, acho que existem ferramentas melhores para criar novas linguagens. Executar uma verdadeira linguagem DSL dentro de outra linguagem não é tão difícil usando qualquer uma dessas ferramentas compiladores-compiladores. O mesmo vale para o "estender o argumento LUA". Uma linguagem é mais provavelmente definida principalmente para resolver problemas de uma maneira específica, não para ser uma base para sub-linguagens (existem exceções).

    
por 30.12.2012 / 15:32
fonte
-1

Outro fator para isso é que nem sempre é fácil definir uma operação com os operadores disponíveis. Quero dizer, sim, para qualquer tipo de número, o operador '*' pode fazer sentido e geralmente é implementado na linguagem ou em módulos existentes. Mas no caso das classes complexas típicas que você precisa definir (coisas como ShipingAddress, WindowManager, ObjectDimensions, PlayerCharacter, etc) esse comportamento não está claro ... O que significa adicionar ou subtrair um número a um endereço? Multiplique dois endereços?

Claro, você pode definir que adicionar uma string a uma classe ShippingAddress significa uma operação personalizada como "substituir a linha 1 no endereço" (em vez da função "setLine1") e adicionar um número é "substituir o código postal" (em vez de " setZipCode "), mas o código não é muito legível e confuso. Geralmente pensamos que o operador é usado em tipos / classes básicas, pois seu comportamento é intuitivo, claro e consistente (uma vez que você esteja familiarizado com a linguagem, pelo menos). Pense em tipos como Integer, String, ComplexNumbers, etc.

Assim, mesmo que a definição de operadores possa ser muito útil em alguns casos específicos, sua implementação no mundo real é bastante limitada, já que os 99% dos casos em que isso será uma vitória clara já estão implementados no pacote de idiomas básicos.

    
por 06.01.2013 / 22:19
fonte