Quali sono gli equivalenti funzionali delle dichiarazioni di interruzione imperativa e di altri controlli del ciclo?


36

Diciamo, ho la logica qui sotto. Come scriverlo in Programmazione funzionale?

    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;
    }

Gli esempi nella maggior parte dei blog, articoli ... Vedo solo che spiega il semplice caso di una funzione matematica diretta dire 'Somma'. Ma ho una logica simile a quella sopra scritta in Java e vorrei migrare a codice funzionale in Clojure. Se non possiamo fare quanto sopra in FP, allora il tipo di promozioni per FP non lo afferma esplicitamente.

So che il codice sopra è assolutamente imperativo. Non è stato scritto con la premessa di migrarlo in FP in futuro.


1
Si noti che la combinazione di breake return answerpuò essere sostituita da un returnciclo interno. In FP potresti implementare questo ritorno anticipato usando le continuazioni, vedi ad esempio en.wikipedia.org/wiki/Continuation
Giorgio

1
Le continuazioni di @Giorgio sarebbero un enorme sovraccarico qui. È comunque un ciclo, per chiamare la prossima iterazione fai una coda, quindi per interromperti non chiamarlo più e restituire semplicemente la risposta. Per nidificate loop, o altro flusso di controllo complicato, è lì che si potrebbe utilizzare continuazioni invece di sollevamento per ristrutturare il codice per utilizzare quanto sopra tecnica semplice (che dovrebbe essere sempre possibile, ma può portare a struttura del codice eccessivamente complessa che sarebbe più o meno esplicato la continuazione; e per più di un punto di uscita ne avresti sicuramente bisogno).
Will Ness,

8
In questo caso: takeWhile.
Jonathan Cast

1
@WillNess: volevo solo menzionarlo perché può essere utilizzato per lasciare un calcolo complesso in qualsiasi momento. Probabilmente non è la soluzione migliore per l'esempio concreto dell'OP.
Giorgio

