Κεφάλαιο 9 Συναρτησιακός προγραμματισμός Υπολογισμός με συναρτήσεις Σύνοψη Σκοπός του κεφαλαίου αυτού είναι η εισαγωγή του αναγνώστη στη φιλοσοφία του συναρτησιακού προγραμματισμού. Ο συναρτησιακός προγραμματισμός συνίσταται στη συγγραφή συναρτησιακών προγραμμάτων, τα οποία αποτελούνται από ορισμούς μαθηματικών συναρτήσεων. Η λύση ενός προβλήματος στο περιβάλλον του συναρτησιακού προγραμματισμού προκύπτει από τον υπολογισμό εκφράσεων που περιγράφουν την εφαρμογή συναρτήσεων σε κατάλληλα δεδομένα εισόδου. Ο συναρτησιακός προγραμματισμός έχει ισχυρή θεωρητική βάση, τον λάμδα λογισμό. Προαπαιτούμενη γνώση Για την κατανόηση του κεφαλαίου, ο αναγνώστης απαιτείται να έχει στοιχειώδεις γνώσεις μαθηματικών συναρτήσεων. 9.1 Ιστορική αναδρομή και γενικά Ο συναρτησιακός προγραμματισμός είναι η δεύτερη δηλωτική προγραμματιστική φιλοσοφία που θα μελετήσουμε, μετά τον λογικό προγραμματισμό [1, 2, 3, 4]. Η αντιμετώπιση ενός προβλήματος με συναρτησιακό προγραμματισμό γίνεται μέσω της δήλωσης αξιωμάτων που περιγράφουν τι ισχύει στον κόσμο του προβλήματος και όχι πώς λύνεται το πρόβλημα, όπως άλλωστε ισχύει σε κάθε δηλωτικό τρόπο προγραμματισμού. Σε αντίθεση με τον λογικό προγραμματισμό, στον συναρτησιακό προγραμματισμό τα αξιώματα δηλώνονται ως συναρτήσεις και η επίλυση ενός προβλήματος ανάγεται στον υπολογισμό εκφράσεων, που δεν είναι τίποτε άλλο από την εφαρμογή συναρτήσεων σε κατάλληλες τιμές εισόδου. Αντίθετα, όπως είδαμε μέχρι τώρα, στον λογικό προγραμματισμό τα αξιώματα είναι συνεπαγωγές και τα προβλήματα επιλύονται με την υποβολή ερωτήσεων και τον υπολογισμό των απαντήσεων σε αυτές με την εφαρμογή ενός μηχανισμού ενοποίησης και της αρχής της ανάλυσης. Ο συναρτησιακός προγραμματισμός έχει ισχυρή θεωρητική θεμελίωση, τον λάμδα λογισμό, που εισήχθη από τον Church τη δεκαετία του 1930. Ο λάμδα λογισμός είναι ένα αξιωματικά ορισμένο μαθηματικό σύστημα, πρόθεση του οποίου είναι να παράσχει τα αναγκαία μέσα, για να περιγραφεί η υπολογιστική συμπεριφορά των μαθηματικών συναρτήσεων. Τα μέσα αυτά είναι οι συντακτικοί κανόνες για τη διατύπωση λάμδα εκφράσεων και ένα σύνολο από κανόνες μετασχηματισμού μεταξύ λάμδα εκφράσεων. Στην Ενότητα 12.1, θα παρουσιαστούν αναλυτικότερα κάποια στοιχεία του λάμδα λογισμού. Εκτός από τον Church, και άλλοι σημαντικοί επιστήμονες του 20ού αιώνα, όπως οι Rosser, Kleene, Turing, Schönfinkel και Curry, μελέτησαν τον λάμδα λογισμό, καθώς και θέματα συναφή με αυτόν, όπως η σχέση του με τις έννοιες της αναδρομικότητας και της υπολογισιμότητας, αλλά και η λογική των συνδυαστών, την οποία θα συναντήσουμε στην Ενότητα 12.2. Έτσι, δημιουργήθηκε σιγά σιγά η βάση στην οποία έμελλε να στηριχθεί η ιδέα του συναρτησιακού προγραμματισμού. Η περιγραφή όσων ισχύουν στον κόσμο ενός προβλήματος που μας ενδιαφέρει γίνεται, στον συναρτησιακό προγραμματισμό, μέσω της συγγραφής ενός συναρτησιακού προγράμματος. Το πρόγραμμα αυτό αποτελείται μόνο από τους ορισμούς κατάλληλων μαθηματικών συναρτήσεων. Μπορούμε, λοιπόν, να δώσουμε τον εξής ορισμό: Συναρτησιακό πρόγραμμα είναι ένα σύνολο από αξιώματα που ισχύουν σε κάποιον κόσμο διατυπωμένα ως ορισμοί μαθηματικών συναρτήσεων. Κατά μια έννοια, ο λάμδα λογισμός μπορεί να θεωρηθεί η πρώτη γλώσσα συναρτησιακού προγραμματισμού, αν και την εποχή κατά την οποία προτάθηκε δεν υπήρχαν υπολογιστές για να «εκτελούν» συναρτησιακά προγράμματα εκφρασμένα με βάση τους κανόνες του λάμδα λογισμού. Από την άλλη πλευρά, πολλοί διατυπώνουν την άποψη ότι πρόγονος των συναρτησιακών γλωσσών προγραμματισμού είναι η Lisp, που κατασκευάστηκε από τον McCarthy τη δεκαετία του 1950. Αυτή η άποψη είναι εν μέρει σωστή και εν μέρει λανθασμένη. Η ορθότητά της έγκειται στο γεγονός ότι στη Lisp ένα πρόγραμμα είναι ένα σύνολο από ορισμούς συναρτήσεων. - 151 -
Η διατύπωση όμως αυτών των ορισμών στη Lisp δεν ακολουθεί αυστηρά τις απαιτήσεις του λάμδα λογισμού, με συνέπεια να εμφανίζονται διάφορες θεωρητικές, αλλά και πρακτικές, ανεπιθύμητες παρενέργειες. Εν πάση περιπτώσει, η πρόθεση του McCarthy δεν ήταν, με την εισαγωγή της Lisp, η εφαρμογή του λάμδα λογισμού στην πράξη, αλλά η κατασκευή μιας γλώσσας προγραμματισμού για επεξεργασία λιστών, με σκοπό την εφαρμογή της σε προβλήματα από την περιοχή της τεχνητής νοημοσύνης. Με βάση αυτό το σκεπτικό, η Lisp είναι μια επιτυχημένη γλώσσα προγραμματισμού, αλλά δεν μπορεί κατ ουδένα τρόπο να θεωρηθεί σήμερα εκπρόσωπος της φιλοσοφίας του συναρτησιακού προγραμματισμού. Όμως, παρά την ύπαρξη της Lisp από πολύ παλιά, έπρεπε, ουσιαστικά, να περάσουν τουλάχιστον δύο δεκαετίες από την εμφάνισή της, για να αρχίσουν να κατασκευάζονται «καθαρές» γλώσσες συναρτησιακού προγραμματισμού, που να βασίζονται, άλλες λιγότερο άλλες περισσότερο, στον λάμδα λογισμό. Τέτοιες γλώσσες, για να αναφέρουμε ενδεικτικά μερικές μόνο από αυτές, ήταν η Standard ML, η Hope, η SASL, η Miranda και η Haskell. Στα κεφάλαια που ακολουθούν, θα παρουσιάσουμε τις βασικές δυνατότητες που παρέχονται από τη φιλοσοφία του συναρτησιακού προγραμματισμού. Αν και οι δυνατότητες αυτές μπορούν να εκφραστούν μέσω μιας πληθώρας από σύγχρονες συναρτησιακές γλώσσες προγραμματισμού, εμείς θα χρησιμοποιήσουμε τη γλώσσα Haskell, για τις ανάγκες της παρουσίασης, αφού είναι μια ευρύτερα αποδεκτή και διαδεδομένη γλώσσα σήμερα στην κοινότητα του συναρτησιακού προγραμματισμού. Πριν αρχίσουμε, όμως, από το επόμενο κεφάλαιο, να βλέπουμε πώς μπορούμε να χρησιμοποιήσουμε τη Haskell για να λύσουμε διάφορα προβλήματα, ας μελετήσουμε λίγο καλύτερα την ιδέα του υπολογισμού που βασίζεται σε συναρτήσεις, από τη μαθηματική σκοπιά του. Στα μαθηματικά, μια συνάρτηση f είναι ένα μέσο με το οποίο μπορούμε να απεικονίσουμε τα στοιχεία ενός συνόλου D, του πεδίου ορισμού της συνάρτησης, στα στοιχεία ενός συνόλου R, του πεδίου τιμών της συνάρτησης. Συμβολικά: f : D R Παράδειγμα 9.1 Θα μπορούσαμε να ορίσουμε τη συνάρτηση square, η οποία απεικονίζει πραγματικούς αριθμούς στα τετράγωνά τους (square : R R), ως εξής: R R square(x) =x 2 Επίσης, θα μπορούσαμε να διατυπώσουμε και μια συνάρτηση sign, με την οποία να ορίζουμε το πρόσημο ακέραιων αριθμών (sign : Z plus, zero, minus}). Δηλαδή: Z } plus αν x>0 sign(x) = zero αν x =0 minus αν x<0 Έχοντας, τώρα, δώσει τους κατάλληλους ορισμούς των συναρτήσεων που μας ενδιαφέρουν, μπορούμε να εφαρμόσουμε αυτές τις συναρτήσεις σε στοιχεία των πεδίων ορισμού τους, για να πάρουμε στοιχεία από τα πεδία τιμών τους. Έτσι, για τις συναρτήσεις square και sign, είναι δυνατόν να γράψουμε square(3) = 9, square(1.7) = 2.89, sign(6) = plus, sign(0) = zero και sign(2) = minus, όπως προκύπτει με απλές εφαρμογές των ορισμών. Βέβαια, πολλές φορές είναι χρήσιμο να ορίζουμε συναρτήσεις των οποίων τα πεδία ορισμού να αποτελούνται από ζευγάρια στοιχείων ή τριάδες κτλ., δηλαδή να είναι καρτεσιανά γινόμενα συνόλων που περιέχουν απλά στοιχεία. Επίσης, όταν δίνουμε τον ορισμό μιας συνάρτησης, μπορούμε να χρησιμοποιούμε άλλες συναρτήσεις (που έχουμε ήδη ορίσει ή που πρόκειται να ορίσουμε). - 152 -
Παράδειγμα 9.2 Ο μέγιστος δύο πραγματικών αριθμών μπορεί να οριστεί με τη συνάρτηση max2 (έχοντας max2 : R 2 R), ως εξής: R R x αν x>y max2(x, y) = y σε άλλη περίπτωση Με τη βοήθεια της συνάρτησης max2, δεν είναι δύσκολο να ορίσουμε και μια συνάρτηση max3 (με max3: R 3 R), για τον μέγιστο R τριών R πραγματικών αριθμών. Δηλαδή: max3(x,y,z)=max2(x, max2(y,z)) Οπότε, max2(2.7, 1.4) = 1.4 και max3(5, 7, 3) = 7. Το πρώτο προκύπτει με απευθείας εφαρμογή του ορισμού, αλλά δεν είναι δύσκολο να δούμε ότι το max3(5, 7, 3) υπολογίζεται ως εξής: max3(5, 7, 3) = max2(5, max2(7, 3)) = max2(5, 7) = 7 Ο απαιτούμενος υπολογισμός έγινε με εφαρμογή του ορισμού της συνάρτησης max3, ο οποίος απαιτεί εφαρμογές του ορισμού της συνάρτησης max2. Με βάση μόνο τα παραδείγματα που παρουσιάσαμε, ίσως είναι δύσκολο να δεχθούμε ότι ορισμοί συναρτήσεων μπορεί να αποτελέσουν την πρώτη ύλη για ένα μοντέλο υπολογισμού στο οποίο να βασίζεται μια γλώσσα προγραμματισμού που να μας διευκολύνει να λύνουμε προβλήματα από τον πραγματικό κόσμο. Ωστόσο, η πραγματική δύναμη των συναρτήσεων εντοπίζεται στο γεγονός ότι αυτές μπορεί να έχουν αναδρομικούς ορισμούς, δηλαδή είναι δυνατόν να ορίσουμε μια συνάρτηση μέσω του εαυτού της. Αυτό μας λύνει τα χέρια, δίνοντάς μας τη δυνατότητα να περιγράψουμε μέσω συναρτήσεων οποιαδήποτε υπολογιστική διαδικασία. Δείτε το επόμενο παράδειγμα: Παράδειγμα 9.3 Η ακολουθία Fibonacci είναι μια ακολουθία που έχει πρώτο και δεύτερο όρο το 1, ενώ κάθε επόμενος όρος ισούται με το άθροισμα των δύο προηγούμενων όρων. Είναι αρκετά εύκολο να γράψουμε ένα πρόγραμμα σε κάποια διαδικαστική γλώσσα προγραμματισμού, όπως και σε κάποια γλώσσα λογικού προγραμματισμού, που να υπολογίζει τον n-οστό όρο της ακολουθίας Fibonacci, για δεδομένο n. Θα μπορούσαμε όμως, εξίσου εύκολα, να ορίσουμε μια συνάρτηση fib στους θετικούς ακέραιους αριθμούς (fib : Z + Z + ), η οποία να απεικονίζει την τάξη ενός όρου στην τιμή του ως εξής: 1 αν n =1 fib(n) = 1 αν n =2 fib(n 1) + fib(n 2) αν n>2 Έτσι, μπορούμε να έχουμε, για παράδειγμα, fib(5) = fib(4)+fib(3) = (fib(3)+fib(2))+ (fib(2) + fib(1)) = ((fib(2) + fib(1))+1)+(1+1)=((1+1)+1)+2=(2+1)+2= 3+2=5. - 153 -
Άσκηση 9.1 Για τις συναρτήσεις: succ : Z Z hypot : R 2 + R + signmax4 :R 4 R gcd : Z+ 2 Z + ο επόμενος ενός ακέραιου αριθμού το μήκος της υποτείνουσας ενός ορθογώνιου τριγώνου με δεδομένες κάθετες πλευρές το πρόσημο του μεγίστου τεσσάρων πραγματικών αριθμών ο μέγιστος κοινός διαιρέτης δύο θετικών ακέραιων αριθμών συμπληρώστε κατάλληλα τους ορισμούς που ακολουθούν: succ(x) = x A 1 hypot(x, y) = B + y 2 signmax4(x, y, z, w) = sign( C (max2(x, y), max2( D, w))) x αν E gcd(x, y) = gcd(x, F ) αν x<y G (x y, H ) αν x>y Ποιες είναι οι τιμές succ(4), hypot(5, 12), signmax4(3, 0, 1, 2) και gcd(24, 9); Άσκηση 9.2 Δώστε τον ορισμό μιας συνάρτησης για το παραγοντικό ενός μη αρνητικού ακέραιου αριθμού. Πώς μπορεί να υπολογιστεί το παραγοντικό του 6, με τη βοήθεια αυτού του ορισμού; Απαντήσεις ασκήσεων Απάντηση άσκησης 9.1 Οι σωστές απαντήσεις είναι: A: + E: x = y B: x 2 F: y x C: max2 G: gcd D: z H: y Όσον αφορά την εφαρμογή των συναρτήσεων στις δεδομένες τιμές, έχουμε: succ(4) =(4)+1=3 hypot(5, 12) = 5 2 + 12 2 = 25 + 144 = 169 = 13 signmax4(3, 0, 1, 2) = sign(max2(max2(3, 0), max2(1, 2))) = sign(max2(0, 2)) = sign(2) = plus gcd(24, 9) = gcd(24 9, 9) = gcd(15, 9) = gcd(15 9, 9) = gcd(6, 9) = gcd(6, 9 6) = gcd(6, 3) = gcd(6 3, 3) = gcd(3, 3) = 3-154 -
Απάντηση άσκησης 9.2 Θα μπορούσαμε να ορίσουμε το παραγοντικό ενός μη αρνητικού ακέραιου αριθμού μέσω μιας συνάρτησης f act (με fact : N N ), ως εξής: N N 1 αν n =0 f act(n) = n fact(n 1) αν n>0 Για το fact(6), έχουμε: fact(6) = 6 fact(6 1) = 6 fact(5) = 6 (5 fact(5 1)) = (6 5) fact(4) = 30 (4 fact(4 1)) = (30 4) fact(3) = 120 (3 fact(3 1)) = (120 3) fact(2) = 360 (2 fact(2 1)) = (360 2) fact(1) = 720 (1 fact(1 1)) = (720 1) fact(0) = 720 1 = 720 Προβλήματα Πρόβλημα 9.1 Δώστε δύο εναλλακτικούς ορισμούς συνάρτησης για τον υπολογισμό του αθροίσματος όλων των ακέραιων αριθμών από το 1 έως το n. Ο ένας ορισμός να είναι αναδρομικός, ενώ ο άλλος όχι. Πρόβλημα 9.2 Ορίστε συνάρτηση που να υπολογίζει το ελάχιστο κοινό πολλαπλάσιο δύο θετικών ακέραιων αριθμών. Πρόβλημα 9.3 Ορίστε συνάρτηση που να υπολογίζει το άθροισμα των ψηφίων δεδομένου θετικού ακέραιου αριθμού. Πρόβλημα 9.4 Ορίστε συναρτήσεις επάνω στο σύνολο B = T, F } (αληθές/ψευδές), για τις λογικές πράξεις της άρνησης, της σύζευξης, της διάζευξης, της συνεπαγωγής και της ισοδυναμίας. Βιβλιογραφικές αναφορές [1] J. Bakus, Can Programming Be Liberated from the von Neumann Style? A Functional Style and Its Algebra of Programs, CACM, 21(8), 613-641, 1978. [2] A. J. Field and P. G. Harrison, Functional Programming, Addison Wesley, 1988. [3] B. Goldberg, Functional Programming Languages, ACM Comput. Surv., 28(1), 249-251, 1996. [4] M. Hanus and H. Kuchen, Integration of Functional and Logic Programming, ACM Comput. Surv., 28(2), 306-308, 1996. - 155 -