Digamos que tengo la siguiente lógica. ¿Cómo escribir eso en la programación funcional?
public int doSomeCalc(int[] array)
{
int answer = 0;
if(array!=null)
{
for(int e: array)
{
answer += e;
if(answer == 10) break;
if(answer == 150) answer += 100;
}
}
return answer;
}
Los ejemplos en la mayoría de los blogs, artículos ... Veo solo explican el caso simple de una función matemática directa que dice 'Sum'. Pero, tengo una lógica similar a la anterior escrita en Java y me gustaría migrarla al código funcional en Clojure. Si no podemos hacer lo anterior en FP, entonces el tipo de promociones para FP no indica esto explícitamente.
Sé que el código anterior es totalmente imprescindible. No fue escrito con la previsión de migrarlo a FP en el futuro.
break
yreturn answer
se puede reemplazar por unreturn
dentro del bucle. En FP, podría implementar este retorno temprano utilizando continuaciones, consulte, por ejemplo, en.wikipedia.org/wiki/ContinuationtakeWhile
.Respuestas:
El equivalente más cercano al bucle sobre una matriz en la mayoría de los lenguajes funcionales es una
fold
función, es decir, una función que llama a una función especificada por el usuario para cada valor de la matriz, pasando un valor acumulado a lo largo de la cadena. En muchos lenguajes funcionales,fold
se complementa con una variedad de funciones adicionales que proporcionan funciones adicionales, incluida la opción de detenerse antes de tiempo cuando surja alguna condición. En lenguajes perezosos (por ejemplo, Haskell), puede detenerse temprano simplemente no evaluando más a lo largo de la lista, lo que hará que nunca se generen valores adicionales. Por lo tanto, traduciendo su ejemplo a Haskell, lo escribiría como:Rompiendo esto línea por línea en caso de que no esté familiarizado con la sintaxis de Haskell, esto funciona así:
Define el tipo de la función, acepta una lista de entradas y devuelve un único int.
El cuerpo principal de la función: argumento dado
values
, retornofoldr1
llamado con argumentoscombine
(que definiremos a continuación) yvalues
.foldr1
es una variante de la primitiva de pliegue que comienza con el acumulador establecido en el primer valor de la lista (de ahí el1
nombre de la función), luego lo combina usando la función especificada por el usuario de izquierda a derecha (que generalmente se denomina pliegue derecho , de ahí elr
en el nombre de la función). Entoncesfoldr1 f [1,2,3]
es equivalente af 1 (f 2 3)
(of(1,f(2,3))
en una sintaxis de tipo C más convencional).Definición de la
combine
función local: recibe dos argumentos,v1
yv2
. Cuandov1
es 10, simplemente regresav1
. En este caso, v2 nunca se evalúa , por lo que el ciclo se detiene aquí.Alternativamente, cuando v1 es 150, agrega 100 adicionales y agrega v2.
Y, si ninguna de esas condiciones es verdadera, solo agrega v1 a v2.
Ahora, esta solución es algo específica para Haskell, porque el hecho de que un pliegue derecho termina si la función de combinación no evalúa su segundo argumento es causado por la estrategia de evaluación perezosa de Haskell. No conozco Clojure, pero creo que utiliza una evaluación estricta, por lo que esperaría que tuviera una
fold
función en su biblioteca estándar que incluye soporte específico para la terminación temprana. Esto a menudo se llamafoldWhile
,foldUntil
o similar.Un vistazo rápido a la documentación de la biblioteca Clojure sugiere que es un poco diferente de la mayoría de los lenguajes funcionales en nomenclatura, y eso
fold
no es lo que está buscando (es un mecanismo más avanzado destinado a permitir el cómputo paralelo) peroreduce
es el más directo equivalente. La terminación temprana ocurre si lareduced
función se llama dentro de su función de combinación. No estoy 100% seguro de entender la sintaxis, pero sospecho que lo que estás buscando es algo como esto:NB: ambas traducciones, Haskell y Clojure, no son del todo adecuadas para este código específico; pero transmiten la esencia general de esto: vea la discusión en los comentarios a continuación para ver problemas específicos con estos ejemplos.
fuente
v1 v2
son confusos:v1
es un "valor de la matriz", perov2
es el resultado acumulado. y su traducción es errónea, creo, las salidas de bucle de la OP cuando el acumulado (desde la izquierda) valor golpea 10, no un elemento de la matriz. Lo mismo con el incremento en 100. Si usa pliegues aquí, use el pliegue izquierdo con salida temprana, alguna variaciónfoldlWhile
aquí .(= v1 150)
utiliza el valor antesv2
(también conocido comoe
).Breaking this down line by line in case you're not familiar with Haskell's syntax
-- Eres mi héroe. Haskell es un misterio para mí.Podrías convertirlo fácilmente a recursión. Y tiene una buena llamada recursiva con cola optimizada.
Pseudocódigo:
fuente
GOTO
. (No es tan malo, pero sigue siendo bastante incómodo). El equivalente a un bucle, como dice Jules, es un pliegue adecuado.GOTO
. En cualquier caso, cuando compila la recursión de la cola, la mayoría de las veces se reduce a unwhile (true)
bucle con el cuerpo de la función en el que el retorno temprano es solo unabreak
declaración. Un pliegue, si bien tiene razón acerca de que es un bucle, en realidad está más restringido que una construcción de bucle general; es más como un ciclo para cada unoGOTO
es que es necesario hacer una contabilidad incómoda de los argumentos en qué estado se pasan a la llamada recursiva, para garantizar que realmente se comporte como se esperaba. Eso no es necesario en el mismo grado en bucles imperativos escritos decentemente (donde está bastante claro cuáles son las variables con estado y cómo cambian en cada iteración), ni en la recursión ingenua (donde generalmente no se hace mucho con los argumentos, y en cambio El resultado se ensambla de una manera bastante intuitiva). ...de verdad me gusta la respuesta de Jules , pero también quería señalar algo que la gente a menudo extraña sobre la programación funcional perezosa, que es que no todo tiene que estar "dentro del ciclo". Por ejemplo:
Puede ver que cada parte de su lógica puede calcularse en una función separada y luego componerse juntas. Esto permite funciones más pequeñas que generalmente son mucho más fáciles de solucionar. Para su ejemplo de juguete, tal vez esto agregue más complejidad de lo que elimina, pero en el código del mundo real, las funciones divididas a menudo son mucho más simples que el todo.
fuente
stopAt10
es un buen consumidor. su respuesta es mejor que la que cita en que aísla correctamente el productor básico de valores. Sin embargo, su consumo debe incorporar la lógica de control directamente, se implementa mejor con solo dos sy a , explícitamente. eso seguiría de cerca la estructura y lógica del código original, también, y sería fácil de mantener.scanl (+) 0
span
last
La mayoría de los ejemplos de procesamiento de listas que verá utilizan funciones como
map
,filter
,sum
etc., que operan en la lista en su conjunto. Pero en su caso, tiene una salida anticipada condicional, un patrón bastante poco común que no es compatible con las operaciones de lista habituales. Por lo tanto, debe desplegar un nivel de abstracción y usar la recursividad, que también está más cerca de cómo se ve el ejemplo imperativo.Esta es una traducción bastante directa (probablemente no idiomática) a Clojure:
Editar: Jules señala que
reduce
en Clojure son compatibles con la salida anticipada. Usar esto es más elegante:En cualquier caso, puede hacer cualquier cosa en lenguajes funcionales como puede hacerlo en lenguajes imperativos, pero a menudo tiene que cambiar un poco su mentalidad para encontrar una solución elegante. En la codificación imperativa, piensa en procesar una lista paso a paso, mientras que en los lenguajes funcionales busca una operación para aplicar a todos los elementos de la lista.
fuente
reduce
operación de Clojure admite la salida anticipada.takeWhile
no es una "operación común"?takeWhile
es una operación común, no es especialmente útil en este caso, porque necesita los resultados de su transformación antes de poder decidir si se detendrá. En un lenguaje perezoso, esto no importa: puede usarscan
ytakeWhile
en los resultados del escaneo (consulte la respuesta de Karl Bielefeldt, que si bien no se usatakeWhile
podría reescribirse fácilmente para hacerlo), pero para un lenguaje estricto como clojure esto sería significa procesar toda la lista y luego descartar los resultados. Sin embargo, las funciones del generador podrían resolver esto, y creo que clojure los admite.take-while
en Clojure produce una secuencia perezosa (según los documentos). Otra forma de abordar esto sería con transductores (quizás el mejor).Como señalan otras respuestas, Clojure tiene
reduced
para detener las reducciones temprano:Esta es la mejor solución para su situación particular. También puede obtener una gran cantidad de kilometraje de la combinación
reduced
contransduce
, lo que le permite utilizar transductores demap
,filter
etc. Sin embargo, está lejos de ser una respuesta completa a la pregunta general.Las continuaciones de escape son una versión generalizada de las declaraciones break y return. Se implementan directamente en algunos esquemas (
call-with-escape-continuation
), Common Lisp (block
+return
,catch
+throw
) e incluso C (setjmp
+longjmp
). Las continuaciones más generales delimitadas o no delimitadas que se encuentran en el Esquema estándar o como mónadas de continuación en Haskell y Scala también se pueden usar como continuaciones de escape.Por ejemplo, en Racket podrías usar
let/ec
así:Muchos otros lenguajes también tienen construcciones similares a la continuación de escape en forma de manejo de excepciones. En Haskell también puedes usar una de las diversas mónadas de error
foldM
. Debido a que son principalmente construcciones de manejo de errores que usan excepciones o mónadas de error para retornos tempranos, generalmente es culturalmente inaceptable y posiblemente bastante lento.También puede desplegar desde funciones de orden superior a llamadas de cola.
Cuando usa bucles, ingresa la siguiente iteración automáticamente cuando llega al final del cuerpo del bucle. Puede ingresar la siguiente iteración temprano con
continue
o salir del ciclo conbreak
(oreturn
). Cuando use llamadas de cola (o laloop
construcción de Clojure que imita la recursión de cola), siempre debe hacer una llamada explícita para ingresar a la siguiente iteración. Para detener el bucle, simplemente no realice la llamada recursiva, sino que proporcione el valor directamente:fuente
MonadError
, el equivalente básicamenteEither
no tiene ese sesgo hacia solo el manejo de errores, por lo que puede usarse fácilmente como un sustituto.La parte intrincada es el bucle. Comencemos con eso. Un bucle generalmente se convierte en estilo funcional al expresar la iteración con una sola función. Una iteración es una transformación de la variable de bucle.
Aquí hay una implementación funcional de un bucle general:
Toma (un valor inicial de la variable de bucle, la función que expresa una única iteración [en la variable de bucle]) (una condición para continuar el bucle).
Su ejemplo usa un bucle en una matriz, que también se rompe. Esta capacidad en su idioma imperativo está integrada en el lenguaje mismo. En la programación funcional, dicha capacidad generalmente se implementa a nivel de biblioteca. Aquí hay una posible implementación
En eso :
Utilizo un par ((val, next_pos)) que contiene la variable de bucle visible en el exterior y la posición en la matriz, que oculta esta función.
La función de iteración es un poco más compleja que en el bucle general, esta versión permite utilizar el elemento actual de la matriz. [Está en forma de curry .]
Dichas funciones generalmente se denominan "pliegue".
Pongo una "l" en el nombre para indicar que la acumulación de los elementos de la matriz se realiza de forma asociativa a la izquierda; para imitar el hábito de los lenguajes de programación imperativos para iterar una matriz de índice bajo a alto.
Puse una "c" en el nombre para indicar que esta versión de fold toma una condición que controla si y cuando el ciclo se detiene antes.
Por supuesto, es probable que tales funciones de utilidad estén fácilmente disponibles en la biblioteca base incluida con el lenguaje de programación funcional utilizado. Los escribí aquí para demostración.
Ahora que tenemos todas las herramientas que están en el idioma en el caso imperativo, podemos recurrir para implementar la funcionalidad específica de su ejemplo.
La variable en su ciclo es un par ('respuesta', un booleano que codifica si continuar).
Tenga en cuenta que utilicé una nueva "variable" 'new_answer'. Esto se debe a que en la programación funcional no puedo cambiar el valor de una "variable" ya inicializada. No me preocupa el rendimiento, el compilador puede volver a utilizar la memoria de 'respuesta' para 'nueva_respuesta' a través del análisis de por vida, si cree que es más eficiente.
Incorporando esto en nuestra función de bucle desarrollada anteriormente:
"Array" aquí es el nombre del módulo que exporta la función foldlc.
"puño", "segundo" representan funciones que devuelven el primer y segundo componente de su par de parámetros
En este caso, el estilo "sin puntos" aumenta la legibilidad de la implementación de doSomeCalc:
(>>>) es la composición de la función:
(>>>) : (a -> b) -> (b -> c) -> (a -> c)
Es lo mismo que arriba, solo el parámetro "arr" se deja fuera de ambos lados de la ecuación de definición.
Una última cosa: verificar el caso (array == null). En lenguajes de programación mejor diseñados, pero incluso en lenguajes mal diseñados con cierta disciplina básica, se utiliza un tipo opcional para expresar la no existencia. Esto no tiene mucho que ver con la programación funcional, de la cual se trata la pregunta en última instancia, por lo tanto, no trato con eso.
fuente
Primero, reescriba el bucle ligeramente, de modo que cada iteración del bucle salga temprano o mute
answer
exactamente una vez:Debe quedar claro que el comportamiento de esta versión es exactamente el mismo que antes, pero ahora, es mucho más sencillo convertirlo al estilo recursivo. Aquí hay una traducción directa de Haskell:
Ahora es puramente funcional, pero podemos mejorarlo desde el punto de vista de la eficiencia y la legibilidad mediante el uso de un pliegue en lugar de una recursividad explícita:
En este contexto,
Left
sale temprano con su valor yRight
continúa la recursión con su valor.Esto ahora podría simplificarse un poco más, así:
Esto es mejor como el código final de Haskell, pero ahora está un poco menos claro cómo se asigna al Java original.
fuente