¿Puede una solución funcional pura a este problema ser tan limpia como el imperativo?

10

Tengo un ejercicio en Python de la siguiente manera:

  • un polinomio se da como una tupla de coeficientes de modo que las potencias están determinadas por los índices, por ejemplo: (9,7,5) significa 9 + 7 * x + 5 * x ^ 2

  • escribir una función para calcular su valor para x dado

Como estoy en la programación funcional últimamente, escribí

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)

que considero ilegible, así que escribí

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]

que es al menos tan ilegible, así que escribí

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

que podría ser menos eficiente (editar: ¡me equivoqué!) ya que usa muchas multiplicaciones en lugar de exponenciación, en principio, no me importan las mediciones aquí (editar: ¡Qué tonto de mi parte! ¡La medición habría señalado mi error!) y todavía no es tan legible (posiblemente) como la solución iterativa:

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

¿Existe una solución puramente funcional tan legible como la imperativa y cercana a la eficiencia?

Es cierto que un cambio de representación ayudaría, pero esto fue dado por el ejercicio.

También puede ser Haskell o Lisp, no solo Python.

usuario1358
fuente
77
En mi experiencia, el código puramente funcional en el sentido de no usar variables mutables (lo que también implica no usar forbucles, por ejemplo) es un mal objetivo al que apuntar en Python. Volver a vincular variables juiciosamente y no mutar objetos le brinda casi todos los beneficios y hace que el código sea infinitamente más legible. Como los objetos numéricos son inmutables y solo vuelven a unir dos nombres locales, su solución "imperativa" se da cuenta mejor de las virtudes de programación funcional que cualquier código Python "estrictamente puro".
2
Por cierto, el método de multiplicación es el método de Horner y es más eficiente que la exponenciación en cada paso, ya que la exponenciación requiere las mismas multiplicaciones y algo más.
1
Python es notoriamente feo cuando se usa lambda, en comparación con los idiomas con una función de sintaxis anónima más ligera. Parte de eso probablemente contribuye a la apariencia "inmunda".
KChaloux
@KChaloux, eso es exactamente lo que iba a decir. El soporte de programación funcional es algo de último momento en Python en muchos aspectos y se nota. Aun así, no creo que incluso la primera versión sea tan terriblemente ilegible que no puedas entender lo que está sucediendo.
Evicatos
Estoy realmente confundido con su código, mientras que el alcance del problema tiene una ecuación matemática que es extremadamente clara, ¿por qué no usa esa ecuación matemática literalmente? Se convierte con bastante facilidad en una función dado cualquier lenguaje ... no estoy seguro de lo que desea asignar o reducir o iterar cuando la pregunta solicita una función que evalúa una sola ecuación y proporciona esa ecuación: no pide iteración en absoluto ...
Jimmy Hoffa

Respuestas:

13

El método de Horner es probablemente más eficiente desde el punto de vista computacional como señala @delnan, pero yo llamaría a esto bastante legible en Python para la solución de exponenciación:

def eval_poly(poly, x):
    return sum( [a * x**i for i,a in enumerate(poly)] )
aelfric5578
fuente
17
Suelte los corchetes y dé a las variables nombres más descriptivos, y es aún mejor: sum(coeff * X**power for power, coeff in enumerate(poly))
Izkata
1
Me entristece que las otras respuestas publicadas sean tan complejas. ¡Usa el lenguaje a tu favor!
Izkata
la comprensión es como un "contrabando" for-loop en la programación funcional
user1358
77
@ user1358 No, es azúcar sintáctico para la composición de mapy filter. También se puede pensar en él como un bucle for de una forma particular, pero los bucles de esa forma son equivalentes al combinador funcitonal mencionado anteriormente.
7

Muchos lenguajes funcionales tienen implementaciones de mapas que le permiten tener un índice entrelazado a través de un mapa. Combina eso con una suma y tienes lo siguiente en F #:

let compute coefficients x = 
    coefficients 
        |> Seq.mapi (fun i c -> c * Math.Pow(x, (float)i))
        |> Seq.sum
Steven Evers
fuente
2
E incluso si no lo hacen, siempre y cuando comprenda cómo mapfunciona, debería ser bastante simple escribir uno propio.
KChaloux
4

No entiendo cómo se relaciona su código con el alcance del problema que definió, así que le daré mi versión de lo que hace su código ignorando el alcance del problema (basado en el código imperativo que escribió).

Haskell bastante legible (este enfoque se puede traducir fácilmente a cualquier lenguaje FP que tenga una lista de desestructuración y salga puro y legible):

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

A veces, el enfoque simple e ingenuo en Haskell es más limpio que el enfoque más conciso para las personas menos acostumbradas a la PF.

Un enfoque más claramente imperativo que todavía es completamente puro es:

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)

La ventaja del segundo enfoque es que estar en la Mónada ST funcionará muy bien.

Aunque para estar seguros, la implementación real más probable de un Haskeller sería el código postal mencionado en otra respuesta anterior. zipWithEs un enfoque muy típico y creo que Python puede imitar el enfoque de compresión de combinar funciones y un indexador que se puede mapear.

Jimmy Hoffa
fuente
4

Si solo tiene una tupla (fija), ¿por qué no hacer esto (en Haskell)?

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

Si, en cambio, tiene una lista de coeficientes, puede usar:

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

o con una reducción como la tenías:

evalPolyList' coefs x = foldl' (\sum (c, p) -> sum + c*x^p) 0 $ zip coefs [0..]
Paul
fuente
1
¡NO es tarea! Sin mencionar que ya hice 3 soluciones.
user1358
La mitad del tiempo en Python (incluido en este caso), "tupla" significa "lista inmutable" y, por lo tanto, tiene una longitud arbitraria.
duración obviamente arbitraria
user1358
1
no por python, sino porque el polinomio implica una longitud arbitraria, y el tamaño fijo no sería un gran ejercicio
user1358
1
@delnan Eso es interesante. Siempre he considerado tupleun conjunto de valores de tamaño fijo, cada uno de tipos potencialmente diferentes, que no se pueden agregar ni quitar. Nunca entendí realmente por qué un lenguaje dinámico con listas, que acepta entradas heterogéneas, las necesitaría.
KChaloux
3

Hay un conjunto general de pasos que puede usar para mejorar la legibilidad de los algoritmos funcionales:

  • Ponga nombres en sus resultados intermedios, en lugar de tratar de agrupar todo en una línea.
  • Utilice funciones con nombre en lugar de lambdas, especialmente en idiomas con sintaxis detallada de lambda. Es mucho más fácil leer algo como evaluateTermuna larga expresión lambda. El hecho de que pueda usar una lambda no significa necesariamente que deba hacerlo .
  • Si una de sus funciones ahora nombradas parece algo que aparecería con bastante frecuencia, es probable que ya esté en la biblioteca estándar. Mira alrededor. Mi pitón está un poco oxidado, pero parece que básicamente reinventaste enumerateo zipWith.
  • A menudo, ver las funciones y los resultados intermedios nombrados hace que sea más fácil razonar sobre lo que está sucediendo y simplificarlo, en cuyo punto podría tener sentido volver a colocar una lambda o combinar algunas líneas.
  • Si un imperativo para el bucle parece más legible, es probable que una comprensión funcione bien.
Karl Bielefeldt
fuente