Parâmetros nomeados tornam o código mais fácil de ler e mais difícil de escrever
Quando estou lendo um pedaço de código, os parâmetros nomeados podem introduzir um contexto que facilita a compreensão do código. Considere, por exemplo, este construtor: Color(1, 102, 205, 170)
. O que diabos isso significa? Na verdade, Color(alpha: 1, red: 102, green: 205, blue: 170)
seria muito mais fácil de ler. Mas, infelizmente, o Compilador diz "não" - ele quer Color(a: 1, r: 102, g: 205, b: 170)
. Ao escrever código usando parâmetros nomeados, você gasta um tempo desnecessário pesquisando os nomes exatos - é mais fácil esquecer os nomes exatos de alguns parâmetros do que esquecer sua ordem.
Isso me incomodou ao usar uma DateTime
API que tinha duas classes de irmãos por pontos e durações com interfaces quase idênticas. Enquanto DateTime->new(...)
aceitou um second => 30
argumento, o DateTime::Duration->new(...)
procurado seconds => 30
e semelhante para outras unidades. Sim, faz absolutamente sentido, mas isso me mostrou que os parâmetros nomeados são fáceis de usar.
Nomes ruins nem facilitam a leitura
Outro exemplo de como os parâmetros nomeados podem ser ruins é provavelmente a linguagem R. Este pedaço de código cria um gráfico de dados:
plot(plotdata$n, plotdata$mu, type="p", pch=17, lty=1, bty="n", ann=FALSE, axes=FALSE)
Você vê dois argumentos posicionais para o x e y linhas de dados, e em seguida uma lista de parâmetros nomeados. Existem muito mais opções com padrões, e apenas aquelas listadas cujos padrões eu queria alterar ou especificar explicitamente. Depois que ignoramos que esse código usa números mágicos e que podemos nos beneficiar do uso de enumerações (se R tiver algum!), O problema é que muitos desses nomes de parâmetros são indecifráveis.
pch
é realmente o caractere da plotagem, o glifo que será desenhado para cada ponto de dados. 17
é um círculo vazio, ou algo assim.
lty
é o tipo de linha. Aqui 1
está uma linha sólida.
bty
é o tipo de caixa. A configuração para "n"
evitar que uma caixa seja desenhada ao redor da plotagem.
ann
controla a aparência das anotações do eixo.
Para alguém que não sabe o que significa cada abreviação, essas opções são bastante confusas. Isso também revela por que o R usa esses rótulos: não como código de auto-documentação, mas (sendo uma linguagem de tipo dinâmico) como chaves para mapear os valores para suas variáveis corretas.
Propriedades de parâmetros e assinaturas
As assinaturas de funções podem ter as seguintes propriedades:
- Os argumentos podem ser ordenados ou não ordenados,
- nomeado ou sem nome,
- obrigatório ou opcional.
- As assinaturas também podem ser sobrecarregadas por tamanho ou tipo,
- e pode ter um tamanho não especificado com varargs.
Idiomas diferentes chegam a coordenadas diferentes deste sistema. Em C, os argumentos são ordenados, sem nome, sempre necessários e podem ser varargs. Em Java, a situação é semelhante, exceto que as assinaturas podem ser sobrecarregadas. No Objetivo C, as assinaturas são ordenadas, nomeadas, necessárias e não podem ser sobrecarregadas porque é apenas açúcar sintático em torno de C.
Linguagens digitadas dinamicamente com varargs (interfaces de linha de comando, Perl,…) podem emular parâmetros nomeados opcionais. Os idiomas com sobrecarga de tamanho de assinatura têm algo como parâmetros opcionais posicionais.
Como não implementar parâmetros nomeados
Ao pensar em parâmetros nomeados, geralmente assumimos parâmetros nomeados, opcionais e não ordenados. Implementar isso é difícil.
Parâmetros opcionais podem ter valores padrão. Eles devem ser especificados pela função chamada e não devem ser compilados no código de chamada. Caso contrário, os padrões não poderão ser atualizados sem recompilar todo o código dependente.
Agora, uma pergunta importante é como os argumentos são realmente passados para a função. Com parâmetros ordenados, os argumentos podem ser passados em um registro ou em sua ordem inerente na pilha. Quando excluímos os registros por um momento, o problema é como colocar argumentos opcionais não ordenados na pilha.
Para isso, precisamos de alguma ordem sobre os argumentos opcionais. E se o código da declaração for alterado? Como a ordem é irrelevante, uma nova ordem na declaração da função não deve alterar a posição dos valores na pilha. Também devemos considerar se é possível adicionar um novo parâmetro opcional. Da perspectiva dos usuários, parece que sim, porque o código que não usava esse parâmetro anteriormente ainda deve funcionar com o novo parâmetro. Portanto, isso exclui pedidos como usar o pedido na declaração ou usar o pedido alfabético.
Considere isso também à luz da subtipagem e do Princípio de Substituição de Liskov - na saída compilada, as mesmas instruções devem poder invocar o método em um subtipo com possíveis parâmetros nomeados e em um supertipo.
Possíveis implementações
Se não podemos ter uma ordem definitiva, precisamos de alguma estrutura de dados não ordenada.
A implementação mais simples é simplesmente passar o nome dos parâmetros junto com os valores. É assim que os parâmetros nomeados são emulados no Perl ou com as ferramentas de linha de comando. Isso resolve todos os problemas de extensão mencionados acima, mas pode ser um enorme desperdício de espaço - não uma opção no código crítico de desempenho. Além disso, o processamento desses parâmetros nomeados agora é muito mais complicado do que simplesmente retirar valores de uma pilha.
Na verdade, os requisitos de espaço podem ser reduzidos usando o pool de strings, o que pode reduzir comparações posteriores de strings a comparações de ponteiros (exceto quando não é possível garantir que as strings estáticas sejam realmente agrupadas, nesse caso, as duas strings terão que ser comparadas em detalhe).
Em vez disso, também poderíamos passar uma estrutura de dados inteligente que funciona como um dicionário de argumentos nomeados. Isso é barato para o chamador, porque o conjunto de teclas é estaticamente conhecido no local da chamada. Isso permitiria criar uma função de hash perfeita ou pré-calcular um trie. O receptor ainda terá que testar a existência de todos os nomes de parâmetros possíveis, o que é um pouco caro. Algo assim é usado pelo Python.
Portanto, é muito caro na maioria dos casos
Se uma função com parâmetros nomeados deve ser adequadamente extensível, uma ordem definitiva não pode ser assumida. Portanto, existem apenas duas soluções:
- Torne a ordem dos parâmetros nomeados parte da assinatura e não permita alterações posteriores. Isso é útil para código de auto-documentação, mas não ajuda com argumentos opcionais.
- Passe uma estrutura de dados de valor-chave para o receptor, que precisará extrair informações úteis. Isso é muito caro em comparação e geralmente é visto apenas em linguagens de script sem ênfase no desempenho.
Outras armadilhas
Os nomes de variáveis em uma declaração de função geralmente têm algum significado interno e não fazem parte da interface - mesmo que muitas ferramentas de documentação ainda os mostrem. Em muitos casos, você deseja nomes diferentes para uma variável interna e o argumento nomeado correspondente. Os idiomas que não permitem escolher os nomes visíveis externamente de um parâmetro nomeado não os ganham muito se o nome da variável não for usado com o contexto de chamada em mente.
Um problema com emulações de argumentos nomeados é a falta de verificação estática no lado do chamador. Isso é especialmente fácil de esquecer ao passar um dicionário de argumentos (olhando para você, Python). Isto é importante porque a passagem de um dicionário é uma solução comum, por exemplo, em JavaScript: foo({bar: "baz", qux: 42})
. Aqui, nem os tipos dos valores nem a existência ou ausência de determinados nomes podem ser verificados estaticamente.
Emulando parâmetros nomeados (em idiomas estaticamente tipados)
O simples uso de strings como chaves e qualquer objeto como valor não é muito útil na presença de um verificador de tipo estático. No entanto, argumentos nomeados podem ser emulados com estruturas ou literais de objeto:
// Java
static abstract class Arguments {
public String bar = "default";
public int qux = 0;
}
void foo(Arguments args) {
...
}
/* using an initializer block */
foo(new Arguments(){{ bar = "baz"; qux = 42; }});