¿Cuáles son los equivalentes funcionales de las declaraciones de ruptura imperativas y otras verificaciones de bucle?

36

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.

Vicky
fuente
1
Tenga en cuenta que la combinación de breaky return answerse puede reemplazar por un returndentro del bucle. En FP, podría implementar este retorno temprano utilizando continuaciones, consulte, por ejemplo, en.wikipedia.org/wiki/Continuation
Giorgio
1
Las continuaciones de @Giorgio serían una enorme exageración aquí. De todos modos, es un bucle, para llamar a su próxima iteración, hace una llamada de cola, por lo que para interrumpirlo simplemente no lo llame más y solo devuelva la respuesta. Para los bucles anidados u otro flujo de control complicado, es allí donde puede usar continuaciones en lugar de agitar para reestructurar su código para usar la técnica simple anterior (que siempre debería ser posible, pero puede conducir a una estructura de código demasiado compleja que explicaría más o menos la continuación; y para más de un punto de salida, sin duda los necesitará).
Will Ness
8
En este caso: takeWhile.
Jonathan emitió el
1
@WillNess: Solo quería mencionarlo porque se puede usar para dejar un cálculo complejo en cualquier momento. Probablemente no sea la mejor solución para el ejemplo concreto del OP.
Giorgio
@Giorgio tienes razón, es el más completo, en general. en realidad esta pregunta es muy amplia, IYKWIM (es decir, se cerraría en SO en un instante).
Will Ness

Respuestas:

45

El equivalente más cercano al bucle sobre una matriz en la mayoría de los lenguajes funcionales es una foldfunció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, foldse 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:

doSomeCalc :: [Int] -> Int
doSomeCalc values = foldr1 combine values
  where combine v1 v2 | v1 == 10  = v1
                      | v1 == 150 = v1 + 100 + v2
                      | otherwise = v1 + v2

Rompiendo esto línea por línea en caso de que no esté familiarizado con la sintaxis de Haskell, esto funciona así:

doSomeCalc :: [Int] -> Int

Define el tipo de la función, acepta una lista de entradas y devuelve un único int.

doSomeCalc values = foldr1 combine values

El cuerpo principal de la función: argumento dado values, retorno foldr1llamado con argumentos combine(que definiremos a continuación) y values. foldr1es una variante de la primitiva de pliegue que comienza con el acumulador establecido en el primer valor de la lista (de ahí el 1nombre 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í el ren el nombre de la función). Entonces foldr1 f [1,2,3]es equivalente a f 1 (f 2 3)(o f(1,f(2,3))en una sintaxis de tipo C más convencional).

  where combine v1 v2 | v1 == 10  = v1

Definición de la combinefunción local: recibe dos argumentos, v1y v2. Cuando v1es 10, simplemente regresa v1. En este caso, v2 nunca se evalúa , por lo que el ciclo se detiene aquí.

                      | v1 == 150 = v1 + 100 + v2

Alternativamente, cuando v1 es 150, agrega 100 adicionales y agrega v2.

                      | otherwise = v1 + 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 foldfunción en su biblioteca estándar que incluye soporte específico para la terminación temprana. Esto a menudo se llama foldWhile, foldUntilo 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 foldno es lo que está buscando (es un mecanismo más avanzado destinado a permitir el cómputo paralelo) pero reducees el más directo equivalente. La terminación temprana ocurre si la reducedfunció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:

(reduce 
    (fn [v1 v2]
        (if (= v1 10) 
             (reduced v1)
             (+ v1 v2 (if (= v1 150) 100 0))))
    array)

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.

Jules
fuente
11
los nombres v1 v2son confusos: v1es un "valor de la matriz", pero v2es 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ón foldlWhile aquí .
Will Ness
2
que es curioso como la respuesta más equivocada consigue la mayoría de upvotes en SE .... Está bien cometer errores, que está en la compañía buena :) , también. Pero el mecanismo de descubrimiento de conocimiento en SO / SE está definitivamente roto.
Will Ness
1
El código Clojure es casi correcto, pero la condición de (= v1 150)utiliza el valor antes v2(también conocido como e).
NikoNyrh
1
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í.
Capitán Man
15
@WillNess Se votó porque es la traducción y explicación más comprensible de inmediato. El hecho de que esté mal es una pena, pero aquí es relativamente poco importante porque los pequeños errores no niegan el hecho de que la respuesta sea útil. Pero, por supuesto, debe corregirse.
Konrad Rudolph el
33

