Κλείσιμο (επιστήμη υπολογιστών)

Στην πληροφορική, κλείσιμο (αγγλ: closure) ονομάζεται μια συνάρτηση πρώτης τάξης με ελεύθερες μεταβλητές που έχουν δεσμευτεί στο λεκτικό περιβάλλον. Μια τέτοια συνάρτηση λέγεται ότι «κλείνεται» ως προς τις ελεύθερες μεταβλητές της. Ένα κλείσιμο ορίζεται τουλάχιστον μέσα στα όρια στα οποία είναι ορατές οι ελεύθερες μεταβλητές του. Η ρητή χρήση κλεισιμάτων είναι συνδεδεμένη με το συναρτησιακό προγραμματισμό και με γλώσσες όπως η ML και η Lisp. Τα κλεισίματα χρησιμοποιούνται για την υλοποίηση στυλ περάσματος συνεχειών, επιτυγχάνοντας με αυτόν τον τρόπο την απόκρυψη δεδομένων (hiding state). Τα αντικείμενα και οι δομές ελέγχου μπορούν να υλοποιηθούν με αυτόν τον τρόπο.

Η ιδέα των κλεισιμάτων αναπτύχθηκε κατά τη δεκαετία του 1960 και υλοποιήθηκε αρχικά στη γλώσσα προγραμματισμού Scheme. Από τότε έχουν σχεδιαστεί πολλές γλώσσες που υποστηρίζουν κλεισίματα. Σε κάποιες γλώσσες, ένα κλείσιμο μπορεί να προκύψει όταν μια συνάρτηση ορίζεται μέσα σε μια άλλη συνάρτηση και η εσωτερική αναφέρεται σε μεταβλητές της εξωτερικής. Στο χρόνο εκτέλεσης, όταν εκτελείται η εξωτερική συνάρτηση, δημιουργείται ένα κλείσιμο, που αποτελείται από την εσωτερική συνάρτηση και όποιες αναφορές σε μεταβλητές της εξωτερικής χρειάζονται (αυτές οι τιμές ονομάζονται upvalues του κλεισίματος).

Ο όρος κλείσιμο συχνά συγχέεται με τον όρο ανώνυμη συνάρτηση. Αυτό μάλλον οφείλεται στο ότι οι περισσότερες γλώσσες που υλοποιούν ανώνυμες συναρτήσεις επιτρέπουν σε αυτές το σχηματισμό κλεισιμάτων και ο προγραμματιστής μαθαίνει αυτές τις δυο ιδέες ταυτόχρονα, αν και διαφέρουν ως έννοιες.

Τα κλεισίματα είναι συγγενείς έννοιες με τα συναρτησιακά αντικείμενα - ο μετασχηματισμός τους σε αυτά ονομάζεται defunctionalization ή lambda lifting.

Ετυμολογία

Επεξεργασία

Το 1964, ο Peter J. Landin όρισε ότι ένα κλείσιμο (closure) έχει ένα τμήμα περιβάλλοντος (environment part) και ένα τμήμα ελέγχου (control part) και τα χρησιμοποίησε στη μηχανή SECD για την αποτίμηση εκφράσεων.[1] Ο Joel Moses υποστηρίζει ότι ο όρος closure χρησιμοποιήθηκε από τον Landin για να αναφέρεται σε μια λ-έκφραση, οι ελεύθερες δεσμεύσεις της οποίας (ελεύθερες μεταβλητές) έχουν κλειστεί (δεσμευτεί) από το λεκτικό περιβάλλον, με αποτέλεσμα μια κλειστή έκφραση, ή κλείσιμο.[2][3] Αυτή η χρήση υιοθετήθηκε στη συνέχεια από τον Sussman και τον Steele όταν έδωσαν τον ορισμό της γλώσσας προγραμματισμού Scheme το 1975,[4] και ο όρος διαδόθηκε.

Κλεισίματα και συναρτήσεις πρώτης τάξης

Επεξεργασία

Τα κλεισίματα συνήθως εμφανίζονται σε γλώσσες στις οποίες οι συναρτήσεις είναι τιμές πρώτης τάξης—σε αυτές τις γλώσσες μια συνάρτηση μπορεί να περαστεί ως παράμετρος, να επιστραφεί από κλήση άλλης συνάρτησης, να δεσμευτεί σε κάποιο όνομα μεταβλητής, κλπ., ακριβώς όπως οι απλούστεροι τύποι (π.χ. συμβολοσειρές, ακέραιοι). Έστω για παράδειγμα, η εξής συνάρτηση σε Scheme:

; Επιστρέφει μια λίστα με όλα τα βιβλία από τα οποία έχουν πουληθεί τουλάχιστον THRESHOLD αντίτυπα.
(define (best-selling-books threshold)
  (filter 
    (lambda (book) (>= (book-sales book) threshold))
    book-list))

Σε αυτό το παράδειγμα, η λ-έκφραση (lambda (book) (>= (book-sales book) threshold)) εμφανίζεται μέσα στη συνάρτηση best-selling-books. Όταν η λ-έκφραση αποτιμάται, η Scheme δημιουργεί ένα κλείσιμο που αποτελείται από τον κώδικα της συνάρτησης και μια αναφορά στη μεταβλητή threshold, η οποία είναι ελεύθερη μεταβλητή της συνάρτησης.

Το κλείσιμο περνιέται στη συνέχεια στη συνάρτηση filter function, η οποία το καλεί επανειλημμένα για να βρει ποια βιβλία πρέπει να προστεθούν στη λίστα των αποτελεσμάτων και ποια όχι. Επειδή το ίδιο το κλείσιμο έχει μια αναφορά στη threshold, μπορεί να χρησιμοποιήσει αυτήν τη μεταβλητή κάθε φορά που η filter την καλεί. Η ίδια η συνάρτηση filter μπορεί να ορίζεται σε κάποιο άλλο αρχείο κώδικα.

