Um recurso que sinto falta das linguagens funcionais é a ideia de que os operadores são apenas funções; portanto, adicionar um operador personalizado é tão simples quanto adicionar uma função. Muitas linguagens procedurais permitem sobrecargas do operador, portanto, em certo sentido, os operadores ainda são funções (isso é verdade em D, onde o operador é passado como uma string em um parâmetro de modelo).
Parece que onde a sobrecarga do operador é permitida, geralmente é trivial adicionar operadores personalizados adicionais. Encontrei este post no blog , que argumenta que os operadores personalizados não funcionam bem com a notação infix por causa das regras de precedência, mas o autor fornece várias soluções para esse problema.
Olhei em volta e não consegui encontrar nenhuma linguagem processual que suporte operadores personalizados na linguagem. Existem hacks (como macros em C ++), mas isso não é o mesmo que o suporte a idiomas.
Como esse recurso é bastante trivial de implementar, por que não é mais comum?
Eu entendo que isso pode levar a algum código feio, mas isso não impediu os designers de linguagem de adicionar recursos úteis que podem ser facilmente abusados (macros, operador ternário, ponteiros inseguros).
Casos de uso reais:
- Implementar operadores ausentes (por exemplo, Lua não possui operadores bit a bit)
- Imitar D's
~
(concatenação de matriz) - DSLs
- Use
|
como açúcar de sintaxe no estilo de canal Unix (usando coroutines / geradores)
Eu também estou interessado em idiomas que fazer permitir que os operadores de costume, mas eu estou mais interessado em por que ele foi excluído. Pensei em criar uma linguagem de script para adicionar operadores definidos pelo usuário, mas me parei quando percebi que não a havia visto em nenhum lugar; portanto, provavelmente há uma boa razão pela qual designers de linguagem mais inteligentes do que eu não o permitiram.
fonte
Respostas:
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 a outra é que eles escrevem um 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.
Operadores definidos pelo usuário podem criar código muito elegante em mãos experientes e código absolutamente horrível para iniciantes. Portanto, se o seu idioma os inclui ou não, depende da escola de pensamento do seu designer de idiomas.
fonte
Format
método) e quando deve recusar ( por exemplo, argumentos de caixa automática paraReferenceEquals
). Quanto mais a linguagem da habilidade permitir que os programadores digam quando determinadas inferências seriam inadequadas, mais seguramente ela pode oferecer inferências convenientes quando apropriado.Dada a opção entre concatenar matrizes com ~ ou com "myArray.Concat (secondArray)", eu provavelmente preferiria o último. Por quê? Porque ~ é um personagem completamente sem sentido que só tem seu significado - o da concatenação de array - dado no projeto específico em que foi escrito.
Basicamente, como você disse, os operadores não são diferentes dos métodos. Porém, embora os métodos possam receber nomes legíveis e compreensíveis que aumentam a compreensão do fluxo de código, os operadores são opacos e situacionais.
É por isso que eu também não gosto do
.
operador do PHP (concatenação de strings) ou da maioria dos operadores do Haskell ou OCaml, embora, neste caso, alguns padrões universalmente aceitos estejam surgindo para linguagens funcionais.fonte
+
e<<
certamente não estão definidasObject
(eu recebo "nenhuma correspondência para o operador + em ..." ao fazer isso em uma classe simples em C ++).Sua premissa está errada. É não “bastante trivial para implementar”. De fato, isso traz um monte de problemas.
Vamos dar uma olhada nas "soluções" sugeridas no post:
Em suma, esse é um recurso caro para implementar, tanto em termos de complexidade do analisador quanto de desempenho, e não está claro se isso traria muitos benefícios. Certamente, existem alguns benefícios na capacidade de definir novos operadores, mas mesmo aqueles são controversos (basta olhar para as outras respostas argumentando que ter novos operadores não é uma coisa boa).
fonte
Vamos ignorar o argumento "operadores abusam para prejudicar a legibilidade" no momento e focar nas implicações do design da linguagem.
Os operadores de infix têm mais problemas do que simples regras de precedência (embora seja franco, o link que você faz referência trivializa o impacto dessa decisão de design). Uma é a resolução de conflitos: o que acontece quando você define
a.operator+(b)
eb.operator+(a)
? Preferir um sobre o outro leva a quebrar a propriedade comutativa esperada desse operador. Lançar um erro pode levar a módulos que, de outra forma, funcionariam, seriam quebrados uma vez 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 preferência clara sobre qual parâmetro (se houver) possui o despacho polimórfico.
E isso ignora os vários problemas de embalagem e resolução que surgem dos operadores. A razão pela qual os designers de idiomas (em geral) limitam a definição de operador de infixo é porque ele cria uma pilha de problemas para o idioma, ao mesmo tempo em que oferece benefícios discutíveis.
E, francamente, porque eles não são triviais para implementar.
fonte
+
é mau. Mas isso é realmente um argumento contra operadores definidos pelo usuário? Parece um argumento contra a sobrecarga do operador em geral.boost::spirit
. Assim que você permitir que os operadores definidos pelo usuário piorem, não há uma boa maneira de definir bem a precedência para a matemática. Escrevi um pouco sobre isso no contexto de uma linguagem que procura especificamente resolver problemas com operadores definidos arbitrariamente.Acho que você ficaria surpreso com a frequência com que as sobrecargas do operador são implementadas de alguma forma. Mas eles não são comumente usados em muitas comunidades.
Por que usar ~ para concatenar para uma matriz? Por que não usar << como Ruby ? Como os programadores com os quais você trabalha provavelmente não são programadores Ruby. Ou programadores D. Então, o que eles fazem quando encontram seu código? Eles precisam procurar o significado do símbolo.
Eu costumava trabalhar com um desenvolvedor de C # muito bom, que também gostava de linguagens funcionais. Do nada, ele começou a introduzir mônadas no C # por meio de métodos de extensão e usando a terminologia padrão das mônadas. Ninguém poderia contestar que parte do código dele era mais tenso e ainda mais legível quando você sabia o que significava, mas isso significava que todos tinham que aprender a terminologia da mônada antes que o código fizesse sentido .
Justo, você acha? Era apenas uma equipe pequena. Pessoalmente, eu discordo. Todo novo desenvolvedor estava destinado a ser confundido com essa terminologia. Não temos problemas suficientes para aprender um novo domínio?
Por outro lado, utilizarei alegremente o
??
operador em C # porque espero que outros desenvolvedores de C # saibam o que é, mas não o sobrecarregaria em um idioma que não o suporta por padrão.fonte
??
exemplo.Eu posso pensar em alguns motivos:
O(1)
. Porém, com sobrecarga do operador,someobject[i]
pode ser facilmente umaO(n)
operação, dependendo da implementação do operador de indexação.Na realidade, existem muito poucos casos em que a sobrecarga do operador tem usos justificáveis em comparação ao uso de funções regulares. Um exemplo legítimo pode estar projetando uma classe numérica complexa para ser usada por matemáticos, que entendem as maneiras bem compreendidas pelas quais os operadores matemáticos são definidos para números complexos. Mas esse realmente não é um caso muito comum.
Alguns casos interessantes a serem considerados:
+
é apenas uma função regular. Você pode definir as funções da maneira que desejar (normalmente, há uma maneira de defini-las em espaços para nome separados para evitar conflitos com o interno+
), incluindo operadores. Mas há uma tendência cultural de usar nomes de funções significativos, para que isso não seja 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 do operador fornecem.cout << "Hello World!"
alguém?), Mas a abordagem faz sentido, dado o posicionamento do C ++ como uma linguagem complexa que permite programação de alto nível e, ao mesmo tempo, permite que você fique muito próximo do metal para desempenho, para que você possa, por exemplo, escrever uma classe numérica complexa que se comporte exatamente como você deseja, sem comprometer o desempenho. Entende-se que é de sua responsabilidade se você atirar no próprio pé.fonte
Não é trivial de implementar (a menos que seja trivialmente implementado). Também não é muito útil, mesmo se implementado de maneira ideal: os ganhos de legibilidade com a dispersão são compensados pelas perdas de legibilidade por falta de familiaridade e opacidade. Em resumo, é incomum, porque geralmente não vale o tempo dos desenvolvedores ou dos usuários.
Dito isto, posso pensar em três idiomas que o fazem, e eles fazem isso de maneiras diferentes:
fonte
Uma das principais razões pelas quais os operadores personalizados são desencorajados é porque qualquer operador pode significar / pode fazer qualquer coisa.
Por exemplo
cstream
, a sobrecarga de turno à esquerda muito criticada.Quando um idioma permite sobrecargas do operador, geralmente há um incentivo para manter o comportamento do operador semelhante ao comportamento da base para evitar confusão.
Os operadores definidos pelo usuário também tornam a análise muito mais difícil, especialmente quando também há regras de preferências personalizadas.
fonte
+
adicione duas coisas,-
subtraia-as,*
multiplique-as. Meu sentimento é que ninguém obriga o programador a fazer com que a função / métodoadd
realmente adicione algo edoNothing
possa lançar armas nucleares. Ea.plus(b.minus(c.times(d)).times(e)
é muito menos legível, entãoa + (b - c * d) * e
(bônus adicional - onde na primeira picada há erro na transcrição). Eu não vejo como primeira é mais significativa ...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 "sworp". A única maneira de transmitir seu pensamento a outra pessoa é usar um idioma compartilhado. E isso significa que palavras e sinais (operadores) devem ser conhecidos da sociedade para a qual você está escrevendo seu código.
Portanto, os operadores que você vê em uso nas linguagens de programação são os que aprendemos na escola (aritmética) ou os que foram estabelecidos na comunidade de programação, como, por exemplo, operadores booleanos.
fonte
elem
é uma ótima idéia e certamente um operador que todos deveriam entender, mas outros parecem discordar.Quanto às linguagens que suportam essa sobrecarga: 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, para que você possa definir operadores como! + * = ++, se desejar. Há suporte interno para infix (para todas as funções que usam um argumento). Eu acho que você também pode definir a associatividade de tais funções. Você não pode, no entanto, definir a precedência (apenas com truques feios, veja aqui ).
fonte
Uma coisa que ainda não foi mencionada é o caso do Smalltalk, onde tudo (incluindo operadores) é uma mensagem enviada. "Operadores" gostam
+
,|
e assim por diante, são realmente métodos unários.Todos os métodos podem ser substituídos, o que
a + b
significa adição de número inteiro sea
eb
são ambos números inteiros e significa adição de vetor se ambos foremOrderedCollection
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ão3 + (4 * 5)
.(Este é um grande obstáculo para os iniciantes no Smalltalk. A quebra das regras da matemática remove um caso especial, para que toda a avaliação do código prossiga uniformemente da esquerda para a direita, tornando a linguagem muito mais simples.
fonte
Você está lutando contra duas coisas aqui:
Na maioria dos idiomas, os operadores não são realmente implementados como funções simples. Eles podem ter algum andaime de função, mas o compilador / tempo de execução está explicitamente ciente de seu significado semântico e de como traduzi-los eficientemente no código da máquina. Isso é muito mais verdadeiro, mesmo quando comparado às funções internas (é por isso que a maioria das implementações também não inclui toda a sobrecarga de chamada de função em sua implementação). A maioria dos operadores é abstração de nível superior em instruções primitivas encontradas em CPUs (razão pela qual em parte a maioria dos operadores é aritmética, booleana ou bit a bit). Você pode modelá-las como funções "especiais" (chamá-las de "primitivas" ou "builtins" ou "nativas" ou o que for), mas fazer isso genericamente requer um conjunto de semânticas muito robustas para definir essas funções especiais. A alternativa é ter operadores internos que semanticamente se parecem com operadores definidos pelo usuário, mas que, de outra forma, invocam caminhos especiais no compilador. Isso está em conflito com a resposta à segunda pergunta ...
Além do problema de tradução automática que mencionei acima, em um nível sintático, os operadores não são realmente diferentes das funções. Suas características distintivas tendem a ser concisas e simbólicas, o que sugere uma característica adicional significativa que eles devem ter para serem úteis: eles devem ter entendido amplamente o significado / semântica para os desenvolvedores. Símbolos curtos não transmitem muito significado, a menos que seja uma mão curta para um conjunto de semânticas que já são entendidas. 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 os nomes de função de uma ou duas letras.
As sobrecargas do operador do C ++ fornecem terreno fértil para examinar isso. A maioria dos "abusos" de sobrecarga de operador ocorre na forma de sobrecargas que quebram parte do contrato semântico amplamente compreendido (um exemplo clássico é uma sobrecarga de operador + de modo que a + b! = B + a, ou onde + modifica um de seus operandos).
Se você observar o Smalltalk, que permite sobrecarga de operador e operadores definidos pelo usuário, você poderá ver como um idioma pode fazê-lo e qual a utilidade dele. No Smalltalk, os operadores são meramente métodos com propriedades sintáticas diferentes (a saber, eles são codificados como binário infix). A linguagem usa "métodos primitivos" para operadores e métodos especiais acelerados. Você descobre que poucos, se houver operadores definidos pelo usuário, são criados e, quando o são, tendem a não se acostumar tanto quanto o autor provavelmente pretendeu que fossem usados. Mesmo o equivalente a uma sobrecarga de operador é raro, porque é principalmente uma perda líquida definir uma nova função como operador em vez de método, pois o último permite a expressão da semântica da função.
fonte
Sempre achei as sobrecargas de operadores em C ++ um atalho conveniente para uma equipe de desenvolvedor único, 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 é fácil. para ferramentas como doxygen se separarem, e as pessoas precisam entender os idiomas para utilizá-los adequadamente.
Às vezes, é muito mais difícil entender do que você esperaria, até. Era uma vez, em um grande projeto C ++ de plataforma cruzada, decidi que seria uma boa idéia normalizar a maneira como os caminhos eram construídos criando um
FilePath
objeto (semelhante aoFile
objeto de Java ), que teria operador / usado para concatenar outro parte do caminho (para que você possa fazer algo comoFile::getHomeDir()/"foo"/"bar"
e faria a coisa certa em todas as nossas plataformas suportadas). Todo mundo que via isso dizia essencialmente: "Que diabos? Divisão de cordas? ... Oh, isso é fofo, mas eu não confio que faça a coisa certa".Da mesma forma, há muitos casos em programação gráfica ou em outras áreas em que a matemática de vetor / matriz acontece muito, e é tentador fazer coisas como Matrix * Matrix, Vector * Vector (ponto), Vector% Vector (cross), Matrix * Vector ( transformação de matriz), vetor Matrix ^ (transformação 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 de matemática de vetores, ela só termina confundir a questão mais com os outros. Simplesmente não vale a pena.
fonte
Sobrecargas de operador são uma má idéia pelo mesmo motivo que sobrecargas de método são uma má idéia: o mesmo símbolo na tela teria significados diferentes, dependendo do que está ao seu redor. Isso dificulta a leitura casual.
Como a legibilidade é um aspecto crítico da capacidade de manutenção, você sempre deve evitar sobrecargas (exceto em alguns casos muito especiais). É muito melhor para cada símbolo (seja operador ou identificador alfanumérico) ter um significado único que seja autônomo.
Para ilustrar: ao ler código desconhecido, se você encontrar um novo identificador alfanum desconhecido, pelo menos terá a vantagem de saber que não o conhece.. Você pode então procurar. Se, no entanto, você vir um identificador ou operador comum do qual saiba o significado, é bem menos provável que note que ele foi sobrecarregado para ter um significado completamente diferente. Para saber quais operadores foram sobrecarregados (em uma base de código que utilizou amplamente a sobrecarga), você precisaria de um conhecimento prático do código completo, mesmo se você quiser apenas ler uma pequena parte dele. Isso dificultaria a atualização de novos desenvolvedores sobre esse código e seria impossível atrair pessoas para um pequeno trabalho. Isso pode ser bom para a segurança do trabalho do programador, mas se você for responsável pelo sucesso da base de código, evite essa prática a todo custo.
Como os operadores são pequenos, 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. A única questão é a legibilidade humana. Como tornar o código compacto não melhora a legibilidade, não há benefício real na compactação. Vá em frente, ocupe o espaço e forneça às operações exclusivas um identificador exclusivo, e seu código terá mais sucesso a longo prazo.
fonte
Dificuldades técnicas para lidar com a precedência e a análise complexa de lado, acho que existem alguns aspectos do que é uma linguagem de programação que deve ser considerada.
Os operadores são tipicamente construções lógicas curtas, bem definidas e documentadas no idioma principal (comparar, atribuir ..). Eles também são geralmente 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 realmente podem fazer sentido na programação normal (>, <, =, + etc.).Minha ideia é que é melhor manter um conjunto de operadores bem definidos em um idioma - depois permitir a sobrecarga do operador desses operadores (dada uma recomendação simples de que os operadores façam a mesma coisa, mas com um tipo de dados personalizado).
Seus casos de uso
~
e|
seriam realmente possíveis com sobrecarga simples 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 novos idiomas. A execução de uma linguagem DSL verdadeira em outra linguagem não é tão difícil usando qualquer uma dessas ferramentas de compilador-compilador. O mesmo vale para o "argumento LUA estendido". Um idioma é provavelmente definido principalmente para resolver problemas de uma maneira específica, não para ser uma base para sub-idiomas (existem exceções).fonte
Outro fator é 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 no idioma ou nos 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 é 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 sequência 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. Normalmente pensamos que o operador é usado em tipos / classes básicos, pois seu comportamento é intuitivo, claro e consistente (quando você estiver familiarizado com o idioma, pelo menos). Pense em tipos como Inteiro, String, ComplexNumbers, etc.
Portanto, mesmo que a definição de operadores possa ser muito útil em alguns casos específicos, sua implementação no mundo real é bastante limitada, pois os 99% dos casos em que essa será uma vitória clara já estão implementados no pacote de idiomas básico.
fonte