@Giorgio hai ragione, è il più completo, in generale. in realtà questa domanda è molto ampia, IYKWIM (cioè sarebbe chiusa su SO in un batter d'occhio).
Will Ness,

Risposte:


45

L'equivalente più vicino al loop su un array nella maggior parte dei linguaggi funzionali è una foldfunzione, ovvero una funzione che chiama una funzione specificata dall'utente per ciascun valore dell'array, passando un valore accumulato lungo la catena. In molti linguaggi funzionali, foldè ampliato da una varietà di funzioni aggiuntive che forniscono funzionalità extra, inclusa l'opzione di interruzione anticipata quando si presentano alcune condizioni. Nei linguaggi pigri (ad es. Haskell), fermarsi presto può essere raggiunto semplicemente non valutando ulteriormente l'elenco, il che farà sì che non vengano mai generati valori aggiuntivi. Pertanto, traducendo il tuo esempio in Haskell, lo scriverei come:

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

Rompendo questa riga per riga nel caso in cui non si abbia familiarità con la sintassi di Haskell, questo funziona in questo modo:

doSomeCalc :: [Int] -> Int

Definisce il tipo di funzione, accettando un elenco di ints e restituendo un singolo int.

doSomeCalc values = foldr1 combine values

Il corpo principale della funzione: dato argomento values, return foldr1chiamato con argomenti combine(che definiremo di seguito) e values. foldr1è una variante della primitiva pieghevole che inizia con l'accumulatore impostato sul primo valore dell'elenco (quindi 1nel nome della funzione), quindi lo combina utilizzando la funzione specificata dall'utente da sinistra a destra (che di solito viene chiamata piega destra , da qui il rnome della funzione). Quindi foldr1 f [1,2,3]è equivalente a f 1 (f 2 3)(o f(1,f(2,3))nella sintassi C-like più convenzionale).

  where combine v1 v2 | v1 == 10  = v1

Definizione della combinefunzione locale: riceve due argomenti v1e v2. Quando v1è 10, ritorna e basta v1. In questo caso, v2 non viene mai valutato , quindi il ciclo si ferma qui.

                      | v1 == 150 = v1 + 100 + v2

In alternativa, quando v1 è 150, aggiunge un ulteriore 100 ad esso e aggiunge v2.

                      | otherwise = v1 + v2

E, se nessuna di queste condizioni è vera, aggiunge semplicemente v1 a v2.

Ora, questa soluzione è in qualche modo specifica per Haskell, perché il fatto che una piega a destra termina se la funzione di combinazione non valuta il suo secondo argomento è causato dalla strategia di valutazione pigra di Haskell. Non conosco Clojure, ma credo che utilizzi una valutazione rigorosa, quindi mi aspetto che abbia una foldfunzione nella sua libreria standard che includa un supporto specifico per la risoluzione anticipata. Questo è spesso chiamato foldWhile, foldUntilo simili.

Una rapida occhiata alla documentazione della biblioteca Clojure suggerisce che è un po 'diverso dalla maggior parte dei linguaggi funzionali nella denominazione e che foldnon è quello che stai cercando (è un meccanismo più avanzato volto a consentire il calcolo parallelo) ma reduceè il più diretto equivalente. La risoluzione anticipata si verifica se la reducedfunzione viene chiamata all'interno della funzione di combinazione. Non sono sicuro al 100% di capire la sintassi, ma sospetto che quello che stai cercando sia qualcosa del genere:

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

NB: entrambe le traduzioni, Haskell e Clojure, non sono del tutto giuste per questo codice specifico; ma ne trasmettono il senso generale - vedi la discussione nei commenti qui sotto per problemi specifici con questi esempi.


11
i nomi v1 v2sono confusi: v1è un "valore dall'array", ma v2è il risultato accumulato. e la traduzione è sbagliata, io credo, di loop uscite del PO quando il accumulato (da sinistra) il valore colpisce 10, non un elemento della matrice. Lo stesso vale per l'incremento di 100. Se utilizzare le pieghe qui, utilizzare la piega a sinistra con uscita anticipata, alcune variazioni foldlWhile qui .
Will Ness,

2
esso è divertente come la risposta più sbagliata ottiene il maggior numero di upvotes sulla SE .... va bene anche fare errori, sei in compagnia buona :) , anche. Ma il meccanismo di scoperta della conoscenza su SO / SE è definitivamente rotto.
Will Ness,

1
Il codice Clojure è quasi corretto, ma la condizione di (= v1 150)usa il valore precedente v2(aka. e) Viene sommato ad esso.
NikoNyrh,

1
Breaking this down line by line in case you're not familiar with Haskell's syntax-- Tu sei il mio eroe. Haskell è un mistero per me.
Captain Man

15
@WillNess È stato votato perché è la traduzione e la spiegazione più immediatamente comprensibili. Il fatto che sia sbagliato è un peccato, ma relativamente poco importante qui perché i lievi errori non negano il fatto che la risposta sia altrimenti utile. Ma ovviamente dovrebbe essere corretto.
Konrad Rudolph,

33

Potresti facilmente convertirlo in ricorsione. E ha una bella chiamata ricorsiva ottimizzata per la coda.

Pseudocodice:

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);
}

14
Sì. L'equivalente funzionale di un loop è la ricorsione della coda e l'equivalente funzionale di un condizionale è ancora un condizionale.
Jörg W Mittag

4
@ JörgWMittag Preferirei dire che la ricorsione della coda è l'equivalente funzionale di GOTO. (Non così male, ma comunque piuttosto imbarazzante.) L'equivalente di un anello, come dice Jules, è una piega adatta.
lasciato il

