Una soluzione puramente funzionale a questo problema può essere pulita come l'imperativo?


10

Ho un esercizio in Python come segue:

  • un polinomio è dato come una tupla di coefficienti in modo tale che i poteri siano determinati dagli indici, ad esempio: (9,7,5) significa 9 + 7 * x + 5 * x ^ 2

  • scrivere una funzione per calcolare il suo valore per x dato

Dato che ultimamente mi occupo di programmazione funzionale, ho scritto

def evaluate1(poly, x):
  coeff = 0
  power = 1
  return reduce(lambda accu,pair : accu + pair[coeff] * x**pair[power],
                map(lambda x,y:(x,y), poly, range(len(poly))),
                0)

che ritengo illeggibile, così ho scritto

def evaluate2(poly, x):
  power = 0
  result = 1
  return reduce(lambda accu,coeff : (accu[power]+1, accu[result] + coeff * x**accu[power]),
                poly,
                (0,0)
               )[result]

che è almeno illeggibile, così ho scritto

def evaluate3(poly, x):
  return poly[0]+x*evaluate(poly[1:],x) if len(poly)>0 else 0

che potrebbe essere meno efficiente (modifica: ho sbagliato!) dal momento che utilizza molte moltiplicazioni anziché esponenziazione, in linea di principio, non mi interessa le misurazioni qui (modifica: quanto sono sciocco! Misurare avrebbe sottolineato il mio malinteso!) e non è ancora leggibile (probabilmente) come la soluzione iterativa:

def evaluate4(poly, x):
  result = 0
  for i in range(0,len(poly)):
      result += poly[i] * x**i
  return result

Esiste una soluzione puramente funzionale tanto leggibile quanto l'imperativo e vicina ad essa in termini di efficienza?

Certo, un cambio di rappresentazione sarebbe di aiuto, ma questo è stato dato dall'esercizio.

Può essere anche Haskell o Lisp, non solo Python.


7
Nella mia esperienza, il codice puramente funzionale nel senso di non usare variabili mutabili (che implica anche il non utilizzo di forloop, per esempio) è un cattivo obiettivo a cui puntare in Python. Riassegnare le variabili in modo giudizioso e non mutare gli oggetti offre quasi tutti i vantaggi e rende il codice infinitamente più leggibile. Poiché gli oggetti numerici sono immutabili e si limitano a ricollegare solo due nomi locali, la soluzione "imperativa" realizza meglio le virtù di programmazione funzionale rispetto a qualsiasi codice Python "rigorosamente puro".

2
A proposito, il metodo di moltiplicazione è il metodo di Horner ed è più efficiente dell'esponenziazione in ogni fase, poiché l'esponenziazione richiede le stesse moltiplicazioni e quindi alcune ancora.

1
Python è notoriamente brutto quando si inizia a usare lambda, rispetto alle lingue con una funzione di sintassi anonima più leggera. Parte di ciò probabilmente contribuisce all'apparenza "sporca".
KChaloux,

@KChaloux è esattamente quello che stavo per dire. Il supporto alla programmazione funzionale è in qualche modo un ripensamento in Python per molti aspetti ed è una specie di spettacolo. Anche così non penso che nemmeno la prima versione sia così orribilmente illeggibile che non riesci a capire cosa sta succedendo.
Evicatos,

Sono davvero confuso dal tuo codice, mentre l'ambito del problema ha un'equazione matematica che è estremamente chiara, perché non usi questa equazione matematica alla lettera? È abbastanza facilmente trasformato in una funzione data qualsiasi lingua ... non sono sicuro di cosa si desidera mappare, ridurre o iterare qualsiasi cosa per quando la domanda richiede una funzione che valuta una singola equazione e fornisce quell'equazione - non chiede iterazione a tutti ...
Jimmy Hoffa il

Risposte:


13

Il metodo di Horner è probabilmente più efficiente dal punto di vista computazionale come sottolinea @delnan, ma lo definirei piuttosto leggibile in Python per la soluzione di esponenziazione:

def eval_poly(poly, x):
    return sum( [a * x**i for i,a in enumerate(poly)] )

17
Elimina le parentesi quadre e dai alle variabili nomi più descrittivi, ed è ancora meglio: sum(coeff * X**power for power, coeff in enumerate(poly))
Izkata

1
Mi rattrista il fatto che le altre risposte postate siano così complesse. Usa la lingua a tuo vantaggio!
Izkata,

la comprensione è come un "contrabbando" for-loop nella programmazione funzionale
user1358

