Stile di codifica per chiamate di funzioni concatenate

5

Una cosa comune che devi fare è prendere un valore, fare qualcosa con esso passando ad una funzione, e poi fare di più con il valore restituito, in una catena. Ogni volta che mi imbatto in questo tipo di scenario, non sono sicuro del modo migliore per scrivere il codice.

Ad esempio, diciamo che ho un numero, num , e ho bisogno di prendere la radice quadrata del pavimento e trasformarla in una stringa. Ecco alcune possibilità (in JavaScript).

Potrei semplicemente passare num a una funzione alla volta e sostituire num con il valore restituito:

function floorSqrt(num) {
    num = Math.sqrt(num);
    num = Math.floor(num);
    num = String(num);
    return num;
}

Questo è bello dal momento che non ho più bisogno del% originale dinum, ma in pratica potrebbe essere fonte di confusione iniziare con una cosa (un numero) e finire con qualcosa di completamente diverso (una stringa).

Posso salvare ogni passaggio in una nuova variabile:

function floorSqrt(num) {
    var sqrtNum = Math.sqrt(num);
    var floorSqrt = Math.floor(sqrtNum);
    var stringNum = String(floorSqrt);
    return stringNum;
}

Anche se sembra uno spreco dichiarare tutte quelle variabili e trovare un buon nome per loro.

Oppure posso semplicemente fare tutto come one-liner :

function floorSqrt(num) {
    return String(Math.floor(Math.sqrt(num)));
}

Ma per più di due funzioni questo approccio diventa incredibilmente illeggibile, quasi come codice golf .

Mi sembra che il modo più bello per farlo sia con un linguaggio basato sullo stack, che potrebbe apparire simile a questo (pseudo codice):

[sqrt floor toString]

C'è un modo per fare qualcosa di simile con JavaScript? Elenca alcune funzioni, eseguine una alla volta e usa il valore di ritorno di ognuna come argomento per il prossimo? Il più vicino è il modo in cui alcune librerie come jQuery o Underscore.js ti consente di concatenare i metodi, in modo simile a questo:

function floorSqrt(num) {
    return _.chain(num).sqrt().floor().toString().value();
}

Sono sicuro che molte persone sagge hanno pensato cose saggi su questo. Quali sono le riflessioni sui pro e contro dei diversi stili?

    
posta last-child 02.09.2013 - 18:12
fonte

6 risposte

15

TL; DR: l'idioma di programmazione funzionale Reduce / Compose

# define/import: compose(fn1, f2)(arg) => fn2(fn1(arg)) 
# define/import: reduce(fn, [l1, l2, l3, ...]) => fn(fn(l1, l2), l3)...

commands = [Math.sqrt, Math.floor, String]
floorSqrt = reduce(compose, commands)

# floorSqrt(2) == String(Math.floor(Math.sqrt(2)))

Note:

L'utilizzo di reduce e compose è un idioma molto comune utilizzato quando si desidera impostare una pipeline di trasformazioni per il passaggio dei dati. È sia leggibile che elegante.

< rant > Ci sono troppi programmatori che confondono la leggibilità con la familiarità e viceversa. Credono che un idioma non familiare sia illeggibile, come l'idioma di ridurre / comporre solo perché non è familiare. Per me questo comportamento è più in linea con un diletante, non con qualcuno che è serio riguardo al loro mestiere. < / rant >

    
risposta data 09.01.2014 - 08:47
fonte
9

Il nostro obiettivo come ingegneri del software dovrebbe essere quello di scrivere un codice che sia chiaro e comprensibile per gli altri esseri umani, e sono felice che tu abbia già realizzato che scrivere troppe chiamate di funzioni annidate ti porta a un codice difficile da leggere.

Quindi, supponendo che la tua lingua non supporti la sintassi simile a jQuery che hai menzionato alla fine, sembra che l'unica buona alternativa rimasta sia quella di suddividere complesse chiamate a catena di funzioni in affermazioni più semplici dove ciascuna assegna un valore temporaneo a un intermediario variabile. Così ora ti restano solo due scelte: 1) riutilizzare lo stesso nome di variabile per ogni passaggio e 2) definire una nuova variabile locale per ogni passaggio.

E presumendo che tu sia arrivato alla mia stessa conclusione, ti aiuterò a fare la scelta finale: Utilizza nuovi nomi di variabili per ogni passaggio

L'argomento "è dispendioso dichiarare 5 variabili quando posso usarne uno" è un po 'di ottimizzazione prematura. Ho inserito ragazzi immersi nella mia squadra e questa era una delle loro abitudini che avrei sempre dovuto rompere. Ogni variabile dovrebbe avere a) un nome significativo eb) sempre lo stesso significato. Quindi, quando si legge il codice, non deve mai indovinare cosa significhi "num", specialmente perché il codice tende a non rimanere semplice come il tuo esempio. Immagina che qualcun altro arrivi a lungo, aggiunga qualche loops e if-statement e continui a riutilizzare la tua variabile "num". Ora per capire cosa c'è in esso, dovresti risalire indietro ed eseguire la ginnastica mentale nel tentativo di determinare solo quale affermazione è stata colpita e quante volte è stato "num" riassegnato.

Supponendo che non si stia eseguendo la programmazione integrata su una scheda con 8 KB di RAM, non c'è motivo di riutilizzare la stessa variabile più volte per più scopi. Vieni con nomi chiari e chiari e usali. Quando lo stack si srotola, costa esattamente lo stesso importo per rimuovere 1 int dallo stack come necessario per rimuovere 6 pollici.

    
risposta data 02.09.2013 - 23:22
fonte
5

