Articoli Manifesto Tools Links Canali Libri Contatti ?
Linguaggi / Ruby

La magia di call/cc

Abstract
call-with-current-continuation, per gli amici call/cc, è una di quelle cose di cui non molti hanno sentito parlare e pochi comprendono davvero. Io non sono nella fascia del comprendono davvero, ma quel che ho capito spero di riuscire a spiegarlo, cancellando un po' di quell'aura magica che circonda questo meccanismo.
Data di stesura: 09/08/2004
Data di pubblicazione: 20/09/2004
Ultima modifica: 04/04/2006
di Gabriele Renzi Discuti sul forum   Stampa

In collaborazione con Gruppo Utenti Ruby Italia
Contenuti
Uno dei problemi nella comprensione di call/cc è che questa funzionalità non è molto diffusa.
Pochi sono i linguaggi/piattaforme che la forniscono come base, tra questi Scheme, SML/NJ, e Ruby. Altri linguaggi, come CommonLisp o SmallTalk riescono in vari modi ad 'emularè questa caratteristica.
Altri linguaggi ancora, esistono su piattaforme che mettono a disposizione funzioni che permettono di manipolare le continuazioni, ad esempio StacklessPython[1] (interprete python), o l'interprete JavaScript integrato in cocoon[2], pur non avendole nella versione principale.
Questi sono buoni esempi del fatto che call/cc non dipende da costrutti sintattici particolari (come throws in java o lambda in python), ma viene invocata come una "normale" funzione di libreria.

[nota: in Scheme, ad esempio, le strutture di controllo sono implementate con call/cc, ma call/cc non rappresenta una forma particolare come define o lambda, bensì una semplice funzione]