6
@leftaroundabout In realtà non sono d'accordo. Direi che la ricorsione della coda è più limitata di un goto, data la necessità di saltare a se stesso e solo in posizione di coda. È fondamentalmente equivalente a un costrutto loop. Direi che la ricorsione in generale è equivalente a GOTO. In ogni caso, quando compili la ricorsione della coda, la maggior parte si riduce a un while (true)ciclo con il corpo della funzione in cui il ritorno anticipato è solo breakun'affermazione. Una piega, mentre si ha ragione sul fatto che si tratta di un ciclo, in realtà è più vincolata di un costrutto di ciclo generale; è più simile a un ciclo for-each
J_mie6

1
@ J_mie6 il motivo per cui considero la ricorsione della coda più come una GOTOè che è necessario fare una contabilità complicata di quali argomenti in quale stato vengono passati alla chiamata ricorsiva, per assicurarsi che si comporti effettivamente come previsto. Ciò non è necessario nella stessa misura nei loop imperativi scritti in modo decente (dove è abbastanza chiaro quali sono le variabili stateful e come cambiano in ogni iterazione), né in una ricorsione ingenua (dove di solito non si fa molto con gli argomenti, e invece il il risultato è assemblato in modo abbastanza intuitivo). ...
circa il

1
... Per quanto riguarda le pieghe: hai ragione, una piega tradizionale (catamorfismo) è un tipo molto specifico di anello, ma questi schemi di ricorsione possono essere generalizzati (ana- / apo- / hylomorphisms); collettivamente questi sono IMO il corretto sostituto per i loop imperativi.
lasciato il

13

Mi piace molto la risposta di Jules , ma volevo anche sottolineare qualcosa che spesso manca alla programmazione pigra funzionale, ovvero che non tutto deve essere "all'interno del ciclo". Per esempio:

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

Puoi vedere che ogni parte della tua logica può essere calcolata in una funzione separata quindi composta insieme. Ciò consente funzioni più piccole che di solito sono molto più facili da risolvere. Per il tuo esempio di giocattolo, forse questo aggiunge più complessità di quanto non rimuova, ma nel codice del mondo reale le funzioni divise sono spesso molto più semplici del tutto.


la logica è sparsa dappertutto qui. questo codice non sarà facile da mantenere. nonstopAt10 è un buon consumatore. la tua risposta è migliore di quella che citi in quanto isola correttamente il produttore di valori di base . il loro consumo dovrebbe incorporare direttamente la logica di controllo, tuttavia è meglio implementato con solo due secondi e esplicitamente. anche questo seguirebbe da vicino la struttura e la logica del codice originale, e sarebbe facile da mantenere. scanl (+) 0spanlast
Will Ness,

6

La maggior parte degli esempi lista di elaborazione vedrete funzioni d'uso come map, filter, sumecc, che operano sulla lista nel suo complesso. Ma nel tuo caso hai un'uscita anticipata condizionale - un modello piuttosto insolito che non è supportato dalle normali operazioni dell'elenco. Quindi è necessario abbassare il livello di astrazione e utilizzare la ricorsione - che è anche più vicino a come appare l'esempio imperativo.

Questa è una traduzione piuttosto diretta (probabilmente non idiomatica) in 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))))))) 

Edit: punto di Jules che reducein Clojure fare supportare l'uscita anticipata. L'uso di questo è più 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)))

In ogni caso, puoi fare qualsiasi cosa nei linguaggi funzionali come puoi nei linguaggi imperativi, ma spesso devi cambiare un po 'la tua mentalità per trovare una soluzione elegante. Nella codifica imperativa pensi di elaborare un elenco passo dopo passo, mentre nei linguaggi funzionali cerchi un'operazione da applicare a tutti gli elementi dell'elenco.


guarda la modifica che ho appena aggiunto alla mia risposta: l' reduceoperazione di Clojure supporta l'uscita anticipata.
Jules

@Jules: Cool - questa è probabilmente una soluzione più idiomatica.
Jacques B

Inesatto - o takeWhilenon è un '"operazione comune"?
Jonathan Cast