Se non sei limitato a Javascript, molti linguaggi di programmazione funzionale ti consentono di utilizzare un approccio simile a quello suggerito da dietbuddha (operatore di composizione delle funzioni). Illustrerò questo usando Haskell.

La tua soluzione con variabili intermedie dovrebbe essere così:

floorSqrt :: Float -> String
floorSqrt num = let
                  sqrtNum   = sqrt num
                  floorSqrt = floor sqrtNum
                  stringNum = show floorSqrt
                in
                  stringNum

L'elemento unico dovrebbe essere

floorSqrt num = show (floor (sqrt num))

o anche

floorSqrt num = (show . floor . sqrt) num

o

floorSqrt = show . floor . sqrt

vale a dire. puoi comporre le tre funzioni usando l'operatore . e applica la funzione risultante all'argomento num . Usando una funzione composita puoi omettere l'argomento num dalla definizione, come illustrato nell'ultimo esempio sopra.

In generale, se hai una sequenza di funzioni

f1, ..., fn

dove, per i ∈ {1, ..., n}: f i : a i - > b i , e per i ∈ {1, ..., n - 1}: b i = a i + 1 , puoi definire la funzione composita

compositeFunction = fn .   . f1

Se tutte le funzioni che vuoi comporre hanno lo stesso tipo a -> a , puoi applicare la funzione foldr all'elenco delle funzioni che vuoi comporre. Ad esempio, immagina di avere la funzione

floorSqrt :: Float -> Float
floorSqrt = (fromIntegral . floor) . sqrt

(nota che dobbiamo inserire fromIntegral perché floor non restituisce Float ). Questo può essere scritto come

floorSqrt = foldr (.) id [fromIntegral . floor, sqrt]

dove foldr corrisponde a reduce nella risposta di dietbuddha e . a compose . La funzione id (identità) è necessaria come punto di partenza per la piegatura / riduzione (suppongo che ciò sia implicito nell'esempio JavaScript).

Puoi usare il modello precedente per qualsiasi sequenza di funzioni f1, ..., fn dove, per i ∈ {1, ..., n}, f i : a - > una:

compositeFunction = foldr (.) id [fn, ..., f1]

In Haskell, non puoi usare questo ultimo modello per il tuo esempio originale perché foldr richiede che tutte le funzioni nell'elenco abbiano lo stesso tipo. Puoi usarlo in JavaScript perché non ha questa restrizione (vedi la risposta di dietbuddha).

    
risposta data 09.01.2014 - 12:23
fonte
4

Non vorrei nidificare troppi metodi quando si usano i metodi standard di matematica e stringa, ad esempio in Javascript. Ma ci sono librerie che hanno una cosiddetta interfaccia fluente , questo tipo di API sono fatte specialmente per rendere leggibile il codice con il metodo concatenamento. Troverai tonnellate di articoli quando utilizzi Google per tali parole chiave.

    
risposta data 02.09.2013 - 22:14
fonte
3

Lingue come quelle che descrivi esistono, le più popolari (essendo popolare un termine relativo) è FORTH

Il tuo esempio sarà simile a FORTH:

: floorSQRT  ( NUM -- NUM )
    Sqrt
    Floor
    ToString ;

(Anche se FORTH è un livello estremamente basso, quindi non ha un tipo di stringa come pensiamo ora.)

Se davvero volevi fare qualcosa di simile in JavaScript, potresti, creando le tue versioni di questi metodi che hanno preso un array come primo parametro, qualcosa del tipo:

function Sqrt(stack) { stack.push(Math.sqrt(stack.pop())) };
function Floor(stack) { stack.push(Math.floor(stack.pop())) };
function MakeString(stack) { stack.push(String(stack.pop())) };

function floorSqrt(stack) {
    Sqrt(stack);
    Math.floor(stack);
    MakeString(stack);
}

stack.push(num);
floorSqrt(stack);
num = stack.pop();

Funzionerebbe, ma sarebbe massicciamente fonte di confusione per la maggior parte dei programmatori JavaScript, quindi non farlo mai.

Avendo lavorato con FORTH nel corso della giornata, l'idea di un linguaggio basato sullo stack è molto allettante, ma in pratica, per qualsiasi pezzo di codice di dimensioni ragionevoli, diventa rapidamente confuso. Perché tutto è implicito, è difficile ricordare su che cosa effettivamente funzionerà ogni metodo. Molto più confusionario del tuo codice originale, in realtà. Onestamente, se stavo scrivendo questo codice e volevo che fosse chiaro, sarebbe simile a questo:

function floorSqrt(num) {
    num = Math.sqrt(num);
    num = Math.floor(num);

    return String(num);
}

Non trovo assolutamente confusionario. È molto chiaro

Se vuoi davvero concatenare, puoi provare a renderlo più chiaro in questo modo:

function floorSqrt(num) {
    return String(
               Math.floor(
                   Math.sqrt(num)
           ));
}

Ma ancora una volta, non penso che sia davvero necessario. Penso che l'originale sia perfetto e semplice.

    
risposta data 02.09.2013 - 23:22
fonte
2

Come dice Doc Brown, questa è un'interfaccia fluente ed è facilmente realizzabile in javascript, per usare il tuo esempio:

Number.prototype.sqrt=function(){return Math.sqrt(this);};
Number.prototype.floor=function(){return Math.floor(this);};
String.prototype.alert=function(){alert(this);return this;}

(5).sqrt().floor().toString().alert();  //alerts "2"
    
risposta data 17.04.2014 - 00:35
fonte