Lidando com o desconhecimento dos nomes dos parâmetros de uma função ao chamá-la

13

Aqui está um problema de programação / linguagem em que gostaria de ouvir seus pensamentos.

Desenvolvemos convenções que a maioria dos programadores deve seguir, que não fazem parte da sintaxe das linguagens, mas servem para tornar o código mais legível. É claro que sempre é uma questão de debate, mas há pelo menos alguns conceitos fundamentais que a maioria dos programadores consideram agradáveis. Nomear suas variáveis ​​adequadamente, nomeando em geral, tornando suas linhas não excessivamente longas, evitando funções longas, encapsulamentos, essas coisas.

No entanto, há um problema que ainda não encontrei alguém comentando e que talvez seja o maior do grupo. É o problema dos argumentos serem anônimos quando você chama uma função.

Funções derivam da matemática, onde f (x) tem um significado claro, porque uma função tem uma definição muito mais rigorosa do que normalmente faz na programação. Funções puras em matemática podem fazer muito menos do que na programação e são uma ferramenta muito mais elegante, geralmente usam apenas um argumento (que geralmente é um número) e sempre retornam um valor (também geralmente um número). Se uma função recebe vários argumentos, quase sempre são apenas dimensões extras do domínio da função. Em outras palavras, um argumento não é mais importante que os outros. Eles são explicitamente ordenados, com certeza, mas fora isso, eles não têm ordenação semântica.

Na programação, no entanto, temos mais funções de definição de liberdade e, neste caso, eu argumentaria que não é uma coisa boa. Uma situação comum, você tem uma função definida como esta

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Observando a definição, se a função estiver escrita corretamente, é perfeitamente claro o que é o quê. Ao chamar a função, você pode até ter alguma mágica de conclusão de código / inteligência acontecendo no seu IDE / editor que lhe dirá qual deve ser o próximo argumento. Mas espere. Se eu precisar disso quando realmente estiver escrevendo, não há algo faltando aqui? A pessoa que lê o código não tem o benefício de um IDE e, a menos que salte para a definição, não tem idéia de qual dos dois retângulos passados ​​como argumentos é usado para quê.

O problema vai além disso. Se nossos argumentos vierem de alguma variável local, pode haver situações em que nem sabemos qual é o segundo argumento, pois apenas vemos o nome da variável. Tomemos, por exemplo, esta linha de código

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

Isso é aliviado em várias extensões em idiomas diferentes, mas mesmo em idiomas estritamente digitados e mesmo se você nomear suas variáveis ​​de maneira sensata, nem mesmo menciona o tipo de variável ao passar para a função.

Como geralmente acontece com a programação, existem muitas soluções em potencial para esse problema. Muitos já estão implementados em idiomas populares. Parâmetros nomeados em C #, por exemplo. No entanto, tudo o que sei tem desvantagens significativas. Nomear cada parâmetro em cada chamada de função não pode levar a um código legível. Quase parece que estamos superando as possibilidades que a programação em texto simples nos oferece. Passamos do APENAS texto em quase todas as áreas, mas ainda codificamos o mesmo. Mais informações são necessárias para serem exibidas no código? Adicione mais texto. De qualquer forma, isso está ficando um pouco tangencial, então vou parar por aqui.

Uma resposta que cheguei ao segundo trecho de código é que você provavelmente descompactaria o array para algumas variáveis ​​nomeadas e as usaria, mas o nome da variável pode significar muitas coisas e a maneira como é chamada não indica necessariamente o caminho que deve ser interpretado no contexto da função chamada. No escopo local, você pode ter dois retângulos nomeados leftRectangle e rightRectangle porque é isso que eles representam semanticamente, mas não precisa se estender ao que eles representam quando atribuídos a uma função.

De fato, se suas variáveis ​​são nomeadas no contexto da função chamada, você está introduzindo menos informações do que poderia potencialmente com essa chamada de função e, em algum nível, se isso levar a um código com código pior. Se você possui um procedimento que resulta em um retângulo armazenado em rectForClipping e outro procedimento que fornece retForDrawing, a chamada real para DrawRectangleClipped é apenas uma cerimônia. Uma linha que não significa nada de novo e existe apenas para que o computador saiba exatamente o que você deseja, mesmo que você já o tenha explicado com a sua nomeação. Isso não é uma coisa boa.

Eu realmente adoraria ouvir novas perspectivas sobre isso. Tenho certeza de que não sou o primeiro a considerar isso um problema, então como é resolvido?

