Affrontare il fatto di non conoscere i nomi dei parametri di una funzione quando la si chiama


13

Ecco un problema di programmazione / linguaggio su cui vorrei sentire le tue opinioni.

Abbiamo sviluppato convenzioni che la maggior parte dei programmatori (dovrebbero) seguire e che non fanno parte della sintassi delle lingue ma servono a rendere il codice più leggibile. Questi sono ovviamente sempre oggetto di dibattito, ma ci sono almeno alcuni concetti chiave che la maggior parte dei programmatori trova gradevoli. Denominare le variabili in modo appropriato, denominare in generale, rendere le linee non eccessivamente lunghe, evitando lunghe funzioni, incapsulamenti, quelle cose.

Tuttavia, c'è un problema che devo ancora trovare qualcuno che commenta e che potrebbe essere il più grande del gruppo. È il problema degli argomenti che sono anonimi quando chiami una funzione.

Le funzioni derivano dalla matematica dove f (x) ha un chiaro significato perché una funzione ha una definizione molto più rigorosa di quella che di solito fa in programmazione. Le funzioni pure in matematica possono fare molto meno di quanto possano fare in programmazione e sono uno strumento molto più elegante, di solito prendono solo un argomento (che di solito è un numero) e restituiscono sempre un valore (anche di solito un numero). Se una funzione accetta più argomenti, sono quasi sempre solo dimensioni extra del dominio della funzione. In altre parole, un argomento non è più importante degli altri. Sono esplicitamente ordinati, certo, ma a parte questo, non hanno un ordine semantico.

Nella programmazione tuttavia, abbiamo più funzioni di definizione della libertà, e in questo caso direi che non è una buona cosa. Una situazione comune, hai una funzione definita come questa

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Guardando la definizione, se la funzione è scritta correttamente, è perfettamente chiaro che cosa. Quando chiami la funzione, potresti anche avere un po 'di magia di completamento di intellisense / codice in corso nel tuo IDE / editor che ti dirà quale dovrebbe essere il prossimo argomento. Ma aspetta. Se ne ho bisogno quando scrivo davvero la chiamata, non c'è qualcosa che ci manca qui? La persona che legge il codice non ha il vantaggio di un IDE e, a meno che non salti alla definizione, non ha idea di quale dei due rettangoli passati come argomenti viene utilizzato per cosa.

Il problema va anche oltre. Se i nostri argomenti provengono da alcune variabili locali, potrebbero esserci situazioni in cui non sappiamo nemmeno quale sia il secondo argomento poiché vediamo solo il nome della variabile. Prendi ad esempio questa riga di codice

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

Questo è alleviato in varia misura in diverse lingue ma anche in linguaggi tipicamente rigorosi e anche se si nominano le variabili in modo sensibile, non si menziona nemmeno il tipo di variabile quando si passa alla funzione.

Come di solito accade con la programmazione, ci sono molte potenziali soluzioni a questo problema. Molti sono già implementati in lingue popolari. Parametri nominati in C # per esempio. Tuttavia, tutto ciò che conosco presenta degli svantaggi significativi. La denominazione di ogni parametro su ogni chiamata di funzione non può portare a codice leggibile. Sembra quasi che stiamo superando le possibilità che ci offre la programmazione in testo semplice. Siamo passati dal testo SOLO in quasi tutte le aree, eppure continuiamo a scrivere lo stesso codice. Ulteriori informazioni sono necessarie per essere visualizzate nel codice? Aggiungi altro testo. Comunque, questo sta diventando un po 'tangenziale, quindi mi fermo qui.

Una risposta che ho ricevuto al secondo frammento di codice è che probabilmente prima dovresti decomprimere l'array in alcune variabili con nome e poi utilizzarle, ma il nome della variabile può significare molte cose e il modo in cui viene chiamato non ti dice necessariamente come dovrebbe essere interpretato nel contesto della funzione chiamata. Nell'ambito locale, potresti avere due rettangoli chiamati leftRectangle e rightRectangle perché è quello che rappresentano semanticamente, ma non è necessario estenderlo a ciò che rappresentano quando viene assegnato a una funzione.

