Στατική Συμβολική Παραγώγιση Λάμδα Εκφράσεων στην C++ Η βιβλιοθήκη λ&d++.
Τί είναι λοιπόν οι λάμδα εκφράσεις; λάμδα εκφράσεις>τί είναι;>στον λ-λογισμό Τί είναι λοιπόν οι λάμδα εκφράσεις; Ο όρος λάμδα έκφραση προέρχεται από τον λάμδα λογισμό του Alonzo Church ο οποίος είναι ένα υπολογιστικό μοντέλο αντίστοιχο των μηχανών Turing, δηλαδή μια μαθηματική θεωρεία που σκοπό έχει να ορίσει τί είναι υπολογισμός και να ερευνήσει το τί είναι υπολογίσιμο και τί όχι. Ο λ-λογισμός ορίζει την γλώσσα των λ-όρων σαν την γλώσσα που παράγεται από τον κανόνα: <λ-όρος>::=<μεταβλητή>|(λ<μεταβλητή>.<λ-όρος>)|(<λ-όρος> <λ-όρος>) Ή αλλιώς: Μια μεταβλητή είναι λ-όρος. Αν x μεταβλητή και t λ-όρος τότε το (λx.t) είναι λ-όρος. Αν t και u είναι λ-όροι τότε (t u) είναι λ-όρος. Οπότε το (λx.x) το ((λx.(y x)) (λx.x)) και το (λf.(λx.(f (λy.((x x) y)))))(λx.(f ((λy.((x x) y))))) είναι όλα λ-όροι.
Οι λ-όροι της 2ης μορφής ονομάζονται λάμδα συναρτήσεις, λάμδα εκφράσεις>τί είναι;>στον λ-λογισμό Οι λ-όροι της 2ης μορφής ονομάζονται λάμδα συναρτήσεις, ή λάμδα εκφράσεις, ή συναρτησιακές αφαιρέσεις, ή ανώνυμες συναρτήσεις. Αλλά αυτή είναι η μορφή των λάμδα εκφράσεων στον λ-λογισμό.
Ο ορισμός τους μπορεί να εμφανίζεται μέσα σε μια έκφραση. λάμδα εκφράσεις>τί είναι;>στις γλώσσες προγραμματισμού Στις γλώσσες προγραμματισμού οι λ-εκφράσεις είναι απλές συναρτήσεις με 2 κυρίως διαφορές: Δεν έχουν όνομα! Ο ορισμός τους μπορεί να εμφανίζεται μέσα σε μια έκφραση.
βιβλιοθήκη>χρήση>includes & δηλώσεις Για να χρησιμοποιήσουμε την βιβλιοθήκη: Συμπεριλαμβάνουμε το κατάλληλο αρχείο επικεφαλίδας: #include "λnd++.h" Δηλώνουμε μια μεταβλητή τύπου “Arg”: Arg x; (ο τύπος Arg είναι ένας τύπος πολυμορφικής ταυτοτικής συνάρτησης Τ->Τ) Γράφουμε την λάμδα συνάρτηση που θέλουμε. π.χ.: x+1 ή κάτι πιο περίπλοκο όπως: 4*x*x+3*x+2.5 ή (2*x+2.2)*(cos(-x)+x*x*sin((-x)/(x)+5))/(3*cos(3.2*x+1)-1e-10)
βιβλιοθήκη>χρήση>βασική χρήση Βεβαίως το να δηλώσουμε μια λάμδα συνάρτηση δεν είναι από μόνο του πολύ χρήσιμο. Πιθανότατα θέλουμε... να την εφαρμόσουμε κάπου, Να κάνουμε κάτι με την τιμή που θα επιστρέψει, Και όλα αυτά μέσα σε ένα πρόγραμμα. #include <iostream> using std::wcout; using std::endl; #include "λnd++.h" int main() { Arg x; wcout << (4*x*x+3*x+2.5)(-2.5) << endl; return 0; } // end function main wcout << (4*x*x+3*x+2.5)(-2.5) << endl; (4*x*x+3*x+2.5)(-2.5) 4*x*x+3*x+2.5
βιβλιοθήκη>χρήση>απόδωση ονόματος Επίσης μπορεί να θέλουμε να της δώσουμε ένα όνομα ώστε να την χρησιμοποιήσουμε πολλές φορές: #include <iostream> using std::wcout; using std::endl; #include "λnd++.h" int main() { Arg x; auto f = 4*x*x+3*x+2.5; wcout << f(-2.5) << endl; wcout << f(1) << endl; return 0; } // end function main
βιβλιοθήκη>χρήση>απόδωση ονόματος>ο καθοριστής τύπου auto auto i = 1; int i = 1; auto x = 2.2; double x = 2.2; auto y = 2.2f; float y = 2.2f; Αναγκαίο καθώς κάθε λάμδα έκφραση έχει έναν μοναδικό τύπο που κωδικοποιεί την εσωτερική δομή της έκφρασης Εξήγηση του auto
βιβλιοθήκη>χρήση>αιχμαλωσία #include <iostream> using std::wcout; using std::wcin; using std::endl; #include "λnd++.h" const int a = 4; const int b = 3; int main() { Arg x; float c; wcout << "Type a number: "; wcin >> c; auto f = a*x*x+b*x+c; wcout << f(-2.5) << endl; wcout << f(1) << endl; return 0; } // end function main Και αν θέλουμε να χρησιμοποιήσουμε στο σώμα της λάμδα συνάρτησης κάποια μεταβλητή από μια περιβάλουσα εμβέλεια; Απλά την χρησιμοποιούμε! Οι τιμές των ‘αιχμαλοτισμένων’ μεταβλητών αντιγράφονται στο αντικείμενο που αναπαρτιστά την λ-έκφραση και μπορούν να χρησιμοποιηθούν από αυτό. Προς το παρόν δεν υποστιρίζεται ‘αιχμαλότιση’ κατ’ αναφορά.
βιβλιοθήκη>χρήση>επιστροφή από συνάρτηση #include <iostream> using std::wcout; using std::endl; #include "λnd++.h" template<typename T> auto sub(T x)->decltype(x-Arg()) { Arg y; return x-y; } // end function sub int main() wcout << sub(2)(5) << endl; wcout << sub(2.2)(1) << endl; wcout << sub(2)(0.5) << endl; return 0; } // end function main Έτσι όμως μπορείς να επιστρέψεις με ασφάλεια μια λ-έκφραση από μια άλλη συνάρτηση. π.χ.:
βιβλιοθήκη>χρήση>πολυμορφισμός Μέχρι τώρα έχουμε δεί ότι μπορούμε να εφαρμόσουμε μια λ-έκφραση σε αντικείμενα τύπου int και double. Οι λ-συναρτήσεις μας όμως χρησιμοποιούν παραμετρικό πολυμορφισμό και μπορούμε να τις εφαρμόσουμε σε οποιοδήποτε αντικείμενο έχει νόημα για το εκάστοτε σώμα τους. Τα λάθη ασφαλώς εντοπίζονται στον χρόνο μεταγλώττισης! π.χ.: complex<int> i(0,1); auto f = 3*x; unsigned long long ull = f(2ull); float fl = f(2.5f); complex<int> ic = f(2+3*i); Vector2D<> dv = f(Vector2D<>(-2.0,5)); αλλά: complex<float> fc = f(2+3*i);// illegal! διότι κάποιος αποφάσισε ότι το παρακάτω είναι illegal! complex<float> fc2 = 3*(2+3*i);// illegal!
βιβλιοθήκη>χρήση>παραγώγιση Για να παραγωγίσουμε μια λ-έκφραση απλώς την δίνουμε σαν όρισμα στην συνάρτηση Differentiate. μπορεί να είναι ανώνυμη: Differentiate(cos(3*x+2)) ή να της έχουμε δώσει ένα όνομα: auto f = cos(3*x+2); // .. Differentiate(f) Το αποτέλεσμα είναι μια λ-έκφραση της παραγώγου. την οποία μπορούμε να εφαρμόσουμε κάπου: Differentiate(cos(3*x+2))(0.01); Differentiate(f)(0.01); ή της δώσουμε ένα όνομα: auto g = Differentiate(cos(3*x+2)); auto h = Differentiate(f);
βιβλιοθήκη>χρήση>παραγώγιση>ονοκληρωμένο πρόγραμμα #include <iostream> using std::wcout; using std::endl; #include "λnd++.h" int main() { Arg x; auto f = cos(3*x+2); wcout << Differentiate(cos(3*x+2))(0.01) << endl; wcout << Differentiate(f)(0.01) << endl; auto g = Differentiate(cos(3*x+2)); auto h = Differentiate(f); wcout << g(0.01) << endl; wcout << h(0.01) << endl; return 0; } // end function main
βιβλιοθήκη>γιατί στατικά; Τί κερδίζουμε κάνοντας την παραγώγιση στατικά; Ταχύτατη εκτέλεση! Όχι μόνο το πρόγραμμα δεν επιβαρύνεται με την ανάγκη να εκτελέσει τους μετασχηματισμούς του δένρου έκφρασης στον χρόνο εκτέλεσης αλλά ο κώδικας της παραγώγου περνάει από τους εξαιρετικά ικανούς και επιθετικούς βελτιστοποιητές των σύγχρονων μεταγλωττιστών! Παραδείγματα παραγόμενου κώδικα (g++ με βελτιστοποιήσεις ενεργές):
Σταθερές εκφράσεις: βιβλιοθήκη>γιατί στατικά;>σταθερές εκφράσεις το wcout << (cos(2*x+3))(3) << endl; παράγει movsd xmm1, QWORD PTR .LC0[rip] #, lea rcx, _ZSt5wcout[rip] #, call _ZNSt13basic_ostreamIwSt11char_traitsIwEE9_M_insertIdEERS2_T_ # mov rcx, rax #, D.28217 call _ZSt4endlIwSt11char_traitsIwEERSt13basic_ostreamIT_T0_ES6_ #
βιβλιοθήκη>χρήση>includes>τιμές εισαγόμενες στον χρόνο εκτέλεσης Μεταβλητές: double c; wcin >> c; lea rdx, 40[rsp] #, lea rcx, _ZSt4wcin[rip] #, call _ZNSt13basic_istreamIwSt11char_traitsIwEE10_M_extractIdEERS2_RT_ wcout << (cos(2*x+3))(c) << endl; movsd xmm0, QWORD PTR 40[rsp] # tmp68, c addsd xmm0, xmm0 # tmp68, tmp68 addsd xmm0, QWORD PTR .LC0[rip] # tmp68, call cos # lea rcx, _ZSt5wcout[rip] #, movapd xmm1, xmm0 # D.28230, call _ZNSt13basic_ostreamIwSt11char_traitsIwEE9_M_insertIdEERS2_T_ # mov rcx, rax #, D.28241 call _ZSt4endlIwSt11char_traitsIwEERSt13basic_ostreamIT_T0_ES6_ #
βιβλιοθήκη>χρήση>includes>τιμές εισαγόμενες στον χρόνο εκτέλεσης Μεταβλητές: Και οι γραμμές που μας απασχολούν: movsd xmm0, QWORD PTR 40[rsp] # tmp68, c addsd xmm0, xmm0 # tmp68, tmp68 addsd xmm0, QWORD PTR .LC0[rip] # tmp68, call cos # είναι ταυτόσημες με αυτές που παράγονται από τον γραμμένο-με-το-χέρι κώδικα: inline double f(double x) { return cos(2*x+3); } // ... wcout << f(c) << endl;
Και πού χρησιμεύουν οι λ-εκφράσεις και η παραγώγισή τους; βιβλιοθήκη>εφαρμογές>γενικά Και πού χρησιμεύουν οι λ-εκφράσεις και η παραγώγισή τους; Γενικά οπουδήποτε γνωρίζουμε τον τύπο μιας συνάρτησης στον χρόνο μεταγλώττισης, χρειαζόμαστε την παράγωγό της και δεν θέλουμε να βγούμε από το περιβάλλον ανάπτυξής μας για να κάνουμε αλλού τον υπολογισμό! Αλλά ας δούμε 2 συγγεκριμένα παραδείγματα: Λακωνικές λάμδα εκφράσεις για τους αλγορίθμους της STL. Κλήση μιας Newton-Raphson χώρίς να πιάσουμε μολύβι ή να ανοίξουμε το Mathematica! Συνδυασμένες γραφικές παραστάσεις συναρτήσεων και των παραγώγων τους ή καλύτερα σχεδίαση εφαπτομένων σε σημεία που επιλέγει ο χρήστης (δεν πρόλαβα να φτιάξω το παράδειγμα)
Λακωνικές λάμδα εκφράσεις για τους αλγορίθμους της STL. βιβλιοθήκη>εφαρμογές>λακωνικές λάμδα εκφράσεις Λακωνικές λάμδα εκφράσεις για τους αλγορίθμους της STL. Όταν η έκφρασή σου είναι αρκετά μικρή, το συντακτικό ‘overhead’ των C++11 lambdas δεν είναι αποδεκτό: transform(begin(v1),end(v1),begin(v2),x-0.5); VS transform(begin(v1),end(v1),begin(v2),[](int x){return x-0.5;});
βιβλιοθήκη>εφαρμογές>newton-raphson>πριν double Newton_Raphson(double (*f)(double),double (*df)(double), double new_x,double accuracy,unsigned int *iterations) { double old_x; *iterations = 0; do (*iterations)++; old_x = new_x; new_x = old_x-(*f)(old_x)/(*df)(old_x); } while(fabs(new_x-old_x)>accuracy); return new_x; No comments for the pointer parameter! These were the dark ages when we wrote in C!
...και την τροποποιούμε για να χρησιμοποιεί την λ&d++: βιβλιοθήκη>εφαρμογές>newton-raphson>μετά ...και την τροποποιούμε για να χρησιμοποιεί την λ&d++: template<typename Func> double Newton_Raphson(Func f,double new_x,double accuracy, unsigned int *iterations) { double old_x; *iterations = 0; do (*iterations)++; old_x = new_x; new_x = old_x-f(old_x)/Differentiate(f)(old_x); } while(fabs(new_x-old_x)>accuracy); return new_x; Βεβαίως μπορούσαμε να κάνουμε και άλλες αλλαγές όπως να επιτρέπουμε στον καλούντα να επιλέγει την αριθμιτική που θα χρησιμοποιηθεί...
και τέλος υπολογίζουμε: βιβλιοθήκη>εφαρμογές>newton-raphson>κλήση και τέλος υπολογίζουμε: int main() { Arg x; unsigned int iterations = 0; wcout << L"root = " << Newton_Raphson((x-3)*(x+2),2,1e-6,&iterations) << L" after " << iterations << L" iterations." << endl; << Newton_Raphson((x-3)*(x+2)*(x-2.2)*(x+5)*(x-7)*3.3, 10,1e-6,&iterations) << L" after " << iterations << L" iterations." << endl; return 0; } // end function main Ποιός θέλει να παραγωγίσει την cos(sin(cos(x*sin(x)-x)+2))*(x+5)*(x-7)*3.3-2.5 με το χέρι?!!
βιβλιοθήκη>χρήση>includes Βεβαίως όπως σε όλες τις βιβλιοθήκες υπάρχουν περιορισμοί και περιθόριο για μελλοντική βελτίωση. Παραδείγματος χάρη πώς θα εκφράσεις μια λάμδα συνάρτηση που επιστρέφει πάντα την ίδια σταθερή τιμή;
References/Further Reading: http://matt.might.net/articles/static-closures-in-c-plus-plus/ domain specific embedded languages in C++ http://www.cplusplus.com/reference/std/complex/ complex library reference ML for the Working Programmer by Lawrence C. Paulson §9.5 introduction to λ-calculus http://www.boost.org/doc/libs/1_48_0/libs/numeric/ublas/doc/index.htm Boost::uBLAS uses expression templates for efficient abstraction http://www.boost.org/doc/libs/1_48_0/libs/spirit/doc/html/index.html Boost::Spirit represents LL parsers as inline EBNF in C++ http://www.boost.org/doc/libs/1_48_0/libs/phoenix/doc/html/index.html Boost::Phoenix has extensive support of polymorphic lambdas http://www10.informatik.uni-erlangen.de/~pflaum/pflaum/ProSeminar/exprtmpl.html original (?) Expression Templates article http://www.boost.org/doc/libs/1_48_0/doc/html/lambda.html Boost::Lambda provides support for lambdas as well http://www.cc.gatech.edu/~yannis/fc++/fc++-sigplan.pdf The FC++ library supports functional programming in C++ C++ How to Program (5th Edition) by P.J. Deitel My first C++ book. A bit outdated.
References/Further Reading: Ivor Horton's Beginning Visual C++ 2010 My 2nd C++ book. Also covers C++/CLI ISO/IEC 14882-2011 International Standard The current C++ standard http://www.open-std.org/JTC1/SC22/WG21/docs/papers/2009/n2927.pdf Final proposal for C++0x Lambdas http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1720.html Final proposal for static assertions http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2087.pdf A Brief Introduction to Variadic Templates http://www2.research.att.com/~bs/C++0xFAQ.html Stroustrup’s C++11 FAQ
Ευχαριστώ! Καλά Χριστούγεννα! Ευχαριστώ! Καλά Χριστούγεννα!