Ακολουθεί ένα παράδειγμα σε JavaScript, μια άλλη δημοφιλή γλώσσα που υποστηρίζει κλεισίματα:

// Επιστρέφει μια λίστα με όλα τα βιβλία από τα οποία έχουν πουληθεί τουλάχιστον 'threshold' αντίτυπα.
function bestSellingBooks(threshold) {
  return bookList.filter(
      function (book) { return book.sales >= threshold; }
    );
}

Η λέξη-κλειδί function χρησιμοποιείται εδώ αντί της lambda, και η Array.filter method[5] αντί της καθολικής συνάρτησης filter, αλλά η δομή και το αποτέλεσμα του κώδικα είναι ίδια.

Μια συνάρτηση μπορεί να δημιουργεί ένα κλείσιμο και να το επιστρέφει, όπως στο εξής παράδειγμα:

// Επιστρέφει μια συνάρτηση που συγκλίνει στην παράγωγο της f
// χρησιμοποιώντας ένα, κατάλληλα μικρό, διάστημα dx.
function derivative(f, dx) {
  return function (x) {
    return (f(x + dx) - f(x)) / dx;
  };
}

Επειδή το κλείσιμο σε αυτήν την περίπτωση ζει περισσότερο από τη συνάρτηση που το δημιουργεί, οι μεταβλητές f και dx ζουν μετά την επιστροφή από την κλήση της derivative. Σε γλώσσες χωρίς κλεισίματα, ο χρόνος ζωής μια τοπικής μεταβλητής συμπίπτει με την εκτέλεση της εμβέλειας στην οποία η μεταβλητή αυτή έχει οριστεί. Σε γλώσσες με κλεισίματα, οι μεταβλητές πρέπει να συνεχίσουν να υπάρχουν, για όσο χρόνο κάποιο κλείσιμο αναφέρεται σε αυτές. Αυτό συνήθως υλοποιείται με κάποια χρήση της τεχνικής της συλλογής απορριμμάτων.

Κλεισίματα και αναπαράσταση κατάστασης

Επεξεργασία

Ένα κλείσιμο μπορεί να χρησιμοποιηθεί για να αντιστοιχίσει μια συνάρτηση σε ένα σύνολο "ιδιωτικών" μεταβλητών, οι οποίες να διατηρούνται ανάμεσα σε διαφορετικές κλήσεις της συνάρτησης. Η εμβέλεια των μεταβλητών περικλείει μόνο τη συνάρτηση που κλείνεται, επομένως άλλος κώδικας δε μπορεί να έχει πρόσβαση σε αυτές.

Σε γλώσσες που υποστηρίζουν μεταβλητή κατάσταση, τα κλεισίματα μπορούν να χρησιμοποιηθούν για να υλοποιηθούν τρόποι προγραμματισμού που αναπαριστούν κατάσταση και απόκρυψη πληροφορίας, επειδή τα upvalues του κλεισίματος (οι κλεισμένες μεταβλητές του) δεν έχουν όριο στο χρόνο ζωής τους, επομένως οι τιμές τους διατηρούνται ανάμεσα σε διαδοχικές κλήσεις και είναι πάντα διαθέσιμες. Τα κλεισίματα που χρησιμοποιούνται με αυτόν τον τρόπο δεν έχουν πια διαφάνεια αναφοράς και επομένως δεν είναι πια αγνές συναρτήσεις, όμως χρησιμοποιούνται συχνά σε "σχεδόν-συναρτησιακές" γλώσσες όπως η Scheme.

Χρήση των κλεισιμάτων

Επεξεργασία

Τα κλεισίματα έχουν πολλές χρήσεις:

  • Επειδή τα κλεισίματα καθυστερούν την αποτίμηση—δηλ. δεν "κάνουν" κάτι μέχρι να κληθούν—μπορούν να χρησιμοποιηθούν για να οριστούν δομές ελέγχου. Για παράδειγμα, όλες οι κλασικές δομές ελέγχου της Smalltalk, συμπεριλαμβανομένων των δομών διακλάδωσης (if/then/else) και των βρόχων (while και for), ορίζονται με τη χρήση αντικειμένων των οποίων οι μέθοδοι δέχονται κλεισίματα. Ο χρήστης μπορεί επίσης να ορίσει τις δικές του δομές ελέγχου.
  • Σε γλώσσες που επιτρέπεται η ανάθεση, μπορούν να παραχθούν πολλαπλές συναρτήσεις που κλείνουν στο ίδιο περιβάλλον, οι οποίες μπορούν να επικοινωνούν χωρίς παρέμβαση άλλου κώδικα, αλλάζοντας αυτό το περιβάλλον. Έστω ο κώδικας σε Scheme:
(define foo #f)
(define bar #f)

(let ((secret-message "none"))
  (set! foo (lambda (msg) (set! secret-message msg)))
  (set! bar (lambda () secret-message)))

(display (bar)) ; τυπώνει "none"
(newline)
(foo "meet me by the docks at midnight")
(display (bar)) ; τυπώνει "meet me by the docks at midnight"

Σημείωση: ορισμένοι αποκαλούν κλείσιμο κάθε δομή δεδομένων που δεσμεύει ένα λεκτικό περιβάλλον, αλλά συνήθως ο όρος αναφέρεται σε συναρτήσεις.

Διαφορές στη σημασιολογία

Επεξεργασία

Διαφορετικές γλώσσες έχουν διαφορετικούς ορισμούς για τα λεκτικά περιβάλλοντα, επομένως ο ορισμός του κλεισίματος διαφέρει από γλώσσα σε γλώσσα. Ο ελάχιστος κοινά αποδεκτός ορισμός του λεκτικού περιβάλλοντος είναι ότι αυτό ορίζει όλες τις δεσμεύσεις μεταβλητών σε μια εμβέλεια, και είναι αυτό το περιβάλλον που πρέπει να κρατούν τα κλεισίματα μιας γλώσσας. Όμως η έννοια της δέσμευσης μεταβλητής επίσης διαφέρει. Στις προστακτικές γλώσσες οι μεταβλητές δεσμεύονται σε σχετικές θέσεις μνήμης που μπορούν να αποθηκεύσουν τιμές. Αν και η σχετική θέση μιας δέσμευσης δεν αλλάζει στο χρόνο εκτέλεσης, η τιμή της μεταβλητής δε μένει πάντα σταθερή. Σε αυτές τις γλώσσες, επειδή το κλείσιμο κρατά τη δέσμευση, κάθε ενέργεια στη μεταβλητή, είτε γίνεται μέσα από το κλείσιμο, είτε όχι, γίνεται στην ίδια σχετική θέση μνήμης. Ακολουθεί ένα παράδειγμα σε μια τέτοια γλώσσα, την ECMAScript:

var f, g;
function foo() {
  var x = 0;
  f = function() { return ++x; };
  g = function() { return --x; };
  x = 1;
  alert('inside foo, call to f(): ' + f()); // "2"
}
foo();
alert('call to g(): ' + g()); // "1"
alert('call to f(): ' + f()); // "2"

Η συνάρτηση foo και τα κλεισίματα που αναφέρονται από τις μεταβλητές f και g χρησιμοποιούν την ίδια σχετική θέση μνήμης, η οποία δηλώνεται από την τοπική μεταβλητή x.

Από την άλλη πλευρά, πολλές συναρτησιακές γλώσσες, όπως η ML, δεσμεύουν μεταβλητές κατευθείαν σε τιμές. Σε αυτήν την περίπτωση, επειδή δεν υπάρχει τρόπος να αλλάξει μια τιμή μιας μεταβλητής μετά τη δέσμευση, δε χρειάζεται να υπάρχει κοινή κατάσταση (state) ανάμεσα στα κλεισίματα—απλά χρησιμοποιούν τις ίδιες τιμές.

Μια άλλη κατηγορία γλωσσών, οι οκνηρές συναρτησιακές γλώσσες προγραμματισμού, όπως η Haskell, δεσμεύουν μεταβλητές σε αποτελέσματα μελλοντικών υπολογισμών και όχι σε τελικές τιμές. Έστω το εξής παράδειγμα σε Haskell:

 
foo x y = let r = x / y
          in (\z -> z + r)
f = foo 1 0
main = print (f 123)

Η δέσμευση της r κρατείται από το κλείσιμο που ορίζεται μέσα στη συνάρτησηfoo και δείχνει στον υπολογισμό (x / y) - ο οποίος σε αυτήν την περίπτωση οδηγεί σε διαίρεση με το μηδέν. Όμως, επειδή είναι ο υπολογισμός που δεσμέυεται, και όχι η τιμή, το σφάλμα εμφανίζεται μόνο όταν καλείται το κλείσιμο και προσπαθεί να χρησιμοποιήσει τη δέσμευση.

Διαφορές επίσης εμφανίζονται στη συμπεριφορά άλλων δομών με λεκτική εμβέλεια, όπως οι εντολές return, break και continue. Αυτές οι δομές μπορούν συνήθως να θεωρηθούν το αποτέλεσμα της κλήσης μιας συνέχειας διαφυγής (escape continuation) που έχει οριστεί από μια εντολή ελέγχου που περικλείει τον κώδικα (στην περίπτωση των break και continue, αυτή η ερμηνεία απαιτεί οι αναδρομικές κλήσεις συναρτήσεων να θεωρούνται με όρους δομών επανάληψης). Σε κάποιες γλώσσες, όπως η ECMAScript, η return αναφέρεται στη συνέχεια που ορίζεται από το λεκτικά πιο εσωτερικό κλείσιμο σε σχέση με την εντολή—επομένως μια return μέσα σε ένα κλείσιμο μεταφέρει τον έλεγχο στον κώδικα που το κάλεσε. Στη Smalltalk, όμως, ο φαινομενικά όμοιος τελεστής ^ καλεί τη συνέχεια διαφυγής που ορίστηκε από την κλήση της μεθόδου, αγνοώντας συνέχειες διαφυγής ενδιάμεσων εμφωλευμένων κλεισιμάτων. Η συνέχεια διαφυγής ενός συγκεκριμένου κλεισίματος μπορεί να κληθεί στη Smalltalk μόνο έμμεσα, όταν η εκτέλεση φτάνει στο τέλος του κώδικα του κλεισίματος. Η διαφορά φαίνεται στα εξής παραδείγματα σε ECMAScript και Smalltalk:

"Smalltalk"
foo
  | xs |
  xs := #(1 2 3 4).
  xs do: [:x | ^x].
  ^0
bar
  Transcript show: (self foo printString) "τυπώνει 1"
// ECMAScript
function foo() {
  var xs = new Array(1, 2, 3, 4);
  xs.forEach(function(x) { return x; });
  return 0;
}
alert(foo()); // τυπώνει 0

Τα παραπάνω κομμάτια κώδικα συμπεριφέρονται διαφορετικά γιατί ο τελεστής ^ της Smalltalk και ο τελεστής return της JavaScript δεν είναι όμοιοι σε λειτουργία. Στο παράδειγμα σε ECMAScript, η εντολή return x θα αφήσει το εσωτερικό κλείσιμο για να αρχίσει μια νέα επανάληψη του βρόχου forEach, ενώ στο παράδειγμα σε Smalltalk, η εντολή ^x θα τερματίσει το βρόχο και θα επιστρέψει από τη μέθοδο foo.

Η Common Lisp παρέχει μια δομή που μπορεί να εκφράσει και τις δύο παραπάνω λειτουργίες: η ^x της Smalltalk συμπεριφέρεται όπως η (return-from foo x), ενώ η return x της JavaScript συμπεριφέρεται όπως η (return-from nil x). Επομένως, η Smalltalk επιτρέπει σε μια συνέχεια διαφυγής που κρατείται να ζήσει περισσότερο από την εμβέλεια από την οποία μπορεί να κληθεί με επιτυχία. Έστω το εξής παράδειγμα:

foo
    ^[ :x | ^x ]
bar
    | f |
    f := self foo.
    f value: 123 "error!"

Όταν καλείται το κλείσιμο που επιστρέφεται από τη μέθοδο foo, προσπαθεί να επιστρέψει μια τιμή από την κλήση της foo που δημιούργησε το κλείσιμο. Η κλήση αυτή όμως έχει ήδη επιστρέψει και το μοντέλο κλήσης μεθόδου της Smalltalk δεν ακολουθεί την αρχή της στοίβας-σπαγγέτι για να επιτρέπει πολλαπλές επιστροφές, με αποτέλεσμα ο κώδικας να έχει ως αποτέλεσμα σφάλμα.

Κάποιες γλώσσες, όπως η Ruby, επιτρέπουν στον προγραμματιστή να επιλέγει τον τρόπο με τον οποίο κρατείται η return. Παράδειγμα σε Ruby:

# ruby
def foo
  f = Proc.new { return "return from foo from inside proc" }
  f.call # ο έλεγχος αφήνει τη foo εδώ
  return "return from foo"
end

def bar
  f = lambda { return "return from lambda" }
  f.call # ο έλεγχος δεν αφήνει την bar εδώ
  return "return from bar"
end
  
puts foo # τυπώνει "return from foo from inside proc"
puts bar # τυπώνει "return from bar"

Οι Proc.new και lambda σε αυτό το παράδειγμα είναι τρόποι να δημιουργηθεί ένα κλείσιμο, αλλά η σημασιολογία των παραγόμενων κλεισιμάτων διαφέρει από αυτήν της εντολής return.

Ο ορισμός και η εμβέλεια της εντολής ελέγχου return στη Scheme είναι ρητός (και ονομάζεται 'return' απλά για το συγκεκριμένο παράδειγμα). Ο κώδικας που ακολουθεί είναι μια απευθείας μετάφραση του κώδικα Ruby.

(define call/cc call-with-current-continuation)

(define (foo)
  (call/cc 
   (lambda (return)
     (define (f) (return "return from foo from inside proc"))
     (f) ; ο έλεγχος αφήνει τη foo εδώ
     (return "return from foo"))))

(define (bar)
  (call/cc
   (lambda (return)
     (define (f) (call/cc (lambda (return) (return "return from lambda"))))
     (f) ; ο έλεγχος δεν αφήνει την bar εδώ
     (return "return from bar"))))

(display (foo)) ; τυπώνει "return from foo from inside proc"
(newline)
(display (bar)) ; τυπώνει "return from bar"

Υλοποίηση και θεωρία

Επεξεργασία

Τα κλεισίματα συνήθως υλοποιούνται με μια ειδική δομή δεδομένων που περιέχει ένα δείκτη στον κώδικα της συνάρτησης και μια αναπαράσταση του λεκτικού περιβάλλοντος της συνάρτησης (π.χ. το σύνολο των διαθέσιμων μεταβλητών και τις τιμές τους) τη στιγμή που δημιουργήθηκε το κλείσιμο.

Η υλοποίηση μιας γλώσσας δε μπορεί εύκολα να υποστηρίξει πλήρη κλεισίματα αν το μοντέλο μνήμης του χρόνου εκτέλεσης της δεσμεύει μνήμη για τις μεταβλητές της μόνο σε μια γραμμική στοίβα. Σε αυτές τις γλώσσες, ο χώρος μνήμης για τις τοπικές μεταβλητές μιας συνάρτησης αποδεσμεύεται όταν επιστρέφει η συνάρτηση. Ένα κλείσιμο όμως απαιτεί οι μεταβλητές στις οποίες αναφέρεται να ζουν και μετά την εκτέλεση της συνάρτησης που περικλείει τον κώδικα. Επομένως, ο χώρος για αυτές τις μεταβλητές πρέπει να δεσμεύεται για όσο χρειάζεται. Αυτό εξηγεί γιατί, συνήθως, οι γλώσσες που υποστηρίζουν κλεισίματα χρησιμοποιούν συλλογή απορριμμάτων. Η εναλλακτική λύση είναι η γλώσσα να αποδέχεται ότι μπορεί να προκύψουν κάποιες περιπτώσεις με απροσδιόριστη συμπεριφορά, όπως η πρόταση για λ-εκφράσεις στη C++.[7] Το πρόβλημα Funarg περιγράφει τη δυσκολία υλοποίησης συναρτήσεων ως αντικείμενα πρώτης τάξης σε μια γλώσσα προγραμματισμού βασισμένη στη στοίβα, όπως η C ή η C++. Στην έκδοση 1 της D, θεωρείται ότι ο προγραμματιστής ξέρει τι κάνει με τα delegates και τις τοπικές μεταβλητές, αφού οι αναφορές τους δεν ισχύουν όταν ο κώδικας επιστρέφει από την εμβέλεια που έχει οριστεί (οι τοπικές μεταβλητές είναι στη στοίβα) - αυτό εξακολουθεί να επιτρέπει αρκετά χρήσιμα σχήματα του συναρτησιακού προγραμματισμού αλλά σε περίπλοκες καταστάσεις είναι απαραίτητη η χειροκίνητη δέσμευση μνήμης για τις μεταβλητές. Η έκδοση 2 της D έλυσε αυτό το πρόβλημα εντοπίζοντας τις μεταβλητές που πρέπει να αποθηκεύονται στο σωρό και δεσμεύει μνήμη αυτόματα για αυτές. Επειδή η D χρησιμοποιεί συλλογή απορριμμάτων και στις δύο εκδόσεις, δε χρειάζεται να παρακολουθείται η χρήση των μεταβλητών όταν περνιούνται.

Στις συναρτησιακές γλώσσες με αμετάβλητα (immutable) δεδομένα όπως η Erlang, είναι πολύ εύκολο να υλοποιηθεί η αυτόματη διαχείριση μνήμης (συλλογή απορριμμάτων), γιατί δε μπορούν να υπάρξουν κύκλοι στις αναφορές των μεταβλητών. Για παράδειγμα, στην Erlang, όλα τα ορίσματα και όλες οι μεταβήτές αποθηκεύονται στο σωρό, αλλά αναφορές σε αυτά αποθηκεύονται και στη στοίβα. Με αυτόν τον τρόπο, ακόμα και όταν μια συνάρτηση επιστρέψει, οι αναφορές είναι ακόμα έγκυρες. Ο σωρός καθαρίζεται από έναν σταδιακό συλλέκτη απορριμμάτων (incremental garbage collector).

Στην ML οι τοπικές μεταβλητές αποθηκεύονται σε μια γραμμική στοίβα. Όταν δημιουργείται ένα κλείσιμο, αντιγράφει τις τιμές των μεταβλητών που χρειάζεται, στη δομή δεδομένων του.

Η Scheme, η οποία έχει ένα σύστημα λεκτικής εμβέλειας σαν της Algol, με δυναμικές μεταβλητές και συλλογή απορριμμάτων, δε διαθέτει κάποιο μοντέλο προγραμματισμού στοίβας και δεν έχει τους περιορισμούς των γλωσσών που βασίζονται σε στοίβα. Τα κλεισίματα εκφράζονται φυσικά στη Scheme. Η μορφή lambda περικλείει τον κώδικα και τις ελεύθερες μεταβλητές του περιβάλλοντός του, ζει στο πρόγραμμα για όσο χρόνο πρέπει να υπάρχει πρόσβαση σε αυτή, και μπορεί να χρησιμοποιηθεί τόσο ελεύθερα, όσο κάθε άλλη έκφραση της Scheme.

Τα κλεισίματα έχουν στενή σχέση με τους Actors του μοντέλου Actor για ταυτόχρονο υπολογισμό, στο οποίο οι τιμές του λεκτικού περιβάλλοντος ονομάζονται acquaintances. Ένα σημαντικό σημείο όσον αφορά τα κλεισίματα στις γλώσσες ταυτόχρονου προγραμματισμού (concurrent programming) είναι αν οι μεταβλητές ενός κλεισίματος μπορούν να τροποποιηθούν και, αν ναι, πώς συγχρονίζονται αυτές οι αλλαγές. Οι Actors παρέχουν μια λύση για αυτό.[8]

Δομές κλεισιμάτων σε άλλες γλώσσες

Επεξεργασία

Στη γλώσσα προγραμματισμού C, κάποιες βιβλιοθήκες που επιστρέφουν συναρτήσεις callback επιτρέπουν μια τέτοια συνάρτηση να ορίζεται με τη χρήση δύο τιμών: ενός δείκτη σε συνάρτηση και ενός ξεχωριστού δείκτη void* σε αυθαίρετα δεδομένα που επιλέγει ο χρήστης. Κάθε φορά που η βιβλιοθήκη εκτελεί τη συνάρτηση, περνάει το δείκτη δεδομένων. Αυτό επιτρέπει τη διατήρηση δεδομένων και την αναφορά σε πληροφορία που είχε κρατηθεί κατά το χρόνο που ορίστηκε. Αυτό το ιδίωμα μοιάζει με τα κλεισίματα όσον αφορά τη λειτουργικότητα, αν και διαφέρει στη σύνταξη.

Αρκετές αντικειμενοστρεφείς τεχνικές και χαρακτηριστικά γλωσσών προσομοιώνουν κάποια από τα χαρακτηριστικά των κλεισιμάτων. Για παράδειγμα:

Η Eiffel περιλαμβάνει τους inline agents που ορίζουν κλεισίματα και είναι αντικείμενα που αναπαριστούν μια ρουτίνα, δίνοντας τον κώδικα αυτής κατευθείαν (in line). Για παράδειγμα, στο:

OK_button.click_event.subscribe(
	agent(x, y: INTEGER) do
		country := map.country_at_coordinates(x, y)
		country.display
	end
)

το όρισμα στην subscribe είναι ένας agent που αναπαριστά μια διαδικασία με δύο ορίσματα: η διαδικασία βρίσκει τη χώρη σε αυτές τις συντεταγμένες και στη δείχνει. Ολόκληρος ο agent έχει "συνδρομή" (είναι "subscribed") στον τύπο συμβάντος click_event για ένα συγκεκριμένο κουμπί, και όταν λενα στιγμιότυπο του συμβάντος εμφανίζεται σε αυτό το κουμπί - ο χρήστης πατάει το κουμπί - η διαδικασία θα εκτελεστεί με ορίσματα x και y τις συντεταγμένες του ποντικιού.

Οι βασικοί περιορισμοί των agents της Eiffel, που τους διαφοροποιεί από τα πραγματικά κλεισίματα, είναι ότι δε μπορούν να αναφέρονται σε τοπικές μεταβλητές από εμβέλεια που να τις περικλείει, αν και αυτό μπορεί να διορθωθεί με το πέρασμα επιπλέον κλειστών τελεστέων στον agent. Μόνο το Current (η αναφορά στο τρέχον αντικείμενο, όπως το this στη Java), τα χαρακτηριστικά του, και τα ορίσματα του ίδιου του agent μπορούν να φαίνονται μέσα από το σώμα του agent.

Στην Erlang τα κλεισίματα υποστηρίζονται απλά με τη χρήση του fun (το όνομα στην Erlang's για μια ανώνυμη συνάρτηση) με αναφορές σε εξωτερικές μεταβλητές. Επειδή η Erlang είναι μια συναρτησιακή γλώσσα με σημασιολογία περάσματος αμετάβλητων δεδομένων, είναι εύκολο να κατασκευάζονται fun, να εκτελούνται και να γίνεται διαχείριση της μνήμης. Η υλοποίηση γίνεται από μια κρυμμένη συνάρτηση στο επίπεδο των μονάδων κώδικα (module-level) με N+M παραμέτρους (N - αριθμός των κλειστών εξωτερικών μεταβλητών, M - αριθμός των κανονικών παραμέτρων της συνάρτησης), η οποία είναι επίσης πολύ απλή (βλ. Lambda lifting).

construct_filter(L) ->
  Filter = fun (X) -> lists:member(X, L) end,  % κατασκευάζουμε κλείσιμο με τη χρήση του L σε αυτό το fun
  Filter.

complex_filter(SmallListOfSearchedElements, BigListToBeSearched) ->
  Filter = construct_filter(SmallListOfSearchedElements),
  Result = lists:filter(Filter, BigListToBeSearched),
  Result.

Η C++ επιτρέπει τον ορισμό συναρτησιακών αντικειμένων με την υπερφόρτωση του operator(). Αυτά τα αντικείμενα συμπεριφέρονται περίπου όπως οι συναρτήσεις σε μια συναρτησιακή γλώσσα προγραμματισμού. Μπορούν να δημιουργηθούν στο χρόνο εκτέλεσης και να περιέχουν δεδομένα κατάστασης αλλά δεν κρατούν τις τοπικές μεταβλητές έμμεσα, όπως τα κλεισίματα. Η Επιτροπή του Πρότυπου της C++ (C++ Standards Committee) επεξεργάζεται δύο προτάσεις για την εισαγωγή υποστήριξης για κλεισίματα στη C++ (και οι δύο προτάσεις τα αποκαλούν λ-συναρτήσεις) [1], [2]. Η βασική διαφορά μεταξύ αυτών των δύο είναι ότι η μια κανονικά αποθηκεύει ένα αντίγραφο όλων των τοπικών μεταβλητων σε ένα κλείσιμο, ενώ η άλλη αποθηκεύει αναφορές στις αρχικές μεταβλητές. Και οι δύο παρέχουν την επιλογή να αλλάζει η συμπεριφορά τους. Αν κάποια μορφή αυτών των προτάσεων γινόταν δεκτή, θα μπορούσε κάποιος να γράψει

void foo(string myname) {
	typedef vector<string> names;
	int y;
	names n;
	// ...
	names::iterator i =
	 find_if(n.begin(), n.end(), [&](const string& s){return s != myname && s.size() > y;});
	// το 'i' είτε είναι τώρα 'n.end()' ή δείχνει στην πρώτη συμβολοσειρά του 'n'
	// που δεν είναι ίση με τη 'myname' και το μήκος της είναι μεγαλύτερο από 'y'
}

Τουλάχιστον δύο μεταγλωττιστές της C++, ο Visual C++ 2010 (γνωστός και ως Visual C++ 10.0) και ο gcc-4.5, ήδη υποστηρίζουν αυτό το συμβολισμό.

Τα κλεισίματα υλοποιούνται από τα delegates στη γλώσσα προγραμματισμού D.

auto test1() {
    int a = 7;
    return delegate() { return a + 3; }; // κατασκευή ανώνυμο delegate
}

auto test2() {
    int a = 20;
    int foo() { return a + 5; } // εσωτερική συνάρτηση
    return &foo;  // ένας άλλος τρόπος να κατασκευαστεί ένα delegate
}

void bar() {
  auto dg = test1();
  dg();    // =10   // ok, η test1.a είναι μέσα σε ένα κλείσιμο και υπάρχει ακόμα

  dg = test2();
  dg();    // =25   // ok, η test2.a είναι μέσα σε ένα κλείσιμο και υπάρχει ακόμα
}

Η έκδοση 1 της D έχει περιορισμένη υποστήριξη για κλεισίματα. Για παράδειγμα, ο παραπάνω κώδικας δε θα λειτουργήσει σωστά γιατί η μεταβλητή a είναι στη στοίβα και μετά την επιστροφή από τη συνάρτηση test() δε είναι πια έγκυρη η χρήση της μεταβλητής (αν κληθεί η foo μέσω της dg(), μάλλον θα επιστρέψει κάποιον 'τυχαίο' ακέραιο). Αυτό μπορεί να λυθεί με τη ρητή δέσμευση μνήμης από το σωρό για την a ή με τη χρήση δομών (structs) ή κλάσεων για την αποθήκευση όλων των απαραίτητων κλειστών μεταβλητών και την κατασκευή του κατάλληλου delegate που στη μέθοδό του να έχει τον ίδιο κώδικα. Αυτός ο περιορισμός σπάνια συναντάται στην πράξη.

Ο περιορισμός αυτός διορθώθηκε στην έκδοση 2 της D.

Η Delphi περιλαμβάνει υποστήριξη για κλεισίματα που υλοποιούνται ως ανώνυμες μέθοδοι από την έκδοση του 2009. Μπορούν να δηλωθούν εμβόλιμα (inline) μέσα σε μια άλλη μέθοδο, και κρατούν αναφορές στην τοπική κατάσταση. Για παράδειγμα:

type
  TCounter = reference to function: integer;

function MakeCounter: TCounter;
var
  i: integer;
begin
  i := 0;
  result := function: integer
            begin
              inc(i);
              result := i;
            end;
end;

Η κλήση της MakeCounter θα επιστρέψει μια νέα ανώνυμη μέθοδο. Η πρώτη κλήση θα επιστρέψει 1, η δεύτερη 2, κλπ.

Οι ανώνυμες συναρτήσεις στην Perl γίνονται κλεισίματα όταν αναφέρονται σε μεταβλητές που υπάρχουν στα περιβάλλοντα που τις περικλείουν.

sub number_of_bagels {
    my $bagels = shift;
    return sub { my $sub_arg = shift; return $sub_arg + $bagels; };
}

my $func = number_of_bagels(5);
print "I now have " . $func->(2) . " bagels!\n";
print "I now have " . $func->(10) . " bagels!\n";

Η Python έχει δύο τύπους κλεισιμάτων: λ-εκφράσεις και κλεισίματα με όνομα (συναρτήσεις που ορίζονται μέσα σε άλλες συναρτήσεις).

Οι λ-εκφράσεις έχουν αυτήν τη μορφή: lambda x: x+3 και ισοδύναμα, σε περίπτωση δύο παραμέτρων: lambda x,y: x*y ή αν δεν υπάρχουν παράμετροι: lambda: foo(). Είναι περιορισμένες από το ότι πρέπει να είναι μια μόνο έκφραση και όχι ολόκληρη εντολή. Δεν είναι δυνατό να τοποθετηθεί μια ενότητα κώδικα με ένα "for", "while", ή "if" σε μια λ-έκφραση. (Αν και οι βρόχοι "for" μπορούν να προσομοιωθούν από μια έκφραση list comprehension, π.χ. lambda x: [foo(y) for y in x], και οι δομές "if" μπορούν να προσομοιωθούν με εκφράσεις συνθήκης, π.χ. lambda x: foo(x) if x == 2 else bar(x). Και οι δύο μπορούν να συνδυαστούν με τη χρήση "if" μέσα σε list comprehensions, π.χ. lambda x: [foo(y) for y in x if y > 5]).

Ο άλλος τύπος κλεισιμάτων είναι τα κλεισίματα με όνομα, όπως φαίνεται από το εξής παράδειγμα σε Python 3.1:

#δοκιμασμένο σε python 3.1

def outer():
    y = 0
    def inner():
        nonlocal y
        y += 1
        return y
    return inner

f = outer()
print(f(), f(), f()) #τυπώνει 1 2 3

Η εμφωλευμένη δήλωση συνάρτησης def inner() στην πραγματικότητα δηλώνει ένα κλείσιμο και του δίνει το όνομα "inner". Αυτές οι εμφωλευμένες συναρτήσεις έχουν την πλήρη ισχύ των κλεισιμάτων: το λεκτικό περιβάλλον κρατείται τη στιγμή που η έκφραση "def" αποτιμάται και μπορεί να γίνει αναφορά ή ανάθεση σε αυτό ακόμα και όταν η συνάρτηση που περικλείει τον κώδικα δεν είναι πια ορατή, όπως φάνηκε.

Ο καλύτερος τρόπος να φανεί πώς λειτουργεί μια δήλωση μιας εμφωλευμένης συνάρτησης είναι να θεωρηθεί ότι είναι ισοδύναμη με τη δημιουργία ενός κλεισίματος που ακολουθείται από την ανάθεσή του σε μια μεταβλητή. Για παράδειγμα, ο παρακάτω κώδικας είναι όπως θα έμοιαζε η προηγούμενη συνάρτηση σε μια υποθετική Python που οι λ-εκφράσεις δεν είναι πια περιορισμένες:

# ΠΡΟΣΟΧΗ: Το παρακάτω δεν είναι έγκυρος κώδικας Python!
# Είναι ψευδοκώδικας που χρησιμοποιεί σύνταξη σαν αυτή της Python.

def outer():
    y = 0
    inner = lambda: (
        nonlocal y
        y += 1
        return y
    )
    return inner

f = outer()
print(f(), f(), f()) #τυπώνει 1 2 3

Η Java επιτρέπει τον ορισμό "ανώνυμων κλάσεων" μέσα σε μια μέθοδο - μια ανώνυμη κλάση μπορεί να αναφέρεται σε ονόματα σε λεκτικά περιβάλλουσες κλάσεις ή σε μεταβλητές μόνο για ανάγνωση (σημειωμένες σαν final) στη λεκτικά περιβάλλουσα μέθοδο.

class CalculationWindow extends JFrame {
	private volatile int result;
	...

	public void calculateInSeparateThread(final URI uri) {
		// Η έκφραση "new Runnable() { ... }" είναι μια ανώνυμη κλάση.
		Runnable runner = new Runnable() {
			void run() {
				// Μπορεί να διαβάσει τοπικές final μεταβλητές:
				calculate(uri);
				// Μπορεί να έχει πρόσβαση σε ιδιωτικά πεδία της περιβάλλουσας κλάσης:
				result = result + 10;
			}
		};
		new Thread(runner).start();
	}
}

Κάποια χαρακτηριστικά των κλεισιμάτων μπορούν να προσομοιωθούν με τη χρήση μιας αναφοράς που είναι final και δείχνει σε ένα μεταβλητό υποδοχέα (mutable container), για παράδειγμα, σε έναν πίνακα ενός στοιχείου. Η εσωτερική κλάση δε μπορεί να αλλάξει την τιμή της ίδιας της αναφοράς αλλά μπορεί να αλλάξει τα περιεχόμενα αυτού στο οποίο δείχνει.

Σύμμφωνα με μια πρόταση για τη Java 7[9], τα κλεισίματα θα επιτρέψουν στον παραπάνω κώδικα να εκτελείται ως εξής:

class CalculationWindow extends JFrame {
	private volatile int result;
	...
	public void calculateInSeparateThread(final URI uri) {
                // ο κώδικας #(){ /* code */ } είναι ένα κλείσιμο
                new Thread(#(){ 
                    calculate( uri ); 
                    result = result + 10; 
                }).start();
	}
}

Η Java επίσης υποστηρίζει μια άλλη μορφή κλάσεων, τις εσωτερικές (ή εμφωλευμένες) κλάσεις.[10][11] Αυτές ορίζονται στο σώμα μιας περιβάλλουσας κλάσης και έχουν πλήρη πρόσβαση σε κάθε μεταβλητή στιγμιότυπου αυτής, με αποτέλεσμα να μοιάζουν αρκετά με κλασικά κλεισίματα συναρτήσεων. Λόγω της δέσμευσής της σε αυτές τις μεταβλητές στιγμιότυπου, η δημιουργία ενός στιγμιότυπου μιας εσωτερικής κλάσης περιλαμβάνει ειδική σύνταξη για τη δέσμευση σε ένα στιγμιότυπο της περιβάλλουσας τάξης.

public class EnclosingClass {
	/* Ορίζει την εσωτερική κλάση */
	public class InnerClass {
		public int incrementAndReturnCounter() {
			return counter++;
		}
	}

	private int counter;

	{
		counter = 0;
	}

	public int getCounter() {
		return counter;
	}

	public static void main(String[] args) {
		EnclosingClass enclosingClassInstance = new EnclosingClass();
		/* Δημιουργεί στιγμιότυπο της εσωτερικής κλάσης, με δέσμευση στο εξωτερικό στιγμιότυπο */
		EnclosingClass.InnerClass innerClassInstance =
			enclosingClassInstance.new InnerClass();

		for(int i = enclosingClassInstance.getCounter(); (i =
		innerClassInstance.incrementAndReturnCounter()) < 10;) {
			System.out.println(i);
		}
	}
}

Όταν εκτελεστεί, θα τυπώσει τους ακέραιους από το 0 έως το 9. Αυτός ο τύπος κλάσης δεν πρέπει να συγχέεται με τη στατική εσωτερική κλάση, η οποία δηλώνεται με τον ίδιο τρόπο και τη χρήση της λέξης-κλειδί "static" - ο δεύτερος τύπος κλάσης δεν έχει το απαιτούμενο αποτέλεσμα αλλά είναι απλά κλάσεις χωρίς καμία δέσμευση που ορίζονται μέσα σε μια άλλη κλάση.

Έχουν υπάρξει κάποιες προτάσεις για πιο φιλική σύνταξη για κλεισίματα στη Java[12][13][14].

Η Objective-C 2.0 (στο Mac OS X 10.6 "Snow Leopard" και στο iOS 4.0) υποστηρίζει ενότητες κώδικα (blocks). Οι μεταβλητές κλεισιμάτων σημειώνονται με __block.

typedef int (^IntBlock)();

IntBlock downCounter(int start) {
	 __block int i = start;
	 return [[ ^int() {
		 return i--;
	 } copy] autorelease];
 }

IntBlock f = downCounter(5);
NSLog(@"%d", f());
NSLog(@"%d", f());
NSLog(@"%d", f());

Δείτε επίσης

Επεξεργασία

Παραπομπές

Επεξεργασία
  1. P. J. Landin (1964), The mechanical evaluation of expressions 
  2. Joel Moses (1970), The Function of FUNCTION in LISP, or Why the FUNARG Problem Should Be Called the Environment Problem, AI Memo 199, http://dspace.mit.edu/handle/1721.1/5854, ανακτήθηκε στις 2009-10-27 
  3. Åke Wikström (1987). Functional Programming using Standard ML. ISBN 0-13-331968-7. The reason it is called a "closure" is that an expression containing free variables is called an "open" expression, and by associating to it the bindings of its free variables, you close it. 
  4. Gerald Jay Sussman and Guy L. Steele, Jr. (1975), Scheme: An Interpreter for the Extended Lambda Calculus, AI Memo 349 
  5. «array.filter». Mozilla Developer Center. 10 Ιανουαρίου 2010. Ανακτήθηκε στις 9 Φεβρουαρίου 2010. 
  6. «Re: FP, OO and relations. Does anyone trump the others?». 29 Δεκεμβρίου 1999. Αρχειοθετήθηκε από το πρωτότυπο στις 26 Δεκεμβρίου 2008. Ανακτήθηκε στις 23 Δεκεμβρίου 2008. 
  7. Lambda Expressions and Closures C++ Standards Committee. 29 February 2008.
  8. Foundations of Actor Semantics Will Clinger. MIT Mathematics Doctoral Dissertation. June 1981.
  9. «Java 7 Closure Draft Specification». 
  10. «Nested Classes (The Java™ Tutorials > Learning the Java Language > Classes and Objects)». 
  11. «Inner Class Example (The Java™ Tutorials > Learning the Java Language > Classes and Objects)». 
  12. http://www.javac.info/
  13. http://docs.google.com/View?docid=k73_1ggr36h[νεκρός σύνδεσμος]
  14. «Αρχειοθετημένο αντίγραφο». Αρχειοθετήθηκε από το πρωτότυπο στις 22 Ιουλίου 2010. Ανακτήθηκε στις 26 Ιουλίου 2010. 

Εξωτερικοί σύνδεσμοι

Επεξεργασία

Τα κλεισίματα στη Delphi

Επεξεργασία

Τα κλεισίματα στη Ruby

Επεξεργασία