In effetti, se le variabili sono nominate nel contesto della funzione chiamata di quanto stai introducendo meno informazioni di quelle che potresti potenzialmente con quella chiamata di funzione e ad un certo livello se portano a un codice peggiore del codice. Se si dispone di una procedura che risulta in un rettangolo memorizzato in rectForClipping e quindi in un'altra procedura che fornisce rectForDrawing, la chiamata effettiva a DrawRectangleClipped è solo una cerimonia. Una linea che non significa nulla di nuovo ed è lì solo per far sì che il computer sappia esattamente cosa vuoi, anche se l'hai già spiegato con il tuo nome. Questa non è una buona cosa.

Mi piacerebbe davvero sentire nuove prospettive su questo. Sono sicuro di non essere il primo a considerare questo un problema, quindi come viene risolto?


2
Sono confuso su quale sia esattamente il problema ... sembrano esserci diverse idee qui, non sono sicuro di quale sia il tuo punto principale.
FrustratedWithFormsDesigner

1
La documentazione per la funzione dovrebbe dirti cosa fanno gli argomenti. Potresti obiettare che qualcuno che legge il codice potrebbe non avere la documentazione, ma poi non sai davvero cosa fa il codice e qualunque significato estraggano dalla lettura sia un'ipotesi colta. In qualsiasi contesto in cui il lettore deve sapere che il codice è corretto, avrà bisogno della documentazione.
Doval,

3
@Darwin Nella programmazione funzionale tutte le funzioni hanno ancora solo 1 argomento. Se è necessario passare "più argomenti", il parametro è di solito una tupla (se si desidera che vengano ordinati) o un record (se non si desidera che siano). Inoltre, è banale creare versioni specializzate di funzioni in qualsiasi momento, in modo da poter ridurre il numero di argomenti necessari. Poiché praticamente tutti i linguaggi funzionali forniscono sintassi per tuple e record, raggruppare i valori è indolore e si ottiene la composizione gratuitamente (è possibile concatenare funzioni che restituiscono tuple con quelle che prendono tuple.)
Doval

1
@Bergi Le persone tendono a generalizzare molto di più in FP puro, quindi penso che le funzioni stesse siano generalmente più piccole e più numerose. Potrei essere lontano però. Non ho molta esperienza di lavoro su progetti reali con Haskell e la banda.
Darwin,

4
Penso che la risposta sia "Non nominare le variabili 'deserializedArray'"?
whatsisname

Risposte:


10

Sono d'accordo che il modo in cui le funzioni vengono spesso utilizzate può essere una parte confusa della scrittura del codice, e in particolare della lettura del codice.

La risposta a questo problema dipende in parte dalla lingua. Come hai detto, C # ha chiamato i parametri. La soluzione di Objective-C a questo problema comporta nomi di metodi più descrittivi. Ad esempio, stringByReplacingOccurrencesOfString:withString:è un metodo con parametri chiari.

In Groovy, alcune funzioni accettano mappe, consentendo una sintassi come la seguente:

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

In generale, è possibile risolvere questo problema limitando il numero di parametri passati a una funzione. Penso che 2-3 sia un buon limite. Se sembra che una funzione abbia bisogno di più parametri, mi fa ripensare il progetto. Tuttavia, può essere più difficile rispondere in generale. A volte stai provando a fare troppo in una funzione. A volte ha senso considerare una classe per la memorizzazione dei parametri. Inoltre, in pratica, trovo spesso che funzioni che accettano un gran numero di parametri normalmente ne abbiano molte come optional.

Anche in un linguaggio come Objective-C ha senso limitare il numero di parametri. Uno dei motivi è che molti parametri sono opzionali. Per un esempio, vedi rangeOfString: e le sue variazioni in NSString .