@jcast: sebbene takeWhilesia un'operazione comune, in questo caso non è particolarmente utile, perché prima di poter decidere se interrompere è necessario disporre dei risultati della trasformazione. In un linguaggio pigro questo non ha importanza: puoi usare scane takeWhilesui risultati della scansione (vedi la risposta di Karl Bielefeldt, che mentre non usa takeWhilepotrebbe essere facilmente riscritta per farlo), ma per un linguaggio rigoroso come il clojure questo sarebbe significa elaborare l'intero elenco e quindi scartare i risultati in seguito. Le funzioni del generatore potrebbero risolvere questo, tuttavia, e credo che il clojure li supporti.
Jules

@Jules take-whilein Clojure produce una sequenza pigra (secondo i documenti). Un altro modo per affrontarlo sarebbe con i trasduttori (forse il migliore).
Will Ness,

4

Come sottolineato da altre risposte, Clojure deve reducedinterrompere anticipatamente le riduzioni:

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

Questa è la soluzione migliore per la tua situazione particolare. È inoltre possibile ottenere un sacco di chilometraggio dalla combinazione reducedcon transduce, che consente di utilizzare trasduttori da map, filterecc. Tuttavia, è tutt'altro che una risposta completa alla domanda generale.

Le continuazioni di escape sono una versione generalizzata di istruzioni break e return. Sono implementati direttamente in alcuni schemi ( call-with-escape-continuation), Common Lisp ( block+ return, catch+ throw) e persino in C ( setjmp+ longjmp). Continuazioni delimitate o non delimitate più generali come si trovano nello Schema standard o come monadi di continuazione in Haskell e Scala possono anche essere usate come continuazioni di fuga.

Ad esempio, in Racket potresti usare in let/ecquesto modo:

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

Molte altre lingue hanno anche costrutti simili alla continuazione della fuga sotto forma di gestione delle eccezioni. In Haskell puoi anche usare una delle varie monadi con errori foldM. Poiché si tratta principalmente di costrutti per la gestione degli errori che utilizzano eccezioni o monadi di errore per i primi ritorni è generalmente inaccettabile dal punto di vista culturale e forse piuttosto lenta.

Puoi anche passare dalle funzioni di ordine superiore alle chiamate di coda.

Quando si utilizzano i loop, si immette automaticamente la successiva iterazione quando si raggiunge la fine del corpo del loop. È possibile immettere l'iterazione successiva in anticipo con continueo uscire dal ciclo con break(o return). Quando si utilizzano le chiamate di coda (o il loopcostrutto di Clojure che imita la ricorsione della coda), è sempre necessario effettuare una chiamata esplicita per inserire l'iterazione successiva. Per interrompere il loop non devi semplicemente effettuare la chiamata ricorsiva ma dare direttamente il valore:

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

1
Per quanto riguarda l'utilizzo delle monadi degli errori in Haskell, non credo che ci sia una vera penalità per le prestazioni qui. Tendono a pensare lungo le linee della gestione delle eccezioni, ma non funzionano allo stesso modo e non è necessario alcun stack stack, quindi in realtà non dovrebbe essere un problema se utilizzato in questo modo. Inoltre, anche se c'è un motivo culturale per non usare qualcosa del genere MonadError, l'equivalente sostanzialmente Eithernon ha tale propensione verso la sola gestione degli errori, quindi può essere facilmente usato come sostituto.
Jules

@Jules Penso che il ritorno a sinistra non impedisca al fold di visitare l'intero elenco (o altra sequenza). Tuttavia, non ho familiarità con gli interni della libreria standard Haskell.
nilern

2

La parte intricata è il ciclo. Cominciamo da quello. Un ciclo viene in genere convertito in stile funzionale esprimendo l'iterazione con una singola funzione. Un'iterazione è una trasformazione della variabile loop.

Ecco un'implementazione funzionale di un ciclo generale:

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