Darwin
fonte
2
Estou confuso sobre qual é o problema exato ... parece haver várias idéias aqui, sem saber qual é o seu ponto principal.
FrustratedWithFormsDesigner
1
A documentação para a função deve informar o que os argumentos fazem. Você pode argumentar que alguém que lê o código pode não ter a documentação, mas realmente não sabe o que o código faz e o significado que extrai da leitura é um palpite. Em qualquer contexto em que o leitor precise saber que o código está correto, ele precisará da documentação.
Doval
3
@ Darwin Na programação funcional, todas as funções ainda possuem apenas 1 argumento. Se você precisar passar "vários argumentos", o parâmetro geralmente será uma tupla (se você desejar que eles sejam ordenados) ou um registro (se você não quiser que eles sejam). Além disso, é trivial formar versões especializadas de funções a qualquer momento, para que você possa reduzir o número de argumentos necessários. Desde praticamente todos os linguagem funcional fornece sintaxe para tuplas e registros, agregação de valores é indolor, e você começa a composição de graça (você pode funções da cadeia que tuplas de retorno com aqueles que tomam tuplas.)
Doval
1
@ Bergi As pessoas tendem a generalizar muito mais em FP puro, então acho que as funções em si são geralmente menores e mais numerosas. Eu poderia estar longe embora. Não tenho muita experiência trabalhando em projetos reais com Haskell e a turma.
Darwin
4
Acho que a resposta é "Não nomeie suas variáveis ​​'deserializedArray'"?
Whatsisname 21/05

Respostas:

10

Concordo que o modo como as funções são frequentemente usadas pode ser uma parte confusa da escrita do código e, principalmente, da leitura do código.

A resposta para esse problema depende parcialmente do idioma. Como você mencionou, o C # nomeou parâmetros. A solução da Objective-C para esse problema envolve nomes de métodos mais descritivos. Por exemplo, stringByReplacingOccurrencesOfString:withString:é um método com parâmetros claros.

No Groovy, algumas funções usam mapas, permitindo uma sintaxe como a seguinte:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

Em geral, você pode resolver esse problema limitando o número de parâmetros que você passa para uma função. Eu acho que 2-3 é um bom limite. Se parece que uma função precisa de mais parâmetros, isso me leva a repensar o design. Mas, isso pode ser mais difícil de responder em geral. Às vezes você está tentando fazer muito em uma função. Às vezes, faz sentido considerar uma classe para armazenar seus parâmetros. Além disso, na prática, muitas vezes acho que funções que usam um grande número de parâmetros normalmente têm muitos deles como opcionais.

Mesmo em uma linguagem como Objective-C, faz sentido limitar o número de parâmetros. Uma razão é que muitos parâmetros são opcionais. Por exemplo, consulte rangeOfString: e suas variações no NSString .

Um padrão que costumo usar em Java é usar uma classe de estilo fluente como parâmetro. Por exemplo:

something.draw(new Box().withHeight(5).withWidth(20))

Isso usa uma classe como parâmetro e, com uma classe de estilo fluente, cria um código facilmente legível.

O snippet Java acima também ajuda onde a ordem dos parâmetros pode não ser tão óbvia. Normalmente assumimos com coordenadas que X vem antes de Y. E normalmente vejo a altura antes da largura como uma convenção, mas isso ainda não é muito claro ( something.draw(5, 20)).

Eu também vi algumas funções como, drawWithHeightAndWidth(5, 20)mas mesmo essas não podem ter muitos parâmetros, ou você começa a perder a legibilidade.

David V
fonte
2
A ordem, se você continuar no exemplo de Java, pode realmente ser muito complicada. Por exemplo, compare os seguintes construtores de awt: Dimension(int width, int height)e GridLayout(int rows, int cols)(o número de linhas é a altura, o significado GridLayouttem a altura primeiro e Dimensiona largura).
Pierre Arlaud
1
Tais inconsistências também têm sido muito criticado com PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), por exemplo: array_filter($input, $callback)contra array_map($callback, $input), strpos($haystack, $needle)contraarray_search($needle, $haystack)
Pierre Arlaud
12

É resolvido principalmente pela boa nomeação de funções, parâmetros e argumentos. Você já explorou isso e descobriu que tinha deficiências, no entanto. A maioria dessas deficiências é atenuada, mantendo as funções pequenas, com um pequeno número de parâmetros, tanto no contexto de chamada quanto no contexto de chamada. Seu exemplo específico é problemático porque a função que você está chamando está tentando fazer várias coisas ao mesmo tempo: especifique um retângulo base, especifique uma região de recorte, desenhe-a e preencha-a com uma cor específica.

É como tentar escrever uma frase usando apenas os adjetivos. Coloque mais verbos (chamadas de função) lá, crie um assunto (objeto) para sua frase e é mais fácil ler:

rect.clip(clipRect).fill(color)

Mesmo se clipRecte colortiver nomes terríveis (e eles não deveriam), você ainda pode discernir seus tipos do contexto.

Seu exemplo desserializado é problemático porque o contexto de chamada está tentando fazer muita coisa de uma só vez: desserializando e desenhando alguma coisa. Você precisa atribuir nomes que façam sentido e separem claramente as duas responsabilidades. No mínimo, algo como isto:

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

Muitos problemas de legibilidade são causados ​​por tentar ser muito conciso, ignorando os estágios intermediários que os humanos precisam para discernir o contexto e a semântica.

Karl Bielefeldt
fonte
1
Eu gosto da ideia de encadear várias chamadas de função para esclarecer o significado, mas isso não é apenas uma questão de dançar? É basicamente "Eu quero escrever uma frase, mas a língua que eu estou processando não vai me deixar, então eu só pode usar o equivalente mais próximo"
Darwin
@ Darwin IMHO, não é como se isso pudesse ser melhorado, tornando a linguagem de programação mais parecida com a linguagem natural. As línguas naturais são muito ambíguas e só podemos entendê-las em contexto e, na verdade, nunca podemos ter certeza. Ativar chamadas de função é muito melhor, pois todo termo (idealmente) possui documentação e fontes disponíveis e temos parênteses e pontos que tornam a estrutura clara.
maaartinus 23/01
3

Na prática, é resolvido com um design melhor. É excepcionalmente incomum que funções bem escritas obtenham mais de 2 entradas e, quando isso ocorre, é incomum que muitas dessas entradas não possam ser agregadas a algum pacote coeso. Isso facilita bastante a quebra de funções ou a agregação de parâmetros, para que você não faça com que uma função faça muito. Um tem duas entradas, fica fácil nomear e muito mais claro sobre qual entrada é qual.

Minha linguagem de brinquedo tinha o conceito de frases para lidar com isso, e outras linguagens de programação mais focadas em linguagem natural tiveram outras abordagens para lidar com isso, mas todas elas tendem a ter outras desvantagens. Além disso, mesmo as frases são pouco mais que uma boa sintaxe para tornar as funções com nomes melhores. Sempre será difícil criar um bom nome de função quando são necessárias várias entradas.

Telastyn
fonte
As frases realmente parecem um passo à frente. Eu sei que alguns idiomas têm recursos semelhantes, mas é MUITO difundido. Sem mencionar, com todo o ódio por macro vindo dos puristas de C (++) que nunca usaram macros corretamente, talvez nunca tenhamos recursos como esses em linguagens populares.
Darwin
Bem-vindo ao tópico geral de idiomas específicos de domínio , algo que eu realmente desejo que mais pessoas entendam a vantagem de ... (+1)
Izkata
2

Em Javascript (ou ECMAScript ), por exemplo, muitos programadores se acostumaram a

passando parâmetros como um conjunto de propriedades de objetos nomeados em um único objeto anônimo.

E, como prática de programação, passou de programadores para suas bibliotecas e de lá para outros programadores que passaram a gostar, usar e escrever mais bibliotecas, etc.

Exemplo

Em vez de ligar

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

como isso:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, que é um estilo válido e correto, você chama o

function drawRectangleClipped (params)

como isso:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, que é válido, correto e agradável em relação à sua pergunta.

Obviamente, é necessário que haja condições adequadas para isso - em Javascript isso é muito mais viável do que em C. digamos C. Em javascript, isso deu origem a notação estrutural amplamente usada que se tornou popular como uma contraparte mais leve do XML. Chama-se JSON (você já deve ter ouvido falar sobre isso).

Pavel
fonte
Não sei o suficiente sobre esse idioma para verificar a sintaxe, mas, em geral, gosto deste post. Parece muito elegante. +1
IT Alex
Freqüentemente, isso é combinado com argumentos normais, ou seja, há 1 a 3 argumentos seguidos por params(geralmente contendo argumentos opcionais e geralmente opcionais) como, por exemplo, esta função . Isso torna as funções com muitos argumentos bastante fáceis de entender (no meu exemplo, existem 2 argumentos obrigatórios e 6 opções).
maaartinus 23/01
0

Você deve usar o objetivo-C, então, aqui está uma definição de função:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

E aqui é usado:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Acho que o ruby ​​tem construções semelhantes e você pode simulá-las em outros idiomas com listas de valores-chave.

Para funções complexas em Java, eu gosto de definir variáveis ​​fictícias na redação das funções. Para o seu exemplo esquerdo-direito:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Parece mais codificação, mas você pode, por exemplo, usar leftRectangle e refatorar o código posteriormente com "Extrair variável local" se achar que não será compreensível para um mantenedor futuro do código, que pode ou não ser você.

awsm
fonte
Sobre esse exemplo de Java, escrevi na pergunta por que acho que não é uma boa solução. O que você acha disso?
Darwin
0

Minha abordagem é criar variáveis ​​locais temporárias - mas não apenas chamá-las LeftRectangee RightRectangle. Em vez disso, uso nomes um pouco mais longos para transmitir mais significado. Costumo tentar diferenciar os nomes o máximo possível, por exemplo, não chamar os dois something_rectangle, se o papel deles não for muito simétrico.

Exemplo (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

e posso até escrever uma função ou modelo de wrapper de uma linha:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(ignore oe comercial, se você não conhece C ++).

Se a função fizer várias coisas estranhas sem uma boa descrição - provavelmente precisará ser refatorada!

einpoklum
fonte