Infine, va ricordato il linguaggio unlambda, che ha solo tre istruzioni, di cui una (k) dovrebbe corrispondere a call/cc. Dico dovrebbe perché il codice unlambda non è particolarmente comprensibile: la serie di fibonacci in unlambda si calcola così:

  1. ```s``s``sii``s`kk`ki`ki``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s``s`ks` 
  2. `s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`k 
  3. s``s`kk`kk``s`kk`kr``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s``s`ks``s`kk 
  4. `ks``s``s`ks``s`kk`kk`ki``s``s`ks``s`kk`kk``s`kk`k.*``s``s`ks``s`kk`kk 
  5. ``s`kki``s``s`ks``s`kk`kk``s`kki``s`kk`ki``s``s`ks``s``s`ks``s`kk`ks`` 
  6. s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s`kk`kk``s`kk`k``s``s`ks``s``s`ks` 
  7. `s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s`kk`kk``s`kk`ks``s``s`k 
  8. s``s``s`ks``s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s`kk`kk``s`kk 
  9. `ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s`kk`kk``s`kk`kk``s``s`ks``s` 
  10. kk`kk``s`kki``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s`kk`kk``s`kk`kk``s` 
  11. kk`ki``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks`` 
  12. s`kk`kk``s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s``s`ks``s`kk`ks 
  13. ``s``s`ks``s`kk`kk``s`kk`ks``s``s`ks``s``s`ks``s`kk`ks``s``s`ks``s`kk` 
  14. kk``s`kk`kk``s``s`ks``s`kk`kk`ki``s``s`ks``s``s`ks``s`kk`ks``s``s`ks`` 
  15. s`kk`kk``s`kk`kk``s`kk`ki``s``s`ks``s`kk`kk``s`kk`ki``s``s`ks``s`kk`kk 
  16. `ki``s`kk`ki 

Ad ogni modo, cos'è una continuazione?
Esistono molte definizioni ad effetto: una continuazione è uno stato. Una continuazione è il futuro di una computazione. Le continuazioni sono la versione matematica del goto. Le continuazioni sono eccezioni sotto steroidi. E altra roba così.

Ma probabilmente queste definizioni non vi aiutano a capire molto. Almeno finché non arrivate ad un epifania in cui tutti i pezzi vanno a posto e potete finalmente esclamare "ah, ma cavolo ...". Io proverò a farvi arrivare a quel punto, speriamo bene.

Un programma è composto di vari passi:

  1. a=10 
  2. b=20 
  3. a=a+b 
  4. print a 
  5. => 30 
In ogni passo, il programma è diviso in due: la parte già eseguita e la parte da eseguire. In un linguaggio dove esista il GOTO possiamo passare da un punto all'altro:
  1. 10: a=10 
  2. 20: b=20 
  3. 30: a=a+b 
  4. 40: print a 
  5. 50: GOTO 40 
  6. => 3030303030303030303030... 
Il problema nell'uso dei GOTO è che questi sono cittadini di seconda classe nel mondo della programmazione. Non si può prendere una label e mandarla in giro per il programma, ed in genere non si può saltare ad un indirizzo stabilito a runtime.
I GOTO sono qualcosa che esiste solo a livello di codice, non di programma.

Insomma, i goto sono cattivi. Le continuazioni invece, non lo sono.

La continuazione, come il goto, è un warp spaziotemporale, con la differenza che possiamo prenderlo e portarcelo in giro.
Ma ora, per una questione di correttezza verso chi lo ha già fatto dobbiamo sbattere la testa su un minimo di teoria.
In genere si è abituati a pensare che un "passo" nello svolgimento di un programma corrisponda grossomodo al concetto di "chiama una funzione F".
È facile visualizzare la cosa pensando in Scheme:

  1. (+ 1 2) 
  2. (display 1) 
  3. (set! foo 10) 
un po' più strano in ruby:
  1. 1+2 
  2. puts 1 
  3. foo=10 
Ma sotto sotto il concetto è quello. In realtà i teorici hanno dimostrato che diventa molto più semplice parlare di informatica se un passo viene definito come:
"chiama una funzione F con la continuazione K"
Che in un certo modo equivale a dire "nel contesto K".
Dunque gli esempi di prima in Scheme sarebbero:
  1. (+ 1 2 K1) 
  2. (display 1 K2) 
  3. (set! foo 10 K3) 
o in pseudo ruby:
  1. 1+2 in K1 
  2. puts 1 in K2 
  3. foo=10 in k3 
Ora, fin qui non c'è niente di strano, abbiamo delle variabili un po' inutili passata qua e là, ma il sistema è sempre quello.

A questo punto compare la magia di call/cc.
chiama-con-la-continuazione-attuale significa appunto che ad una funzione verrà passata una variabile che rappresenta la computazione corrente.
Esempio usando la console interattiva di ruby:

>> callcc do |k|
?>  puts 'ciao'
>> end
ciao
=> nil
Come vedete nulla di strano, c'è quella variabile inutile e basta. A questo punto proviamo a salvare quella variabile da qualche parte:
>> $kont=nil # le variabili con il $ sono globali
=> nil
>> callcc do |k|
?>  $kont=k
>> end
=> #<Continuation:0x401f76f0>
>> $kont
=> #<Continuation:0x401f76f0>
Ora, una continuazione funziona più o meno come una funzione, ed infatti ha un solo metodo utile, #call, che serve, appunto, a chiamarla.
Ma che succede se la chiamiamo? Proviamo da riga di comando:
[nickel@UltimaThule nickel]$ ruby
$kont=nil
callcc do |k|
    $kont=k
end
$kont.call


-:5: Interrupt
Se non avete ben chiaro quel che è successo, provate a guardare questo:
callcc do |k|
    $kont=k
end
puts 'sono nella continuazione k'
$kont.call


sono nella continuazione k
sono nella continuazione k
sono nella continuazione k
sono nella continuazione k
sono nella continuazione k
sono nella continuazione k
-:4: Interrupt
Quello che facciamo è dire all'interprete "esegui questa continuazione"
Ma la continuazione non è altro che la funzione puts e poi la chiamata della continuazione ("esegui questa continuazione").. e così via.
Potete notare anche una cosa importante. Usando le continuazioni in questo modo non c'è mai uno stack overflow.
Lo stack overflow esiste per le funzioni quando, ad esempio eseguiamo una funzione come questa:
  1. def fact(n) 
  2.     if n>1 
  3.         fact(n-1)* n 
  4.     else 
  5.     end 
  6. end 
Perché ad ogni esecuzione della funzione bisogna mantenere in memoria tutto lo stack delle chiamate, infatti ad un certo punto, avremo bisogno di restituire qualcosa alla funzione chiamante, e alla sua funzione chiamante e così via.
Una continuazione, invece, non ritorna mai.
Eseguire una funzione è come entrare in varie stanze in una casa.
Eseguire una continuazione è come entrare in varie stanze e trovarsi sempre all'ingresso.

Ouch, sono ricaduto nella trappola di parlare di callcc in modo magico.
Una spiegazione più semplice per chi ha una minima idea di come funzioni un computer è che quando salvate una continuazione fate una copia dello stack, dei registri e del program counter.
Quando la chiamate non fate altro che cancellare stack e registri correnti, ricopiarci sopra quelli salvati e ricominciare l'esecuzione.
In effetti la specifica C99 mette a disposizione due funzioni che fanno più o meno questo: setjmp() salva lo stack, e longjmp() lo ripristina.
Per mostrare il fatto che callcc non ritorna mai basta che guardiate questo esempio:

  1. callcc do |k| 
  2.    $kont=k 
  3. end 
  4. puts 'sono nella continuazione k' 
  5. $kont.call 
  6. abort "un drago ha incenerito il programma" 
non raggiungerete mai la chiamata ad #abort.
Il che è positivo, perché evitiamo un po' di magia.

A questo punto dovreste aver chiara, bene o male, la potenza di callcc.
Avendo a disposizione questa funzione potete programmare qualsiasi struttura di controllo, dal'if-then-else alle eccezioni.
A proposito di eccezioni, una cosa semplice che potete fare con callcc in ruby è costruire eccezioni resumabili. Cos'è un'eccezione resumabile?
Si tratta di un meccanismo tipico di smalltalk, in cui una volta catturata un'eccezione è possibile usarla per riprendere la computazione dal punto esatto in cui è stata sollevata,piuttosto che ripetere l'intero blocco di codice.
Ruby non le supporta nativamente, ma possiamo implementarle facilmente.
Prima di tutto creiamo una sottoclasse di Exception chiamata ResumableException:

  1. class ResumableException < Exception 
  2.     def initialize (k,msg) 
  3.         super msg 
  4.         @kont=k # quelle con la @ sono variabili d'istanza 
  5.     end 
  6.     def resume 
  7.         @kont.call 
  8.     end 
  9. end 
L'unica differenza rispetto a una normale eccezione è che questa include una continuazione ed un metodo per chiamarla. Ora scriviamo un metodo che crei questo tipo di eccezione e la lanci, salvando la continuazione:
  1. def resumeable_raise(msg) 
  2.     callcc do |k| 
  3.         raise ResumableException.new k, msg 
  4.     end 
  5. end 
Ed infine un metodo di test:
  1. def test 
  2.     puts "pre-eccezione" 
  3.     resumeable_raise "eccezione!!" 
  4.     puts "post-eccezione" 
  5. rescue Exception => e 
  6.     p "recupero da "+ e.inspect 
  7.     e.resume 
  8. end 
  9.  
  10. test() 
Eseguendo dovreste avere un risultato di questo tipo:
pre-eccezione
"recupero da #<ResumableException: eccezione!!>"
post-eccezione
Piuttosto semplice no? Ora provate a farlo in Java!

A questo punto dovreste aver capito un minimo della logica di callcc.
E per rendere le cose più interessanti, mi permetto di scopiazzare senza alcuna vergogna "Teach Yourself Scheme in Fixnum Days" [6], proponendovi una semplice implementazione di operatore non deterministico.
Se qualcuno un giorno scriverà "Teach Yourself QualcosAltro in Fixnum Days" lo invito a copiare da qui.
L'importante è avere call/cc e i Fixnum.

Cosa si intende per operatore non deterministico?
Un qualcosa che sia in grado di scegliere la soluzione migliore o peggiore per il nostro programma, senza bisogno di indicargliela.
Ad esempio, potremmo scrivere:

  1. amb=Amb.new 
  2. x,y= amb.choose(1,2), amb.choose(1,2) 
  3. x* 30 == y* 15 
  4. puts x,y #=> 1, 2 
e l'operatore si preoccuperebbe di trovare x ed y al posto nostro.
Si tratta di un classico sistema di backtracking sullo stile dei linguaggi tipo prolog o mercury. Ovviamente ruby non è pensato come un linguaggio constraint based quindi sarà parecchio lento nel svolgere le operazioni, ma il concetto è comunque interessante.
Dunque, la prima cosa che facciamo è definire una classe Amb. Il nome viene dalla tradizione e sta per qualcosa tipo "ambivalente".
Definiamo una classe perché abbiamo bisogno di qualcosa che mantenga uno stato ed abbia delle funzioni, quindi la scelta è ovvia:
  1. class Amb 
  2.   def initialize 
  3.       @paths = [] 
  4.   end 
L'array (@paths)delle scelte conterrà tutte le varie continuazioni.
Poi aggiungiamo un metodo per "riempire" l'array dei possibili percorsi:
  1. def choose *choices 
  2.     choices.each do |choice| 
  3.         callcc do |k| 
  4.             @paths << k 
  5.             return choice 
  6.         end 
  7.     end 
  8.     fail 
  9. end 
Questo metodo sembra complicato ma non lo è.
Anzitutto iteriamo sulle scelte passate in input.
Per ognuna creiamo una continuazione, la aggiungiamo all'array, e poi restituiamo la scelta. Cercate di capire bene: non riempiamo l'array con le possibili scelte! Quello che l'array contiene è una delle possibili continuazioni, e quello che il metodo restituisce è la scelta relativa a quella continuazione.
Scrivendo
  1. x= amb.choose 1,2 
Otterremo che x vale 1 e che @paths è qualcosa tipo
[#<Continuation:0x401f4374>]
un array di un solo elemento, un oggetto continuazione. Quando la continuazione 0x401f4374 verrà richiamata eseguiremo un nuovo passo nell'iterazione, infilando un nuovo valore in @paths e rendendo x= 2.
Scrivendo:
  1. x,y=amb.choose( 1,2), amb.choose( 1,2) 
Otterremo che @paths diventi
[#<Continuation:0x401f4374>, #<Continuation:0x401f5a30>]
Ciò rappresenta chiaramente che le biforcazioni dei percorsi possibili sono due, e corrispondono all'assegnazione di x ed a quella di y.
Ma quando chiamiamo questa continuazione? Semplice, quando il nostro calcolo fallisce. Questo è appunto il metodo #fail.
  1. def fail 
  2.     if @paths.empty? 
  3.         abort "Choice tree exhausted." 
  4.     else 
  5.        @paths.pop.call 
  6.     end 
  7.   end 
Che tradotto passo passo significa: "se ci sono ancora continuazioni levane una dalla lista e richiamala, altrimenti termina dicendolo all'utente"
È importante notare che #fail. viene richiamato anche quando abbiamo finito di iterare sulle possibili scelte, per mostrare, appunto, che abbiamo fallito.
A questo punto vediamo di far funzionare il nostro esempio di prima:
  1. amb=Amb.new 
  2. x= amb.choose 1,2 
  3. puts "x ora vale "+x.to_s 
  4. puts "amb e' "+amb.inspect 
  5. y= amb.choose 1,2 
  6. puts "y ora vale "+y.to_s 
  7. puts "amb e' "+amb.inspect 
  8.  
  9. if x* 30 == y* 15 
  10.     puts "soluzione trovata per x="+ x.to_s + " ed y=" + y.to_s 
  11. else 
  12.     amb.fail 
  13. end 
che da questo output
x ora vale 1
amb e' #<Amb:0x401c4fe4
@paths=[#<Continuation:0x401c4f6c>]>
y ora vale 1
amb e' #<Amb:0x401c4fe4
@paths=[#<Continuation:0x401c4f6c>,#<Continuation:0x401c4e40>]>
y ora vale 2
amb e' #<Amb:0x401c4fe4 @paths=[#<Continuation:0x401c4f6c>,
#<Continuation:0x401c4d3c>]>
soluzione trovata per x=1 ed y=2
Simpatico vero?
Il nostro amb è piuttosto stupido, in quanto eaurisce tutto lo spazio delle soluzioni finché non ne trova una. Se volessimo trovarle tutte basterebbe aggiungere una condizione mai soddisfatta.
In questo modo amb continuerebbe a cercarne fino ad esaurire tutte le possibilità, e mostrandoci tutte le soluzioni possibili.
Vediamo un altro esempio:
  1. tot = 315 
  2.                                                                               
  3. amb = Amb.new 
  4. a=amb.choose 1,2,3,4,5,6,7  
  5. b=amb.choose 1,2,3,4,5,6,7 
  6. c=amb.choose 1,2,3,4,5,6,7 
  7. d=amb.choose 1,2,3,4,5,6,7 
  8.  
  9. if tot == a * b * c * d 
  10.     puts "#{tot} == #{a} * #{b} * #{c} * #{d}" 
  11. else 
  12.     amb.fail 
  13. end 
che da in output:
315 == 3 * 3 * 5 * 7

La cosa affascinante è che possiamo estendere la nostra classe Amb!
Ad esempio possiamo programmare una classe Amb per la soluzione del problema dei quattro colori (cioè, assegnare i colori alle nazioni su una mappa in modo che due nazioni confinanti non abbiano mai lo stesso):
Creiamo una classe Paese che abbia un nome, un colore ed una lista di paesi vicini:

  1. class Paese 
  2.     attr_accessor :colore, :vicini 
  3.     def initialize nome, colore, *vicini 
  4.         @nome,@colore,@vicini=nome,colore,vicini 
  5.     end 
  6.     def to_s 
  7.         "#{@nome}: colore #{@colore}, colori vicini:  #{@vicini.join ","}" 
  8.     end 
  9. end 
Poi subclassiamo Amb:
  1. class ColorAmb < Amb 
  2.     def choose_color 
  3.         choose 'rosso', 'giallo', 'blu', 'verde' 
  4.     end 
  5. end 
Poi facciamo una scelta di colori per ogni paese:
  1. amb=ColorAmb.new 
  2. po= amb.choose_color 
  3. sp= amb.choose_color 
  4. fr= amb.choose_color 
  5. be= amb.choose_color 
  6. ho= amb.choose_color 
  7. ge= amb.choose_color 
  8. lu= amb.choose_color 
  9. it= amb.choose_color 
  10. sv= amb.choose_color 
  11. au= amb.choose_color 
e creiamo un array di paesi:
  1. paesi=[ Paese.new( 'portogallo',    # nome 
  2.                    po,              # colore 
  3.                    sp),             # vicini 
  4.         Paese.new( 'spagna', 
  5.                    sp, 
  6.                    fr,po), 
  7.         Paese.new( 'francia', 
  8.                    fr, 
  9.                    sp,it,sv,be,ge,lu), 
  10.         Paese.new( 'belgio', 
  11.                    be, 
  12.                    fr,ho,lu,ge), 
  13.         Paese.new( 'olanda', 
  14.                    ho, 
  15.                    be,ge), 
  16.         Paese.new( 'germania', 
  17.                    ge, 
  18.                    fr,au,sv,ho,be,lu), 
  19.         Paese.new( 'lussemburgo', 
  20.                    lu, 
  21.                    fr,be,ge), 
  22.         Paese.new( 'italia', 
  23.                    it, 
  24.                    fr,au,sv), 
  25.         Paese.new( 'svizzera', 
  26.                    sv, 
  27.                    fr,it,au,ge), 
  28.         Paese.new( 'austria', 
  29.                     au, 
  30.                     it,sv,ge), 
Ora dichiariamo che la scelta non ha funzionato se il colore di un paese è uguale a quello dei vicini:
  1. paesi.each do |p| 
  2.     amb.fail if p.vicini.include?(p.colore) 
  3. end 
E visualizziamo il risultato:
  1. puts paesi 
portogallo: colore italia: colore rosso, colori vicini:  giallo,rosso,verde,
colori vicini:  rosso
spagna: colore rosso, colori vicini:  giallo,italia: colore rosso, colori
vicini:  giallo,rosso,verde
francia: colore giallo, colori vicini:  rosso,rosso,verde,rosso,blu,verde
belgio: colore rosso, colori vicini:  giallo,giallo,verde,blu
olanda: colore giallo, colori vicini:  rosso,blu
germania: colore blu, colori vicini:  giallo,giallo,verde,giallo,rosso,verde
lussemburgo: colore verde, colori vicini:  giallo,rosso,blu
italia: colore rosso, colori vicini:  giallo,giallo,verde
austria: colore giallo, colori vicini:  rosso,verde,blu
Funziona che è un incanto.
In realtà avere la possibilità di gestire le continuazioni in questo modo apre delle possibilità illimitate. Possiamo costruire delle coroutine, dei sistemi di concorrenza, o possiamo trasformare degli iteratori interni (quelli classici di ruby) in esterni (quelli di Jave o c++).
E possiamo usarle per costruire dei web framework. Eh già, in effetti l'uso di maggior successo di callcc è quello nei cosiddetti web framework modali.
Il concetto di base è che l'interazione con un utente in una applicazione web viene visto come una sola lunga computazione, e mentre l'utente riempie form o clicca "indietro" nel browser noi non facciamo altro che saltare da un punto della computazione all'altro.
Questo approccio è molto molto potente, ed infatti molti lo hanno reinventato indipendentemente, in vari linguaggi e con virtù e difetti differenti. Seaside2[3] per smalltalk, UncommonWeb[4] per CommonLISP e Borges[5] per ruby sono i framework che incarnano meglio questo concetto.
Ma cosa c'è di diverso in questo tipo di applicazioni? Citando Avi Bryant, l'autore di Seaside (ed inizialmente di Borges), la stessa differenza che c'è tar la programmazione strutturata ed i GOTO.
In un'applicazione web classica non abbiamo un flusso, ma una serie di salti da un punto all'altro del programma, incontrollabile. Grazie alla possibilità di gestire le continuazioni esplicitamente, possiamo linearizzare questo saltellio, e scrivere applicazioni in modo molti più semplice.

Conclusione

Certamente gli usi e le possibilità date da callcc non sono state esaurite in questo breve testo. Quello che spero è avervi dato la possibilità di capire un minimo quel che rappresenta questo meccanismo, e magari, avervi fatto venire un po' di curiosità. In fondo a tutti piace la magia, no?

Informazioni sull'autore

Gabriele Renzi, studente di Ingegneria Informatica, è un appassionato di programmazione e sistemi operativi. Scrive "hello world" in una dozzina di linguaggi ma non riesce ad andare oltre nemmeno in uno.
Collabora con il Progetto Documentazione Italiana FreeBSD ed è membro del Gruppo Utenti Ruby Italia.

È possibile consultare l'elenco degli articoli scritti da Gabriele Renzi.

Altri articoli sul tema Linguaggi / Ruby.

Risorse

  1. Stackless è un fork di python che non usa lo stack C e fornisce continuazioni, coroutine e microthread (thread implementati con continuazioni)
    http://www.stackless.com/
  2. cocoon è usa un'interprete javascript modificato con oggetti di tipo continuazione per gestire il flusso di applicazioni web.
    http://cocoon.apache.org
  3. la versione più evoluta di web framework continuation based
    http://beta4.com/seaside2
  4. ispirato a seaside, UCW è scritto in common lisp, ed usa un continuation passing style transformer invece di callcc (CL non ha call/cc)
    http://www.common-lisp.net/project/ucw/
  5. un clone di Seaside2. È interessante che il primo Seaside è era un clone di un web framework per ruby chiamato IOWA.
    http://borges.rubyforge.org
  6. Teach Yourself Scheme in Fixnum Days
    http://www.ccs.neu.edu/home/dorai/t-y-scheme/t-y-scheme.html
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.