Podrías convertirlo fácilmente a recursión. Y tiene una buena llamada recursiva con cola optimizada.

Pseudocódigo:

public int doSomeCalc(int[] array)
{
    return doSomeCalcInner(array, 0);
}

public int doSomeCalcInner(int[] array, int answer)
{
    if (array is empty) return answer;

    // not sure how to efficiently implement head/tails array split in clojure
    var head = array[0] // first element of array
    var tail = array[1..] // remainder of array

    answer += head;
    if (answer == 10) return answer;
    if (answer == 150) answer += 100;

    return doSomeCalcInner(tail, answer);
}
Eufórico
fuente
14
Sí. El equivalente funcional a un bucle es la recursividad de cola, y el equivalente funcional a un condicional sigue siendo un condicional.
Jörg W Mittag
44
@ JörgWMittag Prefiero decir que la recursión de cola es el equivalente funcional de GOTO. (No es tan malo, pero sigue siendo bastante incómodo). El equivalente a un bucle, como dice Jules, es un pliegue adecuado.
Leftaroundabout
66
@leftaroundabout No estoy de acuerdo en realidad. Yo diría que la recursión de la cola está más restringida que un goto, dada la necesidad de saltar hacia sí misma y solo en posición de cola. Es fundamentalmente equivalente a una construcción en bucle. Yo diría que la recursión en general es equivalente a GOTO. En cualquier caso, cuando compila la recursión de la cola, la mayoría de las veces se reduce a un while (true)bucle con el cuerpo de la función en el que el retorno temprano es solo una breakdeclaració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 uno
J_mie6
1
@ J_mie6, la razón por la que considero más la recursividad de la cola GOTOes 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). ...
leftaroundabout
1
... En cuanto a los pliegues: tienes razón, un pliegue tradicional (catamorfismo) es un tipo muy específico de bucle, pero estos esquemas de recursión pueden generalizarse (ana- / apo- / hylomorphisms); colectivamente, estos son IMO, el reemplazo adecuado para los bucles imperativos.
Leftaroundabout
13

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:

baseSums = scanl (+) 0

offsets = scanl (\offset sum -> if sum == 150 then offset + 100 else offset) 0

zipWithOffsets xs = zipWith (+) xs (offsets xs)

stopAt10 xs = if 10 `elem` xs then 10 else last xs

result = stopAt10 . zipWithOffsets . baseSums

result [1..]         -- 10
result [11..1000000] -- 500000499945

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.

Karl Bielefeldt
fuente
La lógica está dispersa por todas partes. Este código no será fácil de mantener. NostopAt10 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 (+) 0spanlast
Will Ness
6

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:

(defn doSomeCalc 
  ([lst] (doSomeCalc lst 0))
  ([lst sum]
    (if (empty? lst) sum
        (if (= sum 10) sum
            (let [sum (+ sum (first lst))]
                 [sum (if (= sum 150) (+ sum 100) sum)]
               (recur (rest lst) sum))))))) 

Editar: Jules señala que reduceen Clojure son compatibles con la salida anticipada. Usar esto es más elegante:

(defn doSomeCalc [lst]  
  (reduce (fn [sum val]
    (if (= sum 10) (reduced sum)
        (let [sum (+ sum val)]
             [sum (if (= sum 150) (+ sum 100) sum)]
           sum))
   lst)))

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.