Un modello che uso spesso in Java è l'uso di una classe di stile fluente come parametro. Per esempio:

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

Questo utilizza una classe come parametro e con una classe di stile fluente, rende il codice facilmente leggibile.

Lo snippet Java sopra aiuta anche dove l'ordinamento dei parametri potrebbe non essere così ovvio. Normalmente assumiamo con coordinate che X precede Y. E normalmente vedo l'altezza prima della larghezza come una convenzione, ma ciò non è ancora molto chiaro ( something.draw(5, 20)).

Ho anche visto alcune funzioni come, drawWithHeightAndWidth(5, 20)ma anche queste non possono accettare troppi parametri, o inizieresti a perdere la leggibilità.


2
L'ordine, se continui sull'esempio Java, può davvero essere molto complicato. Ad esempio, confronta i seguenti costruttori con awt: Dimension(int width, int height)e GridLayout(int rows, int cols)(il numero di righe è l'altezza, il che significa che GridLayoutl'altezza è prima e Dimensionla larghezza).
Pierre Arlaud,

1
Tali incoerenze sono state anche molto criticate con PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), ad esempio: array_filter($input, $callback)contro array_map($callback, $input), strpos($haystack, $needle)controarray_search($needle, $haystack)
Pierre Arlaud

12

Principalmente è risolto da una buona denominazione di funzioni, parametri e argomenti. Lo hai già esplorato e hai riscontrato carenze, tuttavia. La maggior parte di queste carenze viene mitigata mantenendo piccole le funzioni, con un piccolo numero di parametri, sia nel contesto chiamante che nel contesto chiamato. Il tuo esempio particolare è problematico perché la funzione che stai chiamando sta provando a fare diverse cose contemporaneamente: specifica un rettangolo di base, specifica una regione di ritaglio, disegnala e riempila con un colore specifico.

È un po 'come provare a scrivere una frase usando solo gli aggettivi. Inserisci più verbi (chiamate di funzione) lì dentro, crea un soggetto (oggetto) per la tua frase ed è più facile da leggere:

rect.clip(clipRect).fill(color)

Anche se clipRecte colorhanno nomi terribili (e non dovrebbero), puoi comunque discernere i loro tipi dal contesto.

Il tuo esempio deserializzato è problematico perché il contesto chiamante sta cercando di fare troppo in una volta: deserializzare e disegnare qualcosa. È necessario assegnare nomi che abbiano un senso e separare chiaramente le due responsabilità. Come minimo, qualcosa del genere:

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

Molti problemi di leggibilità sono causati dal tentativo di essere troppo concisi, saltando le fasi intermedie che gli umani richiedono per discernere il contesto e la semantica.


1
Mi piace l'idea di mettere insieme più chiamate di funzione per chiarire il significato, ma non è solo ballare attorno al problema? È fondamentalmente "Voglio scrivere una frase, ma la lingua che sto citando non mi permette di usare solo l'equivalente più vicino"
Darwin,

@Darwin IMHO non è che questo potrebbe essere migliorato rendendo il linguaggio di programmazione più simile al linguaggio naturale. I linguaggi naturali sono molto ambigui e possiamo capirli solo nel contesto e in realtà non possiamo mai esserne sicuri. La stringa di chiamate di funzioni è molto migliore in quanto ogni termine (idealmente) ha documentazione e fonti disponibili e abbiamo parentesi e punti che chiariscono la struttura.
Maaartinus,

3

In pratica, è risolto da un design migliore. È eccezionalmente raro che funzioni ben scritte prendano più di 2 input, e quando ciò accade, non è comune che quei molti input non siano in grado di essere aggregati in un insieme coerente. Ciò semplifica abbastanza la suddivisione di funzioni o parametri aggregati in modo da non fare troppo una funzione. Uno ha due input, diventa facile nominare e molto più chiaro su quale input sia quale.

