Η χρήση της λίστας ως βασικής δομής δεδομένων είναι ένα χαρακτηριστικό που μοιράζονται όλες οι διάλεκτοι της Lisp. Η Scheme κληρονομεί ένα πλούσιο σύνολο από πρωτογενείς συναρτήσεις επεξεργασίας λιστών όπως οιcons,car καιcdr, καθώς και συναρτήσεις πρώτης τάξης από τη Lisp. Η Scheme χρησιμοποιεί αυστηρά (strictly)μεταβλητές δυναμικού τύπου και υποστηρίζει συναρτήσεις πρώτης τάξης: οι συναρτήσεις μπορούν να αποδοθούν σε μεταβλητές ωΔς τιμές ή να δοθούν ως ορίσματα σε συναρτήσεις.
Η ενότητα αυτή ασχολείται κυρίως με πρωτότυπα χαρακτηριστικά της γλώσσας, συμπεριλαμβανομένων αυτών που την ξεχωρίζουν από άλλες διαλέκτους της Lisp. Εκτός και αν αναφέρεται διαφορετικά, οι περιγραφές των χαρακτηριστικών αναφέρονται στο πρότυπο R5RS.
Σε αυτήν την υποενότητα περιγράφονται τα χαρακτηριστικά που διακρίνουν τη Scheme από άλλες γλώσσες προγραμματισμού, από τα πρώτα χρόνια που κυκλοφόρησε. Τα χαρακτηριστικά αυτά αποτελούν βασική επιρροή για κάθε προϊόν της γλώσσας, και αποτελούν κοινό γνώρισμα όλων των εκδόσεων της γλώσσας προγραμματισμόυ Scheme, από το 1973 και μετά.
Η Scheme είναι μια πολύ απλή γλώσσα και είναι πολύ πιο εύκολο να υλοποιηθεί από άλλες γλώσσες παρόμοιας εκφραστικής ισχύος.[8] Αυτή η ευκολία οφείλεται στη χρήση τουλ-λογισμού ως πρωτογενούς φόρμας από την οποία προκύπτει το μεγαλύτερο μέρος της σύνταξης της γλώσσας. Για παράδειγμα, από τις 23 δομές που βασίζονται σε s-εκφράσεις που περιγράφονται στο πρότυπο R5RS Scheme, οι 11 κατηγοριοποιούνται ως παραγόμενες ή μορφές βιβλιοθήκης, οι οποίες μπορούν να γραφούν ως μακροεντολές (macros) που περιέχουν βασικότερες (κυρίως) λ-μορφές.[3][9]
- Βασικές μορφές: define, lambda, if, quote, unquote, unquote-splicing, quasiquote, define-syntax, let-syntax, letrec-syntax, syntax-rules, set!
- Μορφές βιβλιοθήκης: do, let, let*, letrec, cond, case, and, or, begin, named let, delay
Παράδειγμα: μια μακροεντολή που υλοποιεί τηlet ως έκφραση, χρησιμοποιώντας τηlambda για να εκτελεί τις δεσμεύσεις μεταλβητών (variable bindings).
(define-syntaxlet(syntax-rules()((let((varexpr)...)body...)((lambda(var...)body...)expr...))))
Κατά αυτόν τον τρόπο, χρησιμοποιώντας τηlet όπως ορίστηκε παραπάνω σε μια υλοποίηση Scheme θα έγραφε ξανά τον κώδικα "(let ((a 1)(b 2)) (+ b a))" σαν "((lambda (a b) (+ b a)) 1 2)", που μειώνει τις εργασίες της υλοποίησης, χρησιμοποιώντας στιγμιότυπα της κάθε διαδικασίας.
Το 1998 ο Sussman και ο Steele τόνισαν ότι ο μινιμαλισμός της Scheme δεν ήταν σχεδιαστικός στόχος που τον επιδίωξαν συνειδητά, αλλά προέκυψε από τη διαδικασία σχεδίασης.[7]
Όπως οι περισσότερες σύγχρονες γλώσσες προγραμματισμού, και σε αντίθεση με τηEmacs Lisp και με προηγούμενες διαλέκτους της Lisp όπως η Maclisp, η Scheme έχει λεκτική εμβέλεια: όλες οι δυνατές δεσμεύσεις μεταβλητών σε μια μονάδα προγράμματος μπορούν να αναλυθούν διαβάζοντας το κείμενο της μονάδας αυτής χωρίς να πρέπει να ληφθούν υπόψη οι συνθήκες υπό τις οποίες μπορεί να κληθεί.
Αυτό έρχεται σε αντίθεση με τη δυναμική εμβέλεια, η οποία ήταν χαρακτηριστικό των αρχικών διαλέκτων της Lisp λόγω του κόστους της επεξεργασίας που χρειαζόταν για τις πρωτόγονες μεθόδους αντικατάστασης κειμένου που χρησιμοποιούνταν για να υλοποιηθούν οι αλγόριθμοι λεκτικής εμβέλειας στους μεταγλωττιστές και στους διερμηνείς της εποχής. Σε αυτές τις διαλέκτους της Lisp, μια αναφορά σε μια ελεύθερη μεταβλητή μέσα σε μια διαδικασία μπορούσε να αναφέρεται σε πολύ διαφορετικές εξωτερικές δεσμεύσεις ανάλογα με την κλήση και το περιβάλλον της κάθε φορά.
Η έρευνα του Sussman πάνω στηνALGOL τον ενέπνευσε να ενσωματώσει στη νέα έκδοση του της Lisp, στις αρχές της δεκαετίας του 1970, ένα ασυνήθιστο μοντέλο εμβέλειας. Θέση του ήταν ότι οι μηχανισμοί λεκτικής εμβέλειας όπως αυτοί της ALGOL, θα βοηθούσαν στον αρχικό τους στόχο της υλοποίησης του μοντέλου Actor στη Lisp.[7]
Οι βασικές ιδέες για το πώς ενσωματώνεται η λεκτική εμβέλεια σε μια διάλεκτο της Lisp έγιναν γνωστές από το Sussman και το Steele στο Lambda Paper του 1975 με τον τίτλο "Scheme: An Interpreter for Extended Lambda Calculus",[10] στο οποίο υιοθέτησαν την έννοια τουλεκτικού κλείσιμου, το οποίο είχε περιγραφεί σε ένα "AI Memo" το 1970 από τον Joel Moses, ο οποίος με τη σειρά του, υποστήριξε ότι η ιδέα προέρχεται από τονPeter J. Landin.[11]
Ο μαθηματικός συμβολισμός τουAlonzo Church που ονομάζεται λ-λογισμός, επηρέασε τη Lisp όσον αφορά τη χρήση της λέξης-κλειδί "lambda" για την εισαγωγή μιας διαδικασίας (procedure), ενώ σε αυτόν οφείλεται και η ανάπτυξη πολλών τεχνικώνσυναρτησιακού προγραμματισμού, όπως η χρήσησυναρτήσεων υψηλής τάξης στη Lisp. Οι πρώιμες διάλεκτοι της Lisp δεν ήταν κατάλληλες για να εκφράσουν το λ-λογισμό λόγω του τρόπου με τον οποίο χειρίζονταν τις ελεύθερες μεταβλητές.[7]
Η εισαγωγή της λεκτικής εμβέλειας έλυσε το πρόβλημα, εξισώνοντας κάποιες μορφές του λ-συμβολισμού και του πώς αυτός εκφράζεται πρακτικά σε μια πραγματική γλώσσα προγραμματισμού. οι Sussman και Steele έδειξαν ότι η νέα γλώσσα μπορούσε να χρησιμοποιηθεί για να προκύψουν με κομψό τρόπο η προστακτική και ηδηλωτική σημασιολογία όλων των άλλων γλωσσών προγραμματισμού, όπως της ALGOL ή τηςFortran, και η δυναμική εμβέλεια των άλλων διαλέκτων της Lisp, με τη χρήση λ-εκφράσεων, όχι ως απλά στιγμιότυπα διαδικασιών αλλά ως δομές ελέγχου και τελεστές τροποποίησης του περιβάλλοντος.[12] Εισήγαγαν τοστυλ περάσματος συνεχειών μαζί με την πρώτη περιγραφή της Scheme στο πρώτο από τα Lambda Papers, και σε επόμενες δημοσιεύσεις έδειξαν το πόσο ισχυρή είναι αυτή χρήση στην πράξη του λ-λογισμού.
Η Scheme κληρονομεί τη δομή ενοτήτων της (block structure) από προγενέστερες γλώσσες δομημένες με ενότητες και κυρίως από τηνALGOL. Στη Scheme οι ενότητες υλοποιούνται από τρειςδομές δέσμευσης (binding constructs):let,let* καιletrec. Για παράδειγμα, η ακόλουθη δομή δημιουργεί μια ενότητα στην οποία ένα σύμβολο με το όνομαvar δεσμεύεται στον αριθμό 10:
(definevar"goose");; Κάθε αναφορά στη var εδώ δεσμεύεται στο "goose"(let((var10));; οι εντολές βρίσκονται εδώ. Κάθε αναφορά στη var δεσμεύεται στην τιμή 10.);; Κάθε αναφορά στη var εδώ δεσμεύεται στο "goose"
Οι ενότητες μπορούν να είναι εμφωλευμένες (nested) με σκοπό τη δημιουργία πολύπλοκων δομών ενοτήτων σύμφωνα με τις ανάγκες του προγραμματιστή. Η δόμηση με ενότητες για τη δημιουργία τοπικών δεσμεύσεων περιορίζει τον κίνδυνο σύγκρουσης χώρων ονομάτων (namespace collision) που μπορεί να εμφανιστεί σε άλλες περιπτώσεις.
Μια παραλλαγή τηςlet, ηlet*, επιτρέπει σε δεσμεύσεις να αναφέρονται σε μεταβλητές που ορίστηκαν νωρίτερα, όπως στον εξής κώδικα:
(let*((var110)(var2(+var112)));; Αλλά ο ορισμός της var1 δε θα μπορούσε να αναφέρεται στη var2)
Η άλλη παραλλαγή, ηletrec, είναι σχεδιασμένη ώστε να επιτρέπει σε αμοιβαία αναδρομικές διαδικασίες να δεσμεύονται η μια στην άλλη.
;; Στηλοθέτηση των ακολουθιών αντρών-γυναικών του Hofstadter(letrec((female(lambda(n)(if(=n0)1(-n(male(female(-n1)))))))(male(lambda(n)(if(=n0)0(-n(female(male(-n1))))))))(display"i male(i) female(i)")(newline)(do((i0(+i1)))((>i8)#f)(displayi)(display" ")(display(malei))(display" ")(display(femalei))(newline)))
Όλες οι διαδικασίες που δεσμεύονται μέσα σε μιαletrec μπορούν να αναφέρονται η μια στην άλλη με το όνομά τους, καθώς και σε τιμές μεταβλητών που ορίστηκαν νωρίτερα στην ίδιαletrec, αλλά δε μπορούν να αναφέρονται σετιμές που ορίζονται αργότερα στην ίδιαletrec.
Μια παραλλαγή τηςlet, η μορφή "ονομαστικό let", έχει ένα αναγνωριστικό μετά τη λέξη-κλειδίlet. Αυτό δεσμεύει τις μεταβλητές του let στο όρισμα μιας διαδικασίας της οποίας το όνομα είναι το δοθέν αναγνωριστικό και το σώμα της είναι το σώμα της μορφής let. Το σώμα μπορεί να επαναληφθεί όσο είναι επιθυμητό με την κλήση της διαδικασίας. Το ονομαστικό let χρησιμοποιείται συχνά για την υλοποίηση της επανάληψης (iteration).
Παράδειγμα: ένας απλός μετρητής
(letloop((n1))(if(<=n10)(begin(displayn)(newline)(loop(+n1)))))
Όπως κάθε διαδικασία στη Scheme, η διαδικασία που δημιουργείται από το ονομαστικό let είναι αντικείμενο πρώτης τάξης.
Η Scheme έχει μια δομή επανάληψης, τηdo, αλλά στη Scheme συνηθίζεται να χρησιμοποιείται ηαναδρομή ουράς για την έκφραση της επανάληψης. Οι υλοποιήσεις της Scheme που συμφωνούν με το πρότυπο απαιτείται να βελτιστοποιούν τις κλήσεις ουράς (tail calls), ώστε να μπορούν να υποστηρίξουν άπειρες ενεργές κλήσεις ουράς (R5RS sec. 3.5)[3]—μια ιδιότητα που το πρότυπο περιγράφει σανσωστή αναδρομή ουράς (proper tail recursion)— με αποτέλεσμα ο προγραμματιστής να μπορεί να γράφει επαναληπτικούς αλγορίθμους με τη χρήση αναδρομής, κάτι που είναι ευκολότερο. Οι διαδικασίες που έχουν αναδρομή ουράς και η μορφήονομαστικόlet υποστηρίζουν την επανάληψη μέσω αναδρομής ουράς.
;; Στηλοθέτηση των τετραγώνων από το 0 έως το 9:;; Το loop είναι απλά ένα σύμβολο που χρησιμοποιήθηκε σαν ετικέτα.;; Οποιοδήποτε άλλο σύμβολο είναι επίσης κατάλληλο.(letloop((i0))(if(not(=i10))(begin(displayi)(display" squared = ")(display(*ii))(newline)(loop(+i1)))))
Οι συνέχειες στη Scheme είναι αντικείμενα πρώτης τάξης. Η Scheme παρέχει τη διαδικασίαcall-with-current-continuation (γνωστή και σανcall/cc) για να δεσμεύσει την τρέχουσα συνέχεια, πακετάροντάς τη σαν διαδικασία διαφυγής (escape procedure) που δεσμεύεται σε μια παράμετρο μιας διαδικασίας που όρισε ο προγραμματιστής. (R5RS sec. 6.4)[3] Οι συνέχειες πρώτης τάξης επιτρέπουν στον προγραμματιστή να δημιουργεί μη-τοπικές δομές ελέγχου όπως οι επαναλήπτες (iterators), οισυρρουτίνες (coroutines), και η οπισθοδρόμηση (backtracking).
Στο επόμενο παράδειγμα, που είναι ένα κλασικό προγραμματιστικό πρόβλημα, φαίνεται πώς χειρίζεται η Scheme τις συνέχειες ως αντικείμενα πρώτης τάξης, δεσμεύοντάς τις σε μεταβλητές και περνώντας τις ως παραμέτρους σε διαδικασίες.
(let*((yin((lambda(cc)(display"@")cc)(call-with-current-continuation(lambda(c)c))))(yang((lambda(cc)(display"*")cc)(call-with-current-continuation(lambda(c)c)))))(yinyang))
Όταν εκτελεστεί, ο κώδικας εμφανίζει την εξής ακολουθία από μετρητές: "@*@**@***@****@*****@******@*******@********..."
Η έννοια της συνέχειας πρώτης τάξης επεκτάθηκε στο πρότυπο R5RS. Ένας περιορισμός τηςcall/cc είναι ότι παράγει συνέχειες που μπορούν να χειριστούν μόνο μια παράμετρο. Το R5RS αναίρεσε αυτόν τον περιορισμό, ορίζοντας δύο νέες διαδικασίες: ηvalues ορίζεται σαν τη διαδικασία που δίνει όλες της τις παραμέτρους στη συνέχειά της, και ηcall-with-values παράγει μια συνέχεια που απλά περνάει τα ορίσματά της σε μια διαδικασία. (R5RS sec. 6.4)[3]
Αποτέλεσμα των παραπάνω είναι να μπορούν να γραφούν διαδικασίες που επιστρέφουν παραπάνω από μια τιμές. Ένα απλό παράδειγμα της χρησιμότητας αυτής της ισχυρής δομής είναι:
;; Μια διαδικασία που επιστρέφει το όνομα χρήστη και τον κωδικό(define(credentials)(values"myuser""mypassword"));; Μια διαδικασία που εκτελεί ταυτοποίηση ενός ονόματος χρήστη και ενός κωδικού(define(loginusernamepassword);; κώδικας ταυτοποίησης και εισόδου του χρήστη στο σύστημα);; Στέλνει τα δεδομένα στη διαδικασία ταυτοποίησης.(call-with-valuescredentialslogin)
Με τη χρήση της σύνταξηςreceive που εμφανίστηκε στο SRFI 8,[13] η παραπάνωcall-with-values μπορεί να γραφεί ως εξής:
(receive(usernamepassword)(credentials)(loginusernamepassword))
Η μορφήreceive είναι ένα παράδειγμα δομής ενότητας όπως η μορφήlet, και μπορεί να οδηγήσει σε καθαρότερο κώδικα.
Σε αντίθεση με τηνCommon Lisp, όλα τα δεδομένα και οι διαδικασίες στη Scheme μοιράζονται έναν κοινό χώρο ονομάτων, ενώ στην Common Lisp οι συναρτήσεις και τα δεδομένα έχουν ξεχωριστούς χώρους ονομάτων, με αποτέλεσμα να είναι δυνατό μια συνάρτηση και μια μεταβλητή να έχουν το ίδιο όνομα, ενώ χρειάζεται ειδικός συμβολισμός για την αναφορά σε μια συνάρτηση ως τιμή. Αυτό είναι γνωστό και ως διαχωρισμός "Lisp-1/Lisp-2", που αναφέρεται στον ενιαίο χώρο ονομάτων της Scheme και στους ξεχωριστούς χώρους ονομάτων της Common Lisp.[14]
Στη Scheme, οι ίδιες βασικές εντολές που χρησιμοποιούνται για το χειρισμό και τη δέσμευση δεδομένων μπορούν να χρησιμοποιηθούν για τη δέσμευση διαδικασιών. Δεν υπάρχει αντίστοιχη των εντολώνdefun,setf και#' της Common Lisp.
;; Μεταβλητή που δεσμεύεται σε αριθμό:(definef10)f===>10;; Τροποποίηση (mutation, αλλαγή της δεσμευμένης τιμής)(set!f(+ff6))===>26;; Ανάθεση μιας διαδικασίας στην ίδια μεταβλητή:(set!f(lambda(n)(+n12)))(f6)===>18;; Ανάθεση του αποτελέσματος μιας έκφρασης στην ίδια μεταβλητή:(set!f(f1))f===>13;; συναρτησιακός προγραμματισμός:(apply+'(123456))===>21(set!f(lambda(n)(+n100)))(mapf'(123))===>(101102103)
Αυτή η υποενότητα περιγράφει σχεδιαστικές αποφάσεις που έχουν παρθεί από τη δημιουργία της γλώσσας και έχουν δώσει στη Scheme έναν ιδιαίτερο χαρακτήρα, αλλά δεν αποτελούν απευθείας αποτελέσματα του αρχικού σχεδιασμού.
Η Scheme ορίζει ένα σχετικά πλήρες σύνολο από αριθμητικούς τύπους δεδομένων, όπως οι τύποι τωνμιγαδικών και τωνρητών αριθμών, το οποίο είναι γνωστό στη Scheme σαν "ο αριθμητικός πύργος" ("numerical tower"). (R5RS sec. 6.2[3]) Το πρότυπο θεωρεί τους τύπους αφηρημένους και δεν υποχρεώνει αυτόν που υλοποιεί τη γλώσσα σε κάποια συγκεκριμένη εσωτερική αναπαράσταση.
Οι αριθμοί μπορεί να έχουν την ιδιότητα της ακρίβειας. Ένας ακριβής αριθμός μπορεί να παραχθεί μόνο από μια ακολουθία λειτουργιών ακρίβειας πάνω σε ακριβείς αριθμούς - δηλαδή η μη-ακρίβεια είναι "μεταδοτική". Το πρότυπο ορίζει ότι οποιεσδήποτε δύο υλοποιήσεις πρέπει να παράγουν ισοδύναμα αποτελέσματα για όλες τις λειτουργίες που έχουν ως αποτέλεσμα ακριβείς αριθμούς.
Το πρότυπο R5RS ορίζει τις διαδικασίεςexact->inexact καιinexact->exact που μπορούν να χρησιμοποιηθούν για να αλλάξουν την ακρίβεια ενός αριθμού. Το πρότυπο R6RS τις παραλείπει από το βασικό κείμενο αλλά τις ορίζει ως διαδικασίες συμβατότητας με το R5RS στη βασική βιβλιοθήκη (rnrs r5rs (6)).
Σύμφωνα με το πρότυπο R5RS, οι υλοποιήσεις της Scheme δε χρειάζεται να υλοποιήσουν ολόκληρο τον αριθμητικό πύργο αλλά πρέπει να υλοποιήσουν "ένα σωστό υποσύνολο που να συμφωνεί τόσο με τους σκοπούς της υλοποίησης όσο και με το πνεύμα της γλώσσας Scheme." (R5RS sec. 6.2.3).[3] Το νέο πρότυπο R6RS απαιτεί την υλοποίηση ολόκληρου του αριθμητικού πύργου και "τα αντικείμενα ακεραίων ακρίβειας και τα αντικείμενα ρητών αριθμών ακρίβειας πρακτικά άπειρου μεγέθους και ακρίβειας, και να υλοποιηθούν συγκεκριμένες διαδικασίες...που να επιστρέφουν πάντα ακριβή αποτελέσματα όταν τους δίνονται ακριβείς παράμετροι" (R6RS sec. 3.4, sec. 11.7.1).[4]
Παράδειγμα 1: αριθμητική ακρίβειας σε μια υλοποίηση που υποστηρίζει ρητούς μιγαδικούς αριθμούς ακρίβειας.
;; Άθροισμα τριών ρητών αριθμών και δύο ρητών μιγαδικών αριθμών(definex(+1/31/4-1/5-1/3i405/50+2/3i))x===>509/60+1/3i;; Έλεγχος ακρίβειας.(exact?x)===>#t
Παράδειγμα 2: Η ίδια αριθμητική σε μια υλοποίηση που δεν υποστηρίζει ρητούς αριθμούς ακρίβειας, ούτε μιγαδικούς αριθμούς, αλλά δέχεται πραγματικούς αριθμούς σε συμβολισμό ρητών.
;; Άθροισμα τεσσάρων ρητών πραγματικών αριθμών(definexr(+1/31/4-1/5405/50));; Άθροισμα δύο ρητών πραγματικών αριθμών(definexi(+-1/32/3))xr===>8.48333333333333xi===>0.333333333333333;; Έλεγχος ακρίβειας(exact?xr)===>#f(exact?xi)===>#f
Και οι δύο υλοποιήσεις συμφωνούν με το πρότυπο R5RS αλλά η δεύτερη δε συμφωνεί με το R6RS γιατί δεν υλοποιεί τον πλήρη αριθμητικό πύργο.
- Δείτε επίσης:Οκνηρή αποτίμηση
Η Scheme υποστηρίζει την καθυστερημένη αποτίμηση (delayed evaluation) μέσω της μορφήςdelay και της διαδικασίαςforce.
(definea10)(defineeval-aplus2(delay(+a2)))(set!a20)(forceeval-aplus2)===>22(defineeval-aplus50(delay(+a50)))(let((a8))(forceeval-aplus50))===>70(set!a100)(forceeval-aplus2)===>22
Το λεκτικό περιβάλλον του αρχικού ορισμού της υπόσχεσης (promise) διατηρείται, και η τιμή του επίσης διατηρείται μετά από την πρώτη χρήση τηςforce. Η υπόσχεση αποτιμάται μόνο μια φορά.
Αυτές οι πρωτογενείς εντολές, που παράγουν ή χειρίζονται τιμές γνωστές ωςυποσχέσεις (promises), μπορούν να χρησιμοποιηθούν για να υλοποιηθούν προχωρημένες δομέςοκνηρής αποτίμησης, όπως οι ροές (streams).[15]
Στο πρότυπο R6RS, δεν είναι πια πρωτογενείς εντολές αλλά παρέχονται ως μέρος της βιβλιοθήκης συμβατότητας R5RS (rnrs r5rs (6)).
Στο R5RS, δίνεται μια προτεινόμενη υλοποίηση τηςdelay και τηςforce, που υλοποιεί την υπόσχεση ως διαδικασία χωρίς ορίσματα (ένα thunk) και χρησιμοποιεί την τεχνική της απομνημόνευσης (memoization) για να είναι βέβεαιο ότι θα αποτιμηθεί μια μόνο φορά, ανεξάρτητα από τον αριθμό των φορών που θα κληθεί ηforce. (R5RS sec. 6.4)[3]
Το SRFI 41 επιτρέπει την έκφραση πεπερασμένων και άπειρων ακολουθιών με σημαντική οικονομία. Για παράδειγμα, ακολουθεί ένας ορισμός τηςακολουθίας Φιμπονάτσι χρησιμοποιώντας τις συναρτήσεις που ορίζονται στο SRFI 41:[15]
;; Ορίζει την ακολουθία Φιμπονάτσι:(definefibs(stream-cons0(stream-cons1(stream-map+fibs(stream-cdrfibs)))));; Υπολογίζει τον εκατοστό αριθμό της ακολουθίας:(stream-reffibs99)===>218922995834555169026
Οι περισσότερες διάλεκτοι της Lisp ορίζουν μια σειρά αποτίμησης για τις παραμέτρους μιας διαδικασίας, όχι όμως η Scheme. Η σειρά αποτίμησης-συμπεριλαμβανομένης της σειράς με την οποία αποτιμάται η έκφραση στη θέση του τελεστή-μπορεί να επιλεγεί από την υλοποίηση ανάλογα με την κάθε κλήση και ο μόνος περιορισμός είναι ότι "το αποτέλεσμα κάθε ταυτόχρονης αποτίμησης του τελεστή και των εκφράσεων-τελεστέων πρέπει να συμφωνεί με κάποια ακολουθιακή σειρά αποτίμησης." (R5RS sec. 4.1.3)[3]
(let((ev(lambda(n)(display"Evaluating ")(display(if(procedure?n)"procedure"n))(newline)n)))((ev+)(ev1)(ev2)))===>3
Η ev είναι μια διαδικασία που περιγράφει το όρισμα που της περνιέται, και στη συνέχεια επιστρέφει την τιμή του ορίσματος αυτού. Σε αντίθεση με άλλες διαλέκτους της Lisp, η εμφάνιση μιας έκφρασης στη θέση τελεστή (το πρώτο αντικείμενο) μιας έκφρασης της Scheme είναι σωστή, αρκεί το αποτέλεσμα της έκφρασης αυτής να είναι διαδικασία.
Καλώντας τη διαδικασία "+" για την πρόσθεση 1 και 2, οι εκφράσεις (ev +), (ev 1) και (ev 2) μπορούν να αποτιμηθούν με οποιαδήποτε σειρά, αρκεί το αποτέλεσμά τους να μην είναι αυτό που θα προέκυπτε αν αποτιμούνταν παράλληλα, Επομένως, οι ακόλουθες τρεις γραμμές μπορούν να εμφανιστούν με οποιαδήποτε σειρά στην πρότυπη Scheme όταν εκτελεστεί ο παραπάνω κώδικας, αν και το κείμενο μιας γραμμής δεν παρεμβάλλεται με κάποιας άλλης, αφού αυτό θα παραβίαζε τον περιορισμό της ακολουθιακής αποτίμησης.
- Evaluating 1
- Evaluating 2
- Evaluating procedure
Το πρότυπο R5RS εισήγαγε ένα ισχυρό σύστημα για hygienic μακροεντολές που επιτρέπει στον προγραμματιστή να προσθέσει νέες συντακτικές δομές στη γλώσσα χρησιμοποιώντας μια απλή γλώσσα ταιριάσματος προτύπων (pattern matching). (R5RS sec 4.3)[3] Το σύστημα αυτό είχε τοποθετηθεί παλαιότερα σε ένα παράρτημα του προτύπου R4RS, ως σύστημα "υψηλού επιπέδου" μαζί με ένα "χαμηλού επιπέδου" σύστημα μακροεντολών, με τα δύο αυτά συστήματα να θεωρούνται επεκτάσεις της Scheme και όχι απαραίτητο μέρος της γλώσσας.[16]
Οι υλοποιήσεις του συστήματος hygienic μακροεντολών, που αποκαλούνται καιsyntax-rules, πρέπει να συμμορφώνονται με τη λεκτική εμβέλεια της υπόλοιπης γλώσσας. Αυτό πραγματοποιείται με ειδικούς κανόνες ονομασίας και εμβέλειας για την επέκταση των μακροεντολών (macro expansion) και έτσι αποφεύγονται συχνά προγραμματιστικά λάθη που μπορούν να συμβούν σε συστήματα μακροεντολών άλλων γλωσσών προγραμματισμού. Το R6RS ορίζει ένα πιο πολύπλοκο σύστημα μετασχηματισμού, τοsyntax-case, που έχει υπάρξει επέκταση της γλώσσας R5RS Scheme για κάποιο χρονικό διάστημα.
;; Ορίζει μια μακροεντολή που υλοποιεί μια παραλλαγή του "if" με πολλαπλές εκφράσεις;; στον κλάδο "αληθές" και χωρίς κλάδο "ψευδές".(define-syntaxwhen(syntax-rules()((whenpredexpexps...)(ifpred(beginexpexps...)))))
Με αυτόν τον τρόπο μπορεί να επεκταθεί εύκολα η σύνταξη της Scheme.
Οι κλήσεις των μακροεντολών και των διαδικασιών μοιάζουν πολύ-και οι δύο είναι s-εκφράσεις—αλλά ο χειρισμός τους γίνεται διαφορετικά. Όταν ο μεταγλωττιστής συναντά μια s-έκφραση στο πρόγραμμα αρχικά ελέγχει να δει αν το σύμβολο έχει οριστεί ως συντακτική λέξη-κλειδί μέσα στην τρέχουσα λεκτική εμβέλεια. Αν ναι, τότε προσπαθεί να επεκτείνει τη μακροεντολή θεωρώντας ότι τα αντικείμενα στην ουρά της s-έκφρασης είναι ορίσματα χωρίς μεταγλωττισμένο κώδικα που θα τα αποτιμήσει, και αυτό επαναλαμβάνεται αναδρομικά μέχρι να μην απομένουν άλλες κλήσεις μακροεντολών. Αν δεν είναι συντακτική λέξη-κλειδί, ο μεταγλωττιστής μεταγλωττίζει κώδικα για να αποτιμήσει τα ορίσματα στην ουρά της s-έκφρασης και για να αποτιμήσει στη συνέχεια τη μεταβλητή που αναπαρίσταται στο σύμβολο στην κεφαλή της s-έκφρασης και να την καλέσει ως διαδικασία με τις αποτιμημένες εκφράσεις της ουράς να περνιούνται ως πραγματικά ορίσματα σε αυτήν.
Οι περισσότερες υλοποιήσεις της Scheme παρέχουν επίσης επιπλέον συστήματα μακροεντολών. Κάποια από τα πιο δημοφιλή είναι τα συντακτικά κλεισίματα (syntactic closures), οι μακροεντολές ρητής μετονομασίας (explicit renaming macros) και ηdefine-macro, ένα σύστημα μακροεντολών που δεν είναι hygienic και μοιάζει με το σύστημαdefmacro τηςCommon Lisp.
Πριν από το R5RS, η Scheme δεν είχε κάποιο πρότυπο ισοδύναμο της διαδικασίαςeval που υπάρχει σε άλλες διαλέκτους της Lisp, αν και το πρώτο Lambda Paper είχε περιγράψει τηνevaluate ως "παρόμοια με τη συνάρτηση EVAL της LISP"[10] και η πρώτη Αναθεωρημένη Αναφορά του 1978 την αντικατέστησε με τηνenclose, η οποία έπαιρνε δύο ορίσματα. Η δεύτερη, τρίτη και τέταρτη αναφορά παρέλειψαν κάθε εντολή ισοδύναμη με τηνeval.
Ο λόγος αυτής της σύγχυσης είναι ότι στη Scheme, με τη λεκτική της εμβέλεια, το αποτέλεσμα της αποτίμησης μιας έκφρασης εξαρτάται από το πού αποτιμάται αυτή. Για παράδειγμα, δεν είναι ξεκάθαρο αν το αποτέλεσμα της αποτίμησης της ακόλουθης έκφρασης θα έπρεπε να είναι 5 ή 6:[17]
(let((name'+))(let((+*))(evaluate(listname23))))
Αν αποτιμηθεί στο εξωτερικό περιβάλλον, όπου ορίζεται ηname, το αποτέλεσμα είναι το άθροισμα των τελεστέων. Αν αποτιμηθεί στο εσωτερικό περιβάλλον, όπου το σύμβολο "+" έχει δεσμευτεί στην τιμή της διαδικασίας "*", το αποτέλεσμα είναι το γινόμενο των δύο τελεστέων.
Το R5RS ξεκαθαρίζει αυτήν την κατάσταση ορίζοντας τρεις διαδικασίες που επιστρέφουν περιβάλλοντα, και παρέχοντας μια διαδικασίαeval που παίρνει μια s-έκφραση και ένα περιβάλλον και αποτιμά την έκφραση στο δοθέν περιβάλλον. (R5RS sec. 6.5)[3] Το R6RS επεκτείνει αυτήν τη λειτουργικότητα παρέχοντας μια διαδικασία που ονομάζεταιenvironment, με την οποία ο προγραμματιστής μπορεί να ορίσει ακριβώς ποια αντικείμενα θα εισαχθούν σε ένα περιβάλλον αποτίμησης.
Στις περισσότερες διαλέκτους της Lisp, συμπεριλαμβανομένης της Common Lisp, ισχύει η σύμβαση ότι η τιμήNIL αποτιμάται στην τιμή ψευδές σε μια έκφραση αληθείας. Στη Scheme, από το πρότυπο IEEE του 1991,[2] όλες οι τιμές εκτός της #f, συμπεριλαμβανομένης της '() που είναι η ισοδύναμη τηςNIL στη Scheme, αποτιμώνται στην τιμή αληθές σε μια έκφραση αληθείας. (R5RS sec. 6.3.1)[3]
Η Scheme έχει τη σταθερά#t για την αναπαράσταση της τιμής αληθές, όπου οι περισσότερες διάλεκτοι της Lisp έχουν τηνT.
Στη Scheme οι πρωτογενείς τύποι δεδομένων είναι ανεξάρτητοι. Μόνο μια από τις εξής ιδιότητες μπορεί να είναι αληθής για οποιοδήποτε αντικείμενο της Scheme:boolean?,pair?,symbol?,number?,char?,string?,vector?,port?,procedure?. (R5RS sec 3.2)[3]
Μέσα στον αριθμητικό τύπο δεδομένων, αντίθετα, οι αριθμητικές τιμές αλληλοεπικαλύπτονται. Για παράδειγμα, μια ακέραια τιμή ικανοποιεί ταυτόχρονα όλες τις ιδιότητεςinteger?,rational?,real?,complex? καιnumber?. (R5RS sec 6.2)[3]
Η Scheme έχει τρεις διαφορετικούς τύπους ισοδυναμίας μεταξύ τυχαίων αντικειμένων, με τρεις διαφορετικέςπροτάσεις ισοδυναμίας (equivalence predicates), που είναι σχεσιακοί τελεστές που ελέγχουν την ισότητα και είναι οeq?, οeqv? και οequal?:
- ο
eq? αποτιμάται σε#f εκτός και αν οι παράμετροί του αναπαριστούν το ίδιο αντικείμενο δεδομένων στη μνήμη, - ο
eqv? είναι γενικά παρόμοιος με τονeq? αλλά χειρίζεται τα πρωτογενή αντικείμενα (π.χ. χαρακτήρες και αριθμούς) με ειδικό τρόπο με αποτέλεσμα ηeqv? να είναι αληθής για αριθμούς που αναπαριστούν την ίδια τιμή, ακόμα και αν δεν αναφέρονται στο ίδιο αντικείμενο, - ο
equal? συγκρίνει δομές δεδομένων όπως οι λίστες, τα διανύσματα και οι συμβολοσειρές για να βρει αν έχουν σύμφωνη (congruent) δομή και περιεχόμενα ίσα σύμφωνα με τονeqv?.(R5RS sec. 6.1)[3]
Στη Scheme υπάρχουν επίσης λειτουργίες ισοδυναμίας εξαρτώμενης από τους τύπους: οstring=? και οstring-ci=? συγκρίνουν δύο συμβολοσειρές (στην τελευταία περίπτωση χωρίς να λαμβάνεται υπόψη συμφωνία πεζών-κεφαλαίων), οchar=? και οchar-ci=? συγκρίνουν χαρακτήρες και ο= συγκρίνει αριθμούς.[3]
- Δείτε επίσης:Σχόλιο (προγραμματισμός)
Μέχρι το πρότυπο R5RS, τα πρότυπα σχόλια στη Scheme εισάγονταν με ελληνικό ερωτηματικό (";"), το οποίο έκανε την υπόλοιπη γραμμή που ακολουθούσε αόρατη στη Scheme. Διάφορες υλοποιήσεις έχουν υποστηρίξει εναλλακτικές συμβάσεις που επιτρέπουν στα σχόλια να καταλαμβάνουν πάνω από μια γραμμές και το πρότυπο R6RS επιτρέπει δύο από αυτές: μια ολόκληρη s-έκφραση μπορεί να μετατραπεί σε σχόλιο αν στην αρχή της προστεθεί το#; (αυτό εισήχθηκε στο SRFI 62[18]) και ένα σχόλιο πολλών γραμμών ή "σχόλιο-ενότητα" ("block comment") μπορεί να δημιουργηθεί αν περικλειστεί το κείμενο ανάμεσα στα#| και|#.
Η είσοδος και η έξοδος της Scheme βασίζονται στον τύπο δεδομένωνθύρα (port). (R5RS sec 6.6)[3] Το R5RS ορίζει δυο θύρες, η πρόσβαση στις οποίες γίνεται με τις διαδικασίεςcurrent-input-port καιcurrent-output-port, οι οποίες αντιστοιχούν στις έννοιες "προκαθορισμένη έξοδος" ("standard input") και "προκαθορισμένη έξοδος" ("standard output") του Unix. Οι περισσότερες υλοποιήσεις παρέχουν επίσης μιαcurrent-error-port. Η ανακατεύθυνση της προκαθορισμένης εισόδου και της προκαθορισμένης εξόδου υποστηρίζεται από το πρότυπο, με πρότυπες διαδικασίες όπως ηwith-input-from-file και ηwith-output-to-file. Οι πιο πολλές υλοποιήσεις παρέχουν θύρες συμβολοσειρών με παρόμοιες δυνατότητες ανακατεύθυνσης, επιτρέποντας σε πολλές κανονικές λειτουργίες εισόδου-εξόδου να εκτελούνται μέσα σε συμβολοσειρές αντί σε αρχεία, με τη χρήση διαδικασιών που ορίζονται στο SRFI 6.[19] Το πρότυπο R6RS ορίζει πιο πολύπλοκες και ισχυρές διαδικασίες θυρών και πολλούς νέους τύπους θυρών.
Τα παρακάτω παραδείγματα είναι γραμμένα σε αυστηρή R5RS Scheme.
Παράδειγμα 1: Με την έξοδο να είναι η (current-output-port):
(let((hello0(lambda()(display"Hello world")(newline))))(hello0))
Παράδειγμα 2: Όπως το 1, αλλά χρησιμοποιεί την προαιρετική παράμετρο θύρας για τη διαδικασία εξόδου
(let((hello1(lambda(p)(display"Hello world"p)(newlinep))))(hello1(current-output-port)))
Παράδειγμα 3: Όπως το 1, αλλά η έξοδος ανακατευθύνεται σε ένα νέο αρχείο
;; NB: η with-output-to-file είναι μια προαιρετική διαδικασία στο R5RS(let((hello0(lambda()(display"Hello world")(newline))))(with-output-to-file"helloworldoutputfile"hello0))
Παράδειγμα 4: Όπως το 2, αλλά με ρητό άνοιγμα αρχείου και κλείσιμο θύρας για να αποσταλεί η έξοδος στο αρχείο
(let((hello1(lambda(p)(display"Hello world"p)(newlinep)))(output-port(open-output-file"helloworldoutputfile")))(hello1output-port)(close-output-portoutput-port))
Παράδειγμα 5: Όπως το 2, αλλά με τη χρήση call-with-output-file για την αποστολή της εξόδου σε ένα αρχείο.
(let((hello1(lambda(p)(display"Hello world"p)(newlinep))))(call-with-output-file"helloworldoutputfile"hello1))
Για την είσοδο παρέχονται παρόμοιες διαδικασίες. Η R5RS Scheme παρέχει τιςinput-port? καιoutput-port?. Για είσοδο και έξοδο χαρακτήρων, παρέχονται οιwrite-char,read-char,peek-char καιchar-ready?. Για τη γραφή και την ανάγνωση εκφράσεων της Scheme, η γλώσσα παρέχει τιςread καιwrite. Σε μια λειτουργία ανάγνωσης, το αποτέλεσμα που επιστρέφεται είναι το αντικείμενο τέλος-αρχείου (end-of-file) αν η θύρα εισόδου έχει φτάσει στο τέλος του αρχείου, και αυτό μπορεί να ελεγχθεί με τη χρήση τουeof-object?.
Εκτός από το πρότυπο, το SRFI 28 ορίζει μια βασική διαδικασία μορφοποίησης που μοιάζει με τη συνάρτησηformat της Common Lisp, με την οποία έχει και το ίδιο όνομα.[20]
Οι διαδικασίες στη Scheme δεσμεύονται σε μεταβλητές. Το πρότυπο της γλώσσας στο R5RS επίσημα δηλώνει ότι τα προγράμματα μπορούν να αλλάξουν τις δεσμεύσεις των μεταβλητών των ενσωματωμένων διαδικασιών, πρακτικά ορίζοντάς τις πάλι (R5RS, "Language changes"[3]). Για παράδειγμα, κάποιος μπορεί να επεκτείνει τη+ ώστε να δέχεται συμβολοσειρές εκτός από αριθμούς, ορίζοντάς την πάλι:
(set!+(let((original++))(lambdaargs(if(and(not(null?args))(string?(carargs)))(applystring-appendargs)(applyoriginal+args)))))(+123)===>6(+"1""2""3")===>"123"
Στο R6RS κάθε δέσμευση, συμπεριλαμβανομένων των πρότυπων, ανήκει σε κάποια βιβλιοθήκη και όλες οι εξαγόμενες δεσμεύσεις δε μπορούν να τροποποιηθούν (immutable). (R6RS sec 7.1)[4] Για αυτόν το λόγο, ο ορισμός ξανά των βασικών διαδικασιών μέσω τροποποίησης απαγορεύεται. Αντί για αυτό, είναι δυνατό να εισαχθεί μια διαφορετική διαδικασία με το ίδιο όνομα με μια πρότυπη, κάτι που στην πραγματικότητα δε διαφέρει πολύ από την τεχνική που περιγράφηκε παραπάνω.
Στην Πρότυπη Scheme, οι διαδικασίες που μετατρέπουν από έναν τύπο δεδομένων σε έναν άλλο περιέχουν στο όνομά τους την ακολουθία χαρακτήρων "->", οι προτάσεις ιδιοτήτων (predicates) λήγουν σε "?", και οι διαδικασίες που αλλάζουν την τιμή δεδομένων ήδη δεσμευμένων στη μνήμη λήγουν σε "!". Αυτές οι συμβάσεις συχνά ακολουθούνται από τους προγραμματιστές της Scheme.
Σε επίσημο λόγο, όπως στα πρότυπα της Scheme, η λέξη "διαδικασία" ("procedure") χρησιμοποιείται αντί της "συνάρτησης" ("function") για να αναφέρεται σε μια λ-έκφραση ή σε μια πρωτογενή διαδικασία. Στον κανονικό λόγο οι λέξεις "διαδικασία" και "συνάρτηση" χρησιμοποιούνται εξίσου. Η εφαρμογή διαδικασίας μερικές φορές ονομάζεται επίσημασυνδυασμός (combination).
Όπως και σε άλλες διαλέκτους της Lisp, ο όρος "thunk" χρησιμοποιείται στη Scheme για να αναφέρεται σε μια διαδικασία χωρίς ορίσματα. Ο όρος "σωστή αναδρομή ουράς" ("proper tail recursion") αναφέρεται στην ιδιότητα που έχουν όλες οι υλοποιήσεις της Scheme, της εφαρμογής βελτιστοποίησης κλήσης ουράς ("tail-call optimization") ώστε να υποστηρίζουν άπειρο αριθμό ενεργών κλήσεων ουράς.
Η μορφή των τίτλων των εγγράφων των προτύπων από το R3RS, "Revisedn Report on the Algorithmic Language Scheme", είναι αναφορά στον τίτλο του εγγράφου του προτύπου τηςALGOL 60, "Revised Report on the Algorithmic Language Algol 60," και η σελίδα με την περίληψη (Summary) του R3RS μοιάζει με αυτήν της Αναφοράς της ALGOL 60.[21][22]