7
@ user1358 No, è lo zucchero sintattico per la composizione di mape filter. Si può anche pensarlo come un ciclo for di una forma particolare, ma i loop di quella forma sono equivalenti al summenzionato combinatore funzionale.

7

Molti linguaggi funzionali hanno implementazioni di mapi che ti permettono di avere un indice intrecciato attraverso una mappa. Combina questo con una somma e hai il seguente in F #:

let compute coefficients x = 
    coefficients 
        |> Seq.mapi (fun i c -> c * Math.Pow(x, (float)i))
        |> Seq.sum

2
E anche se non lo fanno, purché tu capisca come mapfunziona, dovrebbe essere abbastanza semplice scriverne uno tuo.
KChaloux,

4

Non capisco come il tuo codice sia correlato all'ambito del problema che hai definito, quindi fornirò la mia versione di ciò che fa il tuo codice ignorando l'ambito del problema (basato sul codice imperativo che hai scritto).

Hashell abbastanza leggibile (questo approccio può essere facilmente tradotto in qualsiasi linguaggio FP che ha un elenco destrutturante ed esce puro e leggibile):

eval acc exp val [] = acc
eval acc exp val (x:xs) = eval (acc + execPoly) (exp+1) xs
  where execPoly = x * (val^exp)

A volte l'approccio semplice e ingenuo in haskell del genere è più pulito dell'approccio più conciso alle persone meno abituate a FP.

Un approccio più chiaramente imperativo che è ancora completamente puro è:

steval val poly = runST $ do
  accAndExp <- newSTRef (0,1)
  forM_ poly $ \x -> do
    modifySTRef accAndExp (updateAccAndExp x)
  readSTRef accAndExp
  where updateAccAndExp x (acc, exp) = (acc + x*(val^exp), exp + 1)

il bonus al secondo approccio sta nella monade ST che funzionerà molto bene.

Sebbene sia certo, l'implementazione reale molto probabile da parte di un Haskeller sarebbe la zipwith menzionata in un'altra risposta sopra. zipWithè un approccio molto tipico e credo che Python possa imitare l'approccio zippato della combinazione di funzioni e un indicizzatore che può essere mappato.


4

Se hai solo una tupla (fissa), perché non farlo (in Haskell):

evalPolyTuple (c, b, a) x = c + b*x + a*x^2

Se invece hai un elenco di coefficienti, puoi usare:

evalPolyList coefs x = sum $ zipWith (\c p -> c*x^p) coefs [0..]

o con una riduzione come l'hai avuto:

evalPolyList' coefs x = foldl' (\sum (c, p) -> sum + c*x^p) 0 $ zip coefs [0..]

1
NON sono i compiti! Per non parlare del fatto che ho già fatto 3 soluzioni.
user1358

La metà delle volte in Python (anche in questo caso), "tupla" significa "elenco immutabile" ed è quindi di lunghezza arbitraria.

lunghezza ovviamente arbitraria
user1358

1
non a causa di Python, ma perché il polinomio implica una lunghezza arbitraria e la dimensione fissa non sarebbe un grande esercizio
user1358

1
@delnan È interessante. Ho sempre assunto il tuplesignificato di un set di valori di dimensioni fisse, ciascuno di tipi potenzialmente diversi, che non possono essere aggiunti o rimossi. Non ho mai veramente capito perché un linguaggio dinamico con elenchi, che accetta input eterogenei, ne avrebbe bisogno.
KChaloux,

3

Esiste una serie generale di passaggi che è possibile utilizzare per migliorare la leggibilità degli algoritmi funzionali:

  • Inserisci i nomi sui risultati intermedi, invece di cercare di stipare tutto su una riga.
  • Usa le funzioni con nome invece di lambdas, specialmente nelle lingue con sintassi lambda dettagliata. È molto più facile leggere qualcosa di simile evaluateTerma una lunga espressione lambda. Solo perché puoi usare una lambda non significa necessariamente che dovresti .
  • Se una delle tue funzioni ora nominate sembra qualcosa che potrebbe apparire abbastanza frequentemente, è probabile che sia già nella libreria standard. Guardati intorno. Il mio pitone è un po 'arrugginito, ma sembra che tu abbia praticamente reinventato enumerateo zipWith.
  • Spesso, vedere le funzioni e i risultati intermedi nominati rende più facile ragionare su ciò che sta succedendo e semplificarlo, a quel punto potrebbe avere senso rimettere un lambda o combinare alcune linee insieme.
  • Se un imperativo per il loop sembra più leggibile, è probabile che una comprensione possa funzionare bene.
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.