Οκνηρή αποτίμηση

Στη θεωρία των γλωσσών προγραμματισμού, η οκνηρή αποτίμηση (lazy evaluation) ή κλήση κατ' ανάγκη (call-by-need) είναι μια στρατηγική αποτίμησης (evaluation strategy) που καθυστερεί την αποτίμηση μιας έκφρασης μέχρι αυτή να χρειαστεί (μη αυστηρή αποτίμηση, non-strict evaluation) και αποφεύγει επαναλαμβανόμενους υπολογισμούς (μοίρασμα, sharing). Το μοίρασμα μπορεί να μειώσει το χρόνο εκτέλεσης συγκεκριμένων συναρτήσεων εκθετικά σε σχέση με άλλες μη αυστηρές στρατηγικές αποτίμησης, όπως η κλήση κατ' όνομα ((call-by-name).

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

Το αντίθετο της οκνηρής αποτίμησης είναι η πρόθυμη αποτίμηση (eager evaluation), γνωστή και ως αυστηρή αποτίμηση (strict evaluation). Η αυστηρή αποτίμηση χρησιμοποιείται πιο συχνά στις γλώσσες προγραμματισμού.

Ιστορία 
Η οκνηρή αποτίμηση εισήχθηκε στο λ-λογισμό από τον  και στις γλώσσες προγραμματισμού ανεξάρτητα από τους  και τους .

Εφαρμογές 
Η καθυστερημένη αποτίμηση χρησιμοποιείται περισσότερο στις συναρτησιακές γλώσσες. Όταν γίνεται χρήση της καθυστερημένης υλοποίησης, μια έκφραση δεν αποτιμάται όταν δεσμεύεται σε μια μεταβλητή, αλλά όταν πρέπει να βρεθεί η τιμή της έκφρασης. Κατά αυτόν τον τρόπο, μια εντολή όπως η x:=έκφραση; (δηλ. η ανάθεση του αποτελέσματος μιας έκφρασης σε μια μεταβλητή) φαίνεται να ζητά την αποτίμηση της έκφρασης ώστε το αποτέλεσμα να τοποθετηθεί στην x, αλλά τα περιεχόμενα της x δεν έχουν σημασία μέχρι να εμφανιστεί ανάγκη για την τιμή της μέσω κάποιας αναφοράς στην x αργότερα, σε κάποια άλλη έκφραση, η οποία με τη σειρά της μπορεί να καθυστερεί, αν και τελικά το δέντρο των εξαρτήσεων που θα προκύψει θα πρέπει να παράγει κάποιο σύμβολο για τον έξω κόσμο.

Κάποιες γλώσσες προγραμματισμού καθυστερούν πάντα την αποτίμηση των εκφράσεων, ενώ άλλες προσφέρουν συναρτήσεις ή ειδική σύνταξη για την καθυστέρηση της αποτίμησης. Στη Miranda και τη Haskell, οι παράμετροι των συναρτήσεων αποτιμώνται καθυστερημένα. Σε πολλές άλλες γλώσσες, η αποτίμηση μπορεί να καθυστερήσει σταματώντας ρητά τον υπολογισμό, με χρήση ειδικής σύνταξης (όπως οι "delay" και "force" της Scheme και οι "lazy" και "Lazy.force" της OCaml) ή γενικότερα με το πακετάρισμα της έκφρασης σε ένα thunk (καθυστερημένος υπολογισμός). Το αντικείμενο που αναπαριστά μια ρητα καθυστερημένη αποτίμηση ονομάζεται μέλλον (future) ή υπόσχεση (promise). Η Perl 6 χρησιμοποιεί οκνηρή αποτίμηση στις λίστες: ο προγραμματιστής μπορεί να αναθέσει άπειρες λίστες σε μεταβλητές και να τις περάσει σε συναρτήσεις, αλλά, σε αντίθεση με τη Haskell και τη Miranda, η Perl 6 δεν χρησιμοποιεί οκνηρή αποτίμηση από προεπιλογή στους αριθμητικούς τελεστές και στις συναρτήσεις.

Η καθυστερημένη αποτίμηση έχει το πλεονέκτημα ότι μπορεί να δημιουργήσει υπολογίσιμες άπειρες λίστες χωρίς άπειρους βρόχους ή άλλα προβλήματα μεγέθους στον υπολογισμό. Για παράδειγμα, μπορεί να δημιουργηθεί μια συνάρτηση που δημιουργεί μια άπειρη λίστα (η οποία λίστα συχνά αποκαλείται ροή, αγγλ.stream) από αριθμούς Φιμπονάτσι. Ο υπολογισμός του ν-οστού αριθμού Φιμπονάτσι είναι απλά η εξαγωγή αυτού του στοιχείου από την άπειρη λίστα, που προκαλεί την αποτίμηση μόνο των πρώτων ν στοιχείων της λίστας.

Για παράδειγμα, στη Haskell, η λίστα όλων των αριθμών Φιμπονάτσι μπορεί να γραφτεί ως εξής:

 fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

Στη σύνταξη της Haskell, ο τελεστής ":" προσθέτει ένα στοιχείο στην αρχή της λίστας, η συνάρτηση tail επιστρεφει μια λίστα χωρίς το πρώτο της στοιχείο και η zipWith χρησιμοποιεί κάποια συνάρτηση (εδώ την πρόσθεση) για να συνδυάσει ένα-προς-ένα τα στοιχεία από δύο λίστες για να δημιουργήσει μια τρίτη.

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

Δομές ελέγχου 
Η εντολή if είναι πάντα οκνηρή, ακόμα και στις κοινές (πρόθυμες) γλώσσες. Η έκφραση
 if a then b else c
υπολογίζει την υποέκφραση (a) και μετά, αν και μόνο αν η (a) είναι αληθής, υπολογίζει την (b), αλλιώς υπολογίζει την (c). Δηλαδή, κάποια από τις (b) και (c) δε θα υπολογιστεί. Αντίθετα, σε μια πρόθυμη γλώσσα ο παρακάτω κώδικας
 define f(x,y) = 2*x
 set k = f(e,5)
αναμένεται να υπολογίσει την (e) και την (f) όταν υπολογίζει την (k). Όμως, οι δομές ελέγχου (control structures) που ορίζονται από το χρήστη βασίζονται στη σύνταξη της γλώσσας και στο παράδειγμα
 define g(a,b,c) = if a then b else c
 l = g(h,i,j)
και η (i) και η (j) θα υπολογιστούν σε μια οκνηρή γλώσσα. Αντίθετα, στον κώδικα
 l' = if h then i else j
θα υπολογιστεί η (i) ή η (j), αλλά ποτέ και οι δύο.

Η οκνηρή αποτίμηση επιτρέπει τον ορισμό δομών ελέγχου από το χρήστη, χωρίς να απαιτούνται ειδικές βασικές εντολές (primitives) ή τεχνικές του χρόνου μεταγλώττισης (compile-time). Αν η (i) ή η (j) έχει παρενέργειες (side effects), ή εισάγει σφάλματα χρόνου εκτέλεσης (run time errors), η συμπεριφορά της έκφρασης μπορεί να είναι πολύπλοκη. Επειδή οι περισσότερες γλώσσες προγραμματισμού είναι Τούρινγκ-πλήρεις (Turing-complete), μπορούν να γραφτούν από το χρήστη οκνηρές δομές ελέγχου σε πρόθυμες γλώσσες, σαν συναρτήσεις, αλλά τότε δεν ακολουθούν τη σύνταξη της γλώσσας για πρόθυμη αποτίμηση: συχνά οι υποεκφράσεις (όπως η (i) και η (j)) πρέπει να πακετάρονται σαν συναρτήσεις, ώστε να εκτελούνται μόνο όταν καλούνται.

Η βραχυκυκλωμένη αποτίμηση (short-circuit evaluation) των δομών ελέγχου αληθείας, όπως οι τελεστές && και || της C, συχνά καλείται επίσης "οκνηρή" ("lazy").

Άλλες χρήσεις 
Στα παραθυρικά περιβάλλοντα (windowing systems) των υπολογιστών, η εμφάνιση γραφικής πληροφορίας στην οθόνη οδηγείται από "γεγονότα έκθεσης" ("expose events") που ενεργοποιούν τον κώδικα εμφάνισης την τελευταία στιγμή. Με αυτόν τον τρόπο, αποφεύγεται ο υπολογισμός περιεχομένου που δεν χρειάζεται.

Ένα άλλο παράδειγμα οκνηρίας (laziness) στους υπολογιστές είναι δέσμευση σελίδων μνήμης με την τεχνική αντιγραφή-όταν-γράφεται (copy-on-write) ή demand paging, όπου η μνήμη δεσμεύεται μόνο όταν η τιμή που αποθηκεύεται σε αυτήν τη θέση αλλάζει.

Η οκνηρία μπορεί να είναι χρήσιμη σε περιπτώσεις υψηλής απόδοσης. Ένα παράδειγμα είναι η λειτουργία της κλήσης mmap του Unix. Η mmap φορτώνει σελίδες από το δίσκο με βάση τη ζήτηση ("demand driven"), ώστε να φορτώνονται μόνο οι σελίδες που χρειάζονται πραγματικά και να μην δεσμεύεται μνήμη που δε χρειάζεται.

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

Παρόλα αυτά, υπάρχει μια βελτιστοποίηση σε κάποιους μεταγλωττιστές που ονομάζεται ανάλυση αυστηρότητας (strictness analysis), η οποία σε κάποιες περιπτώσεις επιτρέπει στο μεταγλωττιστή να βρει αν μια τιμή θα χρειάζεται πάντα. Σε αυτές τις περιπτώσεις ο μεταγλωττιστής επιβάλλει την αυστηρή αποτίμηση.

Στη Haskell, η σημείωση πεδίων κατασκευαστών (constructor fields) σαν πρόθυμα, σημαίνει ότι οι τιμές τους θα χρειαστούν κατευθείαν. Η συνάρτηση seq μπορεί επίσης να χρησιμοποιηθεί για να ζητηθεί μια τιμή άμεσα και στη συνέχεια να δοθεί σε μια άλη έκφραση, που είναι χρήσιμη αν κάποιο πεδίο κατασκευαστή είναι οκνηρό. Όμως καμία από αυτές τις τεχνικές δεν υοποιεί αναδρομική αυστηρότητα -- για αυτήν την περίπτωση χρειάζεται η ειδική συνάρτηση deepSeq.

Επίσης, το ταίριασμα προτύπων (pattern matching) στην Haskell 98 είναι εξ ορισμού αυστηρό, εκτός και αν χρησιμοποιηθεί το σύμβολο ~ που το κάνει οκνηρό.

Δείτε επίσης 
 Συνδυαστική λογική
 Currying
 Ροή δεδομένων
 Πρόθυμη αποτίμηση
 Συναρτησιακός προγραμματισμός
 Αναγωγή γράφου
 λ-λογισμός
 Μη αυστηρή γλώσσα προγραμματισμού
 Αποτίμηση σε κανονική σειρά (Normal order evaluation)

Σημειώσεις

Αναφορές

Βιβλιογραφία 
  PhD thesis, Oxford University
 
 
 Design patterns
 John Hughes. "Why functional programming matters". The Computer Journal - Special issue on lazy functional programming. Volume 32 Issue 2, April 1989.
 Philip Wadler. "How to replace failure by a list of successes a method for exception handling, backtracking, and pattern matching in lazy functional languages". Functional Programming Languages and Computer Architecture. Lecture Notes in Computer Science, 1985, Volume 201/1985, 113-128.
 Blog posts by computer scientists
 Robert Harper. "The Point of Laziness"
 Lennart Augustsson. "More points for lazy evaluation"

Εξωτερικοί σύνδεσμοι 
 Οκνηρή αποτίμηση και αποδείξεις ορθότητας
 Lazy evaluation, Haskell Wiki 
 Functional programming in Python becomes lazy 
 Lazy programming and lazy evaluation, για την Scheme 

Προγραμματισμός