JacquesB
fuente
vea la edición que acabo de agregar a mi respuesta: la reduceoperación de Clojure admite la salida anticipada.
Jules
@Jules: Genial, esa es probablemente una solución más idiomática.
JacquesB
¿Incorrecto o takeWhileno es una "operación común"?
Jonathan emitió el
@jcast: si bien takeWhilees 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 usar scany takeWhileen los resultados del escaneo (consulte la respuesta de Karl Bielefeldt, que si bien no se usa takeWhilepodrí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.
Jules
@Jules take-whileen Clojure produce una secuencia perezosa (según los documentos). Otra forma de abordar esto sería con transductores (quizás el mejor).
Will Ness
4

Como señalan otras respuestas, Clojure tiene reducedpara detener las reducciones temprano:

(defn some-calc [coll]
  (reduce (fn [answer e]
            (let [answer (+ answer e)]
               (case answer
                 10  (reduced answer)
                 150 (+ answer 100)
                 answer)))
          0 coll))

Esta es la mejor solución para su situación particular. También puede obtener una gran cantidad de kilometraje de la combinación reducedcon transduce, lo que le permite utilizar transductores de map,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/ecasí:

(define (some-calc ls)
  (let/ec break ; let break be an escape continuation
    (foldl (lambda (answer e)
             (let ([answer (+ answer e)])
               (case answer
                 [(10)  (break answer)] ; return answer immediately
                 [(150) (+ answer 100)]
                 [else  answer])))
           0 ls)))

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 continueo salir del ciclo con break(o return). Cuando use llamadas de cola (o la loopconstrucció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:

(defn some-calc [coll]
  (loop [answer 0, [e es :as coll] coll]
    (if (empty? coll)
      answer
      (let [answer (+ answer e)]
        (case answer
          10 answer
          150 (recur (+ answer 100) es)
          (recur answer es))))))
nilern
fuente
1
Al volver a usar mónadas de error en Haskell, no creo que haya ninguna penalización de rendimiento real aquí. Tienden a pensar en la línea de manejo de excepciones, pero no funcionan de la misma manera y no se requiere caminar de pila, por lo que realmente no debería ser un problema si se usa de esta manera. Además, incluso si hay una razón cultural para no usar algo como esto MonadError, el equivalente básicamente Eitherno tiene ese sesgo hacia solo el manejo de errores, por lo que puede usarse fácilmente como un sustituto.
Jules
@Jules Creo que regresar a la izquierda no impide que el pliegue visite la lista completa (u otra secuencia). Sin embargo, no estoy familiarizado con los componentes internos de la biblioteca estándar de Haskell.
nilern
2

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:

loop : v -> (v -> v) -> (v -> Bool) -> v
loop init iter cond_to_cont = 
    if cond_to_cont init 
        then loop (iter init) iter cond
        else init

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

module Array (foldlc) where

foldlc : v -> (v -> e -> v) -> (v -> Bool) -> Array e -> v
foldlc init iter cond_to_cont arr = 
    loop 
        (init, 0)
        (λ (val, next_pos) -> (iter val (at next_pos arr), next_pos + 1))
        (λ (val, next_pos) -> and (cond_to_cont val) (next_pos < size arr))

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

iter : (Int, Bool) -> Int -> (Int, Bool)
iter (answer, cont) collection_element = 
  let new_answer = answer + collection_element
  in case new_answer of
    10 -> (new_answer, false)
    150 -> (new_answer + 100, true)
    _ -> (new_answer, true)

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:

doSomeCalc :: Array Int -> Int
doSomeCalc arr = fst (Array.foldlc (0, true) iter snd arr)

"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

fst : (x, y) -> x
snd : (x, y) -> y

En este caso, el estilo "sin puntos" aumenta la legibilidad de la implementación de doSomeCalc:

doSomeCalc = Array.foldlc (0, true) iter snd >>> fst

(>>>) 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.

libeako
fuente
0

Primero, reescriba el bucle ligeramente, de modo que cada iteración del bucle salga temprano o mute answerexactamente una vez:

    public int doSomeCalc(int[] array)
    {
        int answer = 0;
        if(array!=null)
        {
            for(int e: array)
            {
                if(answer + e == 10) return answer + e;
                else if(answer + e == 150) answer = answer + e + 100;
                else answer = answer + e;
            }
        }
        return answer;
    }

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:

doSomeCalc :: [Int] -> Int
doSomeCalc = recurse 0
  where recurse :: Int -> [Int] -> Int
        recurse answer [] = answer
        recurse answer (e:array)
          | answer + e == 10 = answer + e
          | answer + e == 150 = recurse (answer + e + 100) array
          | otherwise = recurse (answer + e) array

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:

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer + e == 10 = Left (answer + e)
          | answer + e == 150 = Right (answer + e + 100)
          | otherwise = Right (answer + e)

En este contexto, Leftsale temprano con su valor y Rightcontinúa la recursión con su valor.


Esto ahora podría simplificarse un poco más, así:

import Control.Monad (foldM)

doSomeCalc :: [Int] -> Int
doSomeCalc = either id id . foldM go 0
  where go :: Int -> Int -> Either Int Int
        go answer e
          | answer' == 10 = Left 10
          | answer' == 150 = Right 250
          | otherwise = Right answer'
          where answer' = answer + e

Esto es mejor como el código final de Haskell, pero ahora está un poco menos claro cómo se asigna al Java original.

Joseph Sible-Reinstate a Monica
fuente