Prende (un valore iniziale della variabile loop, la funzione che esprime una singola iterazione [sulla variabile loop]) (una condizione per continuare il ciclo).

Il tuo esempio usa un ciclo su un array, che si interrompe anche. Questa capacità nella tua lingua imperativa è inserita nella lingua stessa. Nella programmazione funzionale tale capacità è di solito implementata a livello di biblioteca. Ecco una possibile implementazione

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

Dentro :

Uso una coppia ((val, next_pos)) che contiene la variabile loop visibile all'esterno e la posizione nell'array, che questa funzione nasconde.

La funzione di iterazione è leggermente più complessa rispetto al ciclo generale, questa versione consente di utilizzare l'elemento corrente dell'array. [È in forma al curry .]

Tali funzioni sono generalmente denominate "fold".

Metto una "l" nel nome per indicare che l'accumulo degli elementi dell'array è fatto in modo associativo sinistro; imitare l'abitudine dei linguaggi di programmazione imperativa di iterare un array dall'indice basso a quello alto.

Ho messo una "c" nel nome per indicare che questa versione di fold ha una condizione che controlla se e quando il ciclo deve essere fermato in anticipo.

Naturalmente è probabile che tali funzioni di utilità siano prontamente disponibili nella libreria di base fornita con il linguaggio di programmazione funzionale utilizzato. Li ho scritti qui per la dimostrazione.

Ora che abbiamo tutti gli strumenti che sono nella lingua nel caso imperativo, possiamo rivolgerci per implementare la funzionalità specifica del tuo esempio.

La variabile nel tuo ciclo è una coppia ('risposta', un valore booleano che codifica se continuare).

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)

Nota che ho usato una nuova "variabile" "new_answer". Questo perché nella programmazione funzionale non posso cambiare il valore di una "variabile" già inizializzata. Non mi preoccupo delle prestazioni, il compilatore potrebbe riutilizzare la memoria di "risposta" per "new_answer" tramite l'analisi del tempo di vita, se ritiene che sia più efficiente.

Incorporando questo nella nostra funzione loop sviluppata in precedenza:

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

"Matrice" qui è il nome del modulo che è la funzione di esportazione foldlc.

"pugno", "secondo" indica funzioni che restituiscono il primo, secondo componente del suo parametro di coppia

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

In questo caso lo stile "senza punti" aumenta la leggibilità dell'implementazione di doSomeCalc:

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

(>>>) è la composizione della funzione: (>>>) : (a -> b) -> (b -> c) -> (a -> c)

È lo stesso di sopra, solo il parametro "arr" è escluso da entrambi i lati dell'equazione di definizione.

Un'ultima cosa: verificare il caso (array == null). In linguaggi di programmazione meglio progettati, ma anche in linguaggi mal progettati con una certa disciplina di base si usa piuttosto un tipo opzionale per esprimere la non esistenza. Questo non ha molto a che fare con la programmazione funzionale, di cui alla fine si tratta la domanda, quindi non me ne occupo.


0

Innanzitutto, riscrivi leggermente il ciclo, in modo tale che ogni iterazione del ciclo esca anticipatamente o muti answeresattamente una volta:

    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;
    }

Dovrebbe essere chiaro che il comportamento di questa versione è esattamente lo stesso di prima, ma ora è molto più semplice convertire in stile ricorsivo. Ecco una traduzione diretta di 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

Ora è puramente funzionale, ma possiamo migliorarlo sia dal punto di vista dell'efficienza che della leggibilità utilizzando una piega anziché una ricorsione esplicita:

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)

In questo contesto, Leftuscite anticipate con il suo valore eRight continua la ricorsione con il suo valore.


Questo ora potrebbe essere semplificato un po 'di più, in questo modo:

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

Questo è meglio come il codice Haskell finale, ma ora è un po 'meno chiaro come si associa alla Java originale.

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.