Il mio linguaggio del giocattolo aveva il concetto di frasi per affrontare questo, e altri linguaggi di programmazione più focalizzati sul linguaggio naturale hanno avuto altri approcci per affrontarlo, ma tutti tendono ad avere altri aspetti negativi. Inoltre, anche le frasi sono poco più che una bella sintassi per rendere le funzioni con nomi migliori. Sarà sempre difficile fare un buon nome di funzione quando ci vogliono un sacco di input.


Le frasi sembrano davvero un passo avanti. So che alcune lingue hanno capacità simili, ma è molto diffusa. Per non parlare del fatto che tutti i macro odio che provengono dai puristi C (++) che non hanno mai usato le macro fatte bene, non potremmo mai avere caratteristiche come queste nei linguaggi popolari.
Darwin,

Benvenuti nell'argomento generale delle lingue specifiche del dominio , qualcosa che vorrei davvero che più persone capissero il vantaggio di ... (+1)
Izkata

2

In Javascript (o ECMAScript ), ad esempio, molti programmatori si sono abituati

passando parametri come un insieme di proprietà dell'oggetto denominato in un singolo oggetto anonimo.

E come pratica di programmazione è arrivata dai programmatori alle loro librerie e da lì ad altri programmatori che sono cresciuti per apprezzarlo e usarlo e scrivere qualche altra libreria ecc.

Esempio

Invece di chiamare

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

come questo:

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

, che è uno stile valido e corretto, si chiama il

function drawRectangleClipped (params)

come questo:

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

, che è valido, corretto e gradevole per quanto riguarda la tua domanda.

Ovviamente, ci devono essere condizioni adeguate per questo - in Javascript questo è molto più praticabile che in, diciamo, C. In javascript, questo ha persino dato vita a notazioni strutturali ora ampiamente utilizzate che sono diventate popolari come controparte più leggera di XML. Si chiama JSON (potresti averne già sentito parlare).


Non so abbastanza su quella lingua per verificare la sintassi ma, nel complesso, mi piace questo post. Sembra piuttosto elegante. +1
IT Alex

Abbastanza spesso, questo viene combinato con argomenti normali, vale a dire, ci sono 1-3 argomenti seguiti da params(spesso contenenti argomenti opzionali e spesso esso stesso facoltativo) come ad esempio questa funzione . Questo rende le funzioni con molti argomenti abbastanza facili da comprendere (nel mio esempio ci sono 2 argomenti obbligatori e 6 opzioni).
Maaartinus,

0

Dovresti usare goal-C quindi, ecco una definizione di funzione:

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

E qui è usato:

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

Penso che ruby ​​abbia costrutti simili e puoi simularli in altre lingue con elenchi di valori-chiave.

Per le funzioni complesse in Java mi piace definire variabili fittizie nella formulazione delle funzioni. Per il tuo esempio sinistro-destro:

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

Sembra più codifica, ma puoi ad esempio usare leftRectangle e poi refactificare il codice in seguito con "Estrai variabile locale" se pensi che non sarà comprensibile per un futuro manutentore del codice, che potresti o meno essere tu.


A proposito di questo esempio Java, ho scritto nella domanda perché penso che non sia una buona soluzione. Cosa ne pensi di questo?
Darwin,

0

Il mio approccio è creare variabili locali temporanee, ma non solo chiamarle LeftRectangee RightRectangle. Piuttosto, uso nomi un po 'più lunghi per trasmettere più significato. Cerco spesso di differenziare il più possibile i nomi, ad es. Non chiamarli entrambi something_rectangle, se il loro ruolo non è molto simmetrico.

Esempio (C ++):

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

e potrei anche scrivere una funzione o un modello di wrapper a riga singola:

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)
}

(ignora le e commerciali se non conosci C ++).

Se la funzione fa diverse cose strane senza una buona descrizione, allora probabilmente deve